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

  ViewVC Help
Powered by ViewVC 1.1.30