/[soft]/identity/CatDap/trunk/lib/CatDap/Controller/user.pm
ViewVC logotype

Contents of /identity/CatDap/trunk/lib/CatDap/Controller/user.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 561 - (show annotations) (download)
Wed Feb 16 13:33:29 2011 UTC (13 years, 2 months ago) by buchan
File size: 15223 byte(s)
Add error pages for user self-editing buttons, and correctly collect ldap errors
Respect Controller::User::editable_attrs in user editing page
Add preferredLanguage to editable_attrs

1 package CatDap::Controller::user;
2 use Moose;
3 use namespace::autoclean;
4 use Net::LDAP;
5 use Net::LDAP::Schema;
6 use Net::LDAP::Extension::SetPassword;
7 use Net::LDAP::Control::PasswordPolicy 0.02;
8 use Crypt::CBC;
9 use Data::UUID;
10 #use Data::Dumper;
11
12 BEGIN {extends 'Catalyst::Controller'; }
13
14 =head1 NAME
15
16 CatDap::Controller::user - Catalyst Controller
17
18 =head1 DESCRIPTION
19
20 Catalyst Controller.
21
22 =head1 METHODS
23
24 =cut
25
26 =head2 auto
27
28 Ensure the user is logged in. In order to bind as the user, we use
29 CatDap::Model::User, which uses Catalyst::Model::LDAP::FromAuthentication,
30 which effectively requires calling $c->authenticate on every request.
31
32 To do this, we keep the password, encrypted with blowfish, using the
33 (for now), first 3 octets of IPv4 request address and a UUID string (stored in
34 a cookie) as the key. To access the password, an attacker would need:
35 - the first 3 octets of IPv4 request (not stored anywhere, but accessible
36 in server logs)
37 - the encrpyted password (only available server-side in the session variable)
38 - the UUID key portion (only available on the browser-side in a cookie)
39
40 So, if the user does "not exist", we authenticate them, if it succeeds we encrypt
41 the password and store it in the session.
42
43 If the user is logged in, we get the encrypted password from the session, decrypt
44 it (we need to handle failure to decrypt it better)
45
46 =cut
47
48 sub auto : Private {
49 my ( $self, $c ) = @_;
50 if ($c->req->path eq 'user/firstlogin') {
51 return 1;
52 }
53 my $cipher;
54 my $password;
55 my $mesg;
56 my $dn;
57 my @errors;
58 my $keyprefix = $self->get_keyprefix($c);
59 if (! defined $c->user or not $c->req->cookie('key')) {
60 if (not $c->req->param('password')) {
61 push @errors,$c->loc('Your session has expired');
62 $c->stash(template => 'index.tt',errors => \@errors);
63 $c->detach;
64 }
65
66 $c->log->debug("No session, logging user in");
67 if (! $c->authenticate({ username => $c->req->param('username'),
68 password => $c->req->param('password') || $c->req->param('key')}) ) {
69
70 #TODO: ppolicy ....
71 $c->stash(errors => ['Incorrect username or password']);
72 $c->stash(template => 'index.tt');
73
74 #$c->forward('/index');
75 $c->detach('/user/login');
76 } else {
77
78 #if (defined $c->user->pwdReset) {
79 # $c->res->redirect('/user');
80 #}
81 #$c->persist_user;
82 $c->log->debug('Logging user in to LDAP');
83
84 my $ug = Data::UUID->new;
85 my $key = $ug->create_str();
86 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
87 -cipher => 'Blowfish'
88 ) or die $!;
89 $c->session->{enc_password} = $cipher->encrypt($c->req->param('password') || $c->req->param('key'));
90 $c->response->cookies->{'key'} = { value => $key, expires => '+10m' };
91 $c->stash(pages => roles2pages($c->user->roles));
92 $c->session->{dn} = $c->user->ldap_entry->dn;
93 $c->session->{user} = $c->req->param('username');
94 $password = $c->req->param('password') || $c->req->param('key');
95 return 1;
96 }
97
98 } else {
99 my $key = $c->req->cookie('key')->value;
100 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
101 -cipher => 'Blowfish'
102 ) or die $!;
103 $password = $cipher->decrypt($c->session->{enc_password});
104 $c->log->debug("Re-authenticating user " . $c->user->username);
105 $c->authenticate({username => $c->user->username,password => $password});
106 $c->res->cookies->{'key'} = {value => $key, expires => '+10m'};
107
108 $c->stash(pages => roles2pages($c->user->roles));
109 $c->log->info($@) if $@;
110 return 1;
111 }
112
113 }
114
115 sub get_keyprefix : Private {
116 my ( $self, $c ) = @_;
117 my $keyprefix;
118 if ($c->req->address =~ m/:/) {
119 my $address = $c->req->address;
120 $address =~ s/\[\]://;
121
122 # if we shift right 104 bits from 128 we have 24 bits left or 3 bytes.
123 $keyprefix = sprintf( "%06x", $address >> 104 );
124 }
125 else {
126 $keyprefix = sprintf( "%02x%02x%02x", split /\./, $c->req->address );
127 }
128 return $keyprefix;
129 }
130
131 =head2 index
132
133 =cut
134
135 sub index :Path :Args(0) {
136 my ( $self, $c ) = @_;
137 my $cipher;
138 my $password;
139 my $mesg;
140 my $dn;
141 my $userfilter;
142
143 if (not defined $c->user ) {
144 $c->stash(template => 'index.tt');
145 $c->forward('/index');
146 $c->detach;
147 }
148 my $schemaldap = Net::LDAP->new(${$c->config}{'Model::Proxy'}{'host'}) or warn "LDAP bind failed: $!";
149 $schemaldap->start_tls if ${$c->config}{'Model::Proxy'}{'start_tls'};
150 $schemaldap->bind;
151 my $schema = $schemaldap->schema or die ("Searching schema failed: $!");
152 my $attrdef;
153
154 my $user = $c->user->username;
155 my $entry;
156 $userfilter = $c->user->store->user_filter;
157 $userfilter =~ s/\%s/$user/g,
158 $c->log->debug("Searching for user $user with filter $userfilter");
159 $mesg = $c->model('User')->search($userfilter);
160 $entry = $mesg->entry;
161 my %mods;
162 my %params = %{$c->req->parameters};
163 my $update = 0;
164 foreach my $req (keys %params) {
165 next if $req !~ /(.+)_new/;
166 my $attrname = $1;
167 next if $params{$attrname . '_new'} eq $params{$attrname . '_old'};
168 $c->log->info("Received update request for attribute $attrname");
169 $update = 1;
170 $attrdef = $schema->attribute($attrname) or die ("getting schema failed: $!");
171 if ($$attrdef{'single-value'}) {
172 $entry->replace($attrname => $params{$attrname . '_new' }) or $c->log->info($!);
173 } else {
174 $entry->delete($attrname => $params{$attrname . '_old'});
175 $entry->add($attrname => $params{$attrname . '_new'});
176 }
177 if ($update) {
178 $mesg = $entry->update;
179 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
180 }
181 }
182
183 $mesg = $c->model('User')->search($userfilter);
184 $c->log->debug($mesg->error) if $mesg->code;
185 $entry = $mesg->entry;
186 $c->log->debug($mesg->error) if $mesg->code;
187
188 my @values;
189 my @attributes = $entry->attributes;
190 my @may;
191 my @addable_attrs = @attributes;
192 my @ocs;
193 my @must;
194 @ocs = $entry->get_value("objectClass");
195 foreach my $oc (@ocs) {
196 foreach my $attr ($schema->must($oc)) {
197 push @must,$$attr{'name'} if not grep /$$attr{'name'}/,@must;
198 }
199 }
200
201 foreach my $attr (sort @attributes) {
202 next if ($attr eq "objectClass");
203 next if grep /$attr/,@{${$c->config}{'Controller::User'}{'skip_attrs'}};
204 my @vals = $entry->get_value($attr);
205 foreach (@vals) { $_ = Encode::decode_utf8( $_ ); }
206 $attrdef = $schema->attribute($attr) or die ("getting schema failed: $!");
207 my %valhash = (
208 name => $attr,
209 values => \@vals,
210 desc => $$attrdef{'desc'},
211 );
212 if (! grep /^$attr$/, @{${$c->config}{'Controller::User'}{'uneditable_attrs'}}) {
213 $valhash{'editable'} = 1;
214 }
215 if (! $$attrdef{'single-value'} && $valhash{'editable'}) { $valhash{'addable'} = 1; }
216 if (! grep /$attr/,@must) { $valhash{'removable'} = 1; }
217 push @values, \%valhash;
218 }
219 foreach my $oc (@ocs) {
220 foreach my $attrdef ($schema->may($oc)) {
221 my $attrname = $$attrdef{'name'};
222 grep /$attrname/,@may or
223 grep /$attrname/,@attributes or
224 grep /$attrname/,@{${$c->config}{'Controller::User'}{'uneditable_attrs'}} or
225 grep /$attrname/,@{${$c->config}{'Controller::User'}{'skip_attrs'}} or
226 grep /$attrname/,@{${$c->config}{'Controller::User'}{'editable_attrs'}} and
227 push @may, $attrname;
228 }
229 }
230 @may = sort @may;
231 $c->stash({ username => $user,
232 values => \@values,
233 attrdef => $attrdef,
234 may => \@may,
235 must => \@must,
236 });
237 $c->stash(subpages => gensubpages());
238 }
239
240 sub add : Local {
241 my ( $self, $c) = @_;
242 my $attr = $c->req->param('attribute');
243 my $value = $c->req->param('value');
244 my $user = $c->user->username;
245 my $userfilter = sprintf($c->user->store->user_filter ,$c->user->username);
246 $c->log->debug("Searching for user $user with $userfilter");
247 my $mesg = $c->model('User')->search($userfilter);
248 my $entry = $mesg->entry;
249 $entry->add( $attr => $value);
250 $c->log->info("Adding $attr = $value to user $user");
251 $mesg = $entry->update;
252 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
253 $c->log->info($mesg->error);
254 $c->res->redirect('/user');
255 }
256
257 sub delete : Local : Args(2) {
258 my ( $self, $c, $attrname,$attrvalue) = @_;
259 my ($mesg,$entry,$user,$userfilter);
260 $user = $c->user->username;
261 $userfilter = $c->user->store->user_filter;
262 $userfilter =~ s/%s/$user/g;
263 $c->log->debug("Searching for user $user with filter $userfilter");
264 $mesg = $c->model('User')->search($userfilter);
265 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
266 $c->log->info($mesg->error) if $mesg->code;
267 $entry = $mesg->entry;
268 $c->log->info("Deleting $attrname: $attrvalue from dn " . $entry->dn);
269 $entry->delete($attrname => $attrvalue);
270 $mesg = $entry->update;
271 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
272 $c->log->info("Result of update: " . $mesg->error . "," . $mesg->code) if $mesg->code;
273 $c->res->redirect('/user') unless $mesg->code;
274 $c->stash({ attrname => $attrname, attrvalue => $attrvalue});
275 }
276
277 sub password : Local {
278 my ( $self, $c) = @_;
279 my ($mesg,$newpass,$cipher);
280 $c->stash(subpages => gensubpages());
281 if ( not defined $c->req->param('password') or not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
282
283 #if ( not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
284
285 $c->detach;
286 }
287 # Re-authenticate to check the user has the right password
288 if (not $c->authenticate({
289 'username' => $c->user->username,
290 'password' => $c->req->param('password'),
291 })
292 ) {
293 $c->stash(errors => [ $c->loc('Password incorrect') ]);
294 $c->detach;
295 }
296 if ($c->req->param('newpassword1') eq $c->req->param('newpassword2')) {
297 $newpass = $c->req->param('newpassword1');
298 } else {
299 push @{${$c->stash}{'errors'}},$c->loc('New passwords dont match');
300 $c->detach;
301 }
302 my $pp = Net::LDAP::Control::PasswordPolicy->new;
303 $mesg = $c->model('User')->set_password(
304 #oldpasswd => $c->req->param('password'),
305 newpasswd => $newpass,
306 control => [ $pp ],
307 );
308 if ($mesg->code) {
309 my $perror = $mesg->error;
310 push @{${$c->stash}{'errors'}},"Password change failed: $perror";
311 $c->detach;
312 } else {
313
314 # re-encrypt the new password and forward to user view
315 my $keyprefix = $self->get_keyprefix($c);
316 my $key = $c->req->cookie('key')->value;
317 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
318 -cipher => 'Blowfish'
319 ) or die $!;
320 $c->session->{enc_password} = $cipher->encrypt($newpass);
321 push @{${$c->stash}{'errors'}},"Password change succeeded";
322 #$c->res->redirect('/user');
323 }
324
325 }
326
327 sub firstlogin : Local {
328 my ( $self, $c ) = @_;
329 my ($mesg,$newpass,$cipher);
330
331 # we want to do our own authentication and caching here, as we
332 # dont want what auto does, and auto returns early for this path
333
334 if ( not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
335 if (! $c->authenticate({
336 username => $c->req->param('username'),
337 password => $c->req->param('key')}) ) {
338 $c->stash(errors => ['An error occurred']);
339 $c->log->info("Failed to authenticate user in first login: " . $c->req->param('key'));
340 $c->res->redirect('/user');
341 }
342 # cache password for next request with form data
343 my $keyprefix = $self->get_keyprefix($c);
344 my $key = Data::UUID->new->create_str();
345 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
346 -cipher => 'Blowfish'
347 ) or die $!;
348 $c->session->{enc_password} = $cipher->encrypt($c->req->param('key'));
349 $c->response->cookies->{'key'} = { value => $key, expires => '+10m' };
350 $c->detach;
351 }
352
353 if ($c->req->param('newpassword1') eq $c->req->param('newpassword2')) {
354 $newpass = $c->req->param('newpassword1');
355 } else {
356 push @{${$c->stash}{'errors'}},"New passwords dont match";
357 }
358
359 #Re-authenticate user
360 my $keyprefix = $self->get_keyprefix($c);
361 my $key = $c->req->cookie('key')->value;
362 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
363 -cipher => 'Blowfish'
364 ) or die $!;
365 my $password = $cipher->decrypt($c->session->{enc_password});
366 $c->authenticate({username => $c->req->param('username'),password => $password})
367 or $c->log->info("Authenticating user for first password change failed");
368
369 my $pp = Net::LDAP::Control::PasswordPolicy->new;
370 $mesg = $c->model('User')->set_password(
371
372 #oldpasswd => $c->req->param('password'),
373 newpasswd => $newpass,
374 control => [ $pp ],
375 );
376 if ($mesg->code) {
377 my $perror = $mesg->error;
378 push @{${$c->stash}{'errors'}},"Password change failed: $perror";
379 $c->detach;
380 }
381
382 # re-encrypt the new password and redirect to user view
383 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
384 -cipher => 'Blowfish'
385 ) or die $!;
386 $c->session->{enc_password} = $cipher->encrypt($newpass);
387 $c->authenticate({username => $c->req->param('username'),password => $newpass});
388 push @{${$c->stash}{'errors'}},"Password change succeeded";
389 $c->res->redirect('/user');
390
391 }
392
393 sub login : Local {
394 my ( $self, $c ) = @_;
395 if ($c->authenticate({ username => $c->req->param('username'),
396 password => $c->req->param('password') || $c->req->param('key')}) ) {
397 $c->res->redirect('/user');
398 } else {
399
400 #TODO: ppolicy ....
401 $c->stash(errors => ['Incorrect username or password']);
402 $c->stash(template => 'index.tt');
403 $c->forward('/index');
404 }
405 return $c->error;
406 }
407
408 sub logout : Local {
409 my ( $self, $c ) = @_;
410 $c->delete_session;
411 $c->res->redirect('/');
412 }
413
414 sub roles2pages : Private {
415 my @roles = @_;
416 my @pages;
417 foreach my $role (sort @roles) {
418 if ($role =~ /^(\w+) ?(\w*) (Admin)s$/) {
419 my $page = lc("/$3/$1$2");
420 push @pages,{ page => lc("/$3/$1$2"), title => "$1 $2 $3"};
421 }
422 }
423 return \@pages;
424 }
425
426 sub gensubpages : Private {
427 my ($type) = @_;
428 my @subpagenames;
429 @subpagenames = (
430 { page => './', title => 'Edit'},
431 { page => 'password', title => 'Change password'},
432 );
433 return \@subpagenames;
434 }
435
436 =head1 AUTHOR
437
438 Buchan Milne
439
440 =head1 LICENSE
441
442 This library is free software. You can redistribute it and/or modify
443 it under the same terms as Perl itself.
444
445 =cut
446
447 __PACKAGE__->meta->make_immutable;
448
449 1;

  ViewVC Help
Powered by ViewVC 1.1.30