/[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 559 - (show annotations) (download)
Tue Feb 15 23:01:21 2011 UTC (13 years, 2 months ago) by buchan
File size: 14853 byte(s)
Fix display of non-ascii characters (may need some work in admin for multi-valued attributes)

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 push @may, $attrname;
227 }
228 }
229 @may = sort @may;
230 $c->stash({ username => $user,
231 values => \@values,
232 attrdef => $attrdef,
233 may => \@may,
234 must => \@must,
235 });
236 $c->stash(subpages => gensubpages());
237 }
238
239 sub add : Local {
240 my ( $self, $c) = @_;
241 my $attr = $c->req->param('attribute');
242 my $value = $c->req->param('value');
243 my $user = $c->user->username;
244 my $userfilter = sprintf($c->user->store->user_filter ,$c->user->username);
245 $c->log->debug("Searching for user $user with $userfilter");
246 my $mesg = $c->model('User')->search($userfilter);
247 my $entry = $mesg->entry;
248 $entry->add( $attr => $value);
249 $c->log->info("Adding $attr = $value to user $user");
250 $entry->update;
251 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
252 $c->log->info($mesg->error);
253 $c->res->redirect('/user');
254 }
255
256 sub delete : Local : Args(2) {
257 my ( $self, $c, $attrname,$attrvalue) = @_;
258 my ($mesg,$entry,$user,$userfilter);
259 $user = $c->user->username;
260 $userfilter = $c->user->store->user_filter;
261 $userfilter =~ s/%s/$c->user->username/g;
262 $c->log->debug("Searching for user $user");
263 $mesg = $c->model('User')->search($userfilter);
264 $entry = $mesg->entry;
265 $c->log->info("Deleting $attrname = $attrvalue from user $user");
266 $entry->delete($attrname => $attrvalue);
267 $entry->update;
268 push @{${$c->stash}{'errors'}},$mesg->error if $mesg->code;
269 $c->log->info($mesg->error);
270 $c->res->redirect('/user');
271 }
272
273 sub password : Local {
274 my ( $self, $c) = @_;
275 my ($mesg,$newpass,$cipher);
276 $c->stash(subpages => gensubpages());
277 if ( not defined $c->req->param('password') or not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
278
279 #if ( not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
280
281 $c->detach;
282 }
283 # Re-authenticate to check the user has the right password
284 if (not $c->authenticate({
285 'username' => $c->user->username,
286 'password' => $c->req->param('password'),
287 })
288 ) {
289 $c->stash(errors => [ $c->loc('Password incorrect') ]);
290 $c->detach;
291 }
292 if ($c->req->param('newpassword1') eq $c->req->param('newpassword2')) {
293 $newpass = $c->req->param('newpassword1');
294 } else {
295 push @{${$c->stash}{'errors'}},$c->loc('New passwords dont match');
296 $c->detach;
297 }
298 my $pp = Net::LDAP::Control::PasswordPolicy->new;
299 $mesg = $c->model('User')->set_password(
300 #oldpasswd => $c->req->param('password'),
301 newpasswd => $newpass,
302 control => [ $pp ],
303 );
304 if ($mesg->code) {
305 my $perror = $mesg->error;
306 push @{${$c->stash}{'errors'}},"Password change failed: $perror";
307 $c->detach;
308 } else {
309
310 # re-encrypt the new password and forward to user view
311 my $keyprefix = $self->get_keyprefix($c);
312 my $key = $c->req->cookie('key')->value;
313 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
314 -cipher => 'Blowfish'
315 ) or die $!;
316 $c->session->{enc_password} = $cipher->encrypt($newpass);
317 push @{${$c->stash}{'errors'}},"Password change succeeded";
318 #$c->res->redirect('/user');
319 }
320
321 }
322
323 sub firstlogin : Local {
324 my ( $self, $c ) = @_;
325 my ($mesg,$newpass,$cipher);
326
327 # we want to do our own authentication and caching here, as we
328 # dont want what auto does, and auto returns early for this path
329
330 if ( not defined $c->req->param('newpassword1') or not defined $c->req->param('newpassword2')) {
331 if (! $c->authenticate({
332 username => $c->req->param('username'),
333 password => $c->req->param('key')}) ) {
334 $c->stash(errors => ['An error occurred']);
335 $c->log->info("Failed to authenticate user in first login: " . $c->req->param('key'));
336 $c->res->redirect('/user');
337 }
338 # cache password for next request with form data
339 my $keyprefix = $self->get_keyprefix($c);
340 my $key = Data::UUID->new->create_str();
341 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
342 -cipher => 'Blowfish'
343 ) or die $!;
344 $c->session->{enc_password} = $cipher->encrypt($c->req->param('key'));
345 $c->response->cookies->{'key'} = { value => $key, expires => '+10m' };
346 $c->detach;
347 }
348
349 if ($c->req->param('newpassword1') eq $c->req->param('newpassword2')) {
350 $newpass = $c->req->param('newpassword1');
351 } else {
352 push @{${$c->stash}{'errors'}},"New passwords dont match";
353 }
354
355 #Re-authenticate user
356 my $keyprefix = $self->get_keyprefix($c);
357 my $key = $c->req->cookie('key')->value;
358 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
359 -cipher => 'Blowfish'
360 ) or die $!;
361 my $password = $cipher->decrypt($c->session->{enc_password});
362 $c->authenticate({username => $c->req->param('username'),password => $password})
363 or $c->log->info("Authenticating user for first password change failed");
364
365 my $pp = Net::LDAP::Control::PasswordPolicy->new;
366 $mesg = $c->model('User')->set_password(
367
368 #oldpasswd => $c->req->param('password'),
369 newpasswd => $newpass,
370 control => [ $pp ],
371 );
372 if ($mesg->code) {
373 my $perror = $mesg->error;
374 push @{${$c->stash}{'errors'}},"Password change failed: $perror";
375 $c->detach;
376 }
377
378 # re-encrypt the new password and redirect to user view
379 $cipher = Crypt::CBC->new( -key => $keyprefix . $key,
380 -cipher => 'Blowfish'
381 ) or die $!;
382 $c->session->{enc_password} = $cipher->encrypt($newpass);
383 $c->authenticate({username => $c->req->param('username'),password => $newpass});
384 push @{${$c->stash}{'errors'}},"Password change succeeded";
385 $c->res->redirect('/user');
386
387 }
388
389 sub login : Local {
390 my ( $self, $c ) = @_;
391 if ($c->authenticate({ username => $c->req->param('username'),
392 password => $c->req->param('password') || $c->req->param('key')}) ) {
393 $c->res->redirect('/user');
394 } else {
395
396 #TODO: ppolicy ....
397 $c->stash(errors => ['Incorrect username or password']);
398 $c->stash(template => 'index.tt');
399 $c->forward('/index');
400 }
401 return $c->error;
402 }
403
404 sub logout : Local {
405 my ( $self, $c ) = @_;
406 $c->delete_session;
407 $c->res->redirect('/');
408 }
409
410 sub roles2pages : Private {
411 my @roles = @_;
412 my @pages;
413 foreach my $role (sort @roles) {
414 if ($role =~ /^(\w+) ?(\w*) (Admin)s$/) {
415 my $page = lc("/$3/$1$2");
416 push @pages,{ page => lc("/$3/$1$2"), title => "$1 $2 $3"};
417 }
418 }
419 return \@pages;
420 }
421
422 sub gensubpages : Private {
423 my ($type) = @_;
424 my @subpagenames;
425 @subpagenames = (
426 { page => './', title => 'Edit'},
427 { page => 'password', title => 'Change password'},
428 );
429 return \@subpagenames;
430 }
431
432 =head1 AUTHOR
433
434 Buchan Milne
435
436 =head1 LICENSE
437
438 This library is free software. You can redistribute it and/or modify
439 it under the same terms as Perl itself.
440
441 =cut
442
443 __PACKAGE__->meta->make_immutable;
444
445 1;

  ViewVC Help
Powered by ViewVC 1.1.30