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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 46 - (show annotations) (download)
Wed Nov 3 16:14:41 2010 UTC (13 years, 5 months ago) by buchan
File size: 20234 byte(s)
Fixed registration sequence for password encryption with cookie instead of session id
Initial work on admin password resets of users, email notification to user still
 not working

1 package CatDap::Controller::admin;
2 use Moose;
3 use namespace::autoclean;
4 use Data::UUID;
5 use Data::Dumper;
6 use Net::LDAP::Extension::SetPassword;
7
8 BEGIN { extends 'Catalyst::Controller'; }
9
10 =head1 NAME
11
12 CatDap::Controller::admin - Catalyst Controller
13
14 =head1 DESCRIPTION
15
16 Catalyst Controller.
17
18 =head1 METHODS
19
20 =cut
21
22 =head2 auto
23
24 Ensure the user is logged in. In order to bind as the user, we use
25 CatDap::Model::User, which uses Catalyst::Model::LDAP::FromAuthentication,
26 which effectively requires calling $c->authenticate on every request.
27
28 To do this, we keep the password, encrypted with blowfish, using the
29 (for now), first 3 octets of IPv4 request address and a UUID string (stored in
30 a cookie) as the key. To access the password, an attacker would need:
31 - the first 3 octets of IPv4 request (not stored anywhere, but accessible
32 in server logs)
33 - the encrpyted password (only available server-side in the session variable)
34 - the UUID key portion (only available on the browser-side in a cookie)
35
36 So, if the user does "not exist", we authenticate them, if it succeeds we encrypt
37 the password and store it in the session.
38
39 If the user is logged in, we get the encrypted password from the session, decrypt
40 it (we need to handle failure to decrypt it better)
41
42 =cut
43
44 sub auto : Private {
45 my ( $self, $c ) = @_;
46 my $cipher;
47 my $password;
48 my $mesg;
49 my $dn;
50 my $keyprefix = sprintf( "%02x%02x%02x", split /\./, $c->req->address );
51 if ( !defined $c->user ) {
52 $c->detach('/user/login')
53 if ( not $c->req->param('username')
54 or not $c->req->param('password') );
55 $c->log->info("No session, logging user in");
56 if (
57 !$c->authenticate(
58 {
59 username => $c->req->param('username'),
60 password => $c->req->param('password')
61 || $c->req->param('key')
62 }
63 )
64 )
65 {
66
67 #TODO: ppolicy ....
68 $c->stash( errors => ['Incorrect username or password'] );
69 $c->stash( template => 'index.tt' );
70 $c->log->info("Logging user in failed, forwarding to login page");
71 $c->visit('/user/login');
72 $c->detach;
73 return 1;
74 }
75 else {
76
77 #if (defined $c->user->pwdReset) {
78 # $c->res->redirect('/user');
79 #}
80 #$c->persist_user;
81 $c->log->info('Logging user in to LDAP');
82 my $ug = Data::UUID->new;
83 my $key = $ug->create_str();
84 $cipher = Crypt::CBC->new(
85 -key => $keyprefix . $key,
86 -cipher => 'Blowfish'
87 ) or die $!;
88 $c->session->{enc_password} =
89 $cipher->encrypt( $c->req->param('password')
90 || $c->req->param('key') );
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 }
99 else {
100 my $key = $c->req->cookie('key')->value;
101 $cipher = Crypt::CBC->new(
102 -key => $keyprefix . $key,
103 -cipher => 'Blowfish'
104 ) or die $!;
105 $password = $cipher->decrypt( $c->session->{enc_password} );
106 $c->log->info( "Re-authenticating user " . $c->session->{user} );
107 $c->authenticate(
108 { username => $c->session->{user}, password => $password } )
109 or $c->view('/user/login');
110 $c->res->cookies->{'key'} = {value => $key, expires => '+10m'};
111 $c->stash( pages => roles2pages( $c->user->roles ) );
112 $c->log->info($@) if $@;
113 return 1;
114 }
115 }
116
117 sub account : Local {
118 my ( $self, $c ) = @_;
119 $c->detach('/user/login') if not $c->user;
120 $c->assert_user_roles('Account Admins');
121 $c->stash( subpages => gensubpages('account') );
122 my @errors;
123 return
124 if not defined $c->req->param('attribute')
125 and not defined $c->req->param('value');
126
127 #my $attribute =~ m/^([\w\d]*)/,$c->req->param('attribute');
128 my $attribute = $c->req->param('attribute');
129 $c->log->info("Searching for account using attribute $attribute");
130
131 #my $value =~ /^[\w\d]*/,$c->req->param('attribute');
132 my $value = $c->req->param('value');
133 my $mesg =
134 $c->model('user')
135 ->search("(&(objectClass=inetOrgPerson)($attribute=$value))");
136 my @entries = $mesg->entries;
137 push @errors, $mesg->error if $mesg->code;
138 $c->stash( entries => \@entries );
139 $c->stash( errors => \@errors );
140 }
141
142 sub account_promote : Local {
143 my ( $self, $c ) = @_;
144 $c->detach('/user/login') if not $c->user;
145 $c->assert_user_roles('Account Admins');
146 $c->stash( subpages => gensubpages('account') );
147 my @errors;
148 if ( defined $c->req->param('gid') and defined $c->req->param('username') )
149 {
150 my $gid = $c->req->param('gid');
151 foreach my $uid ( $c->req->param('username') ) {
152 $c->log->info("Promoting user $uid with gid $gid");
153 my $mesg =
154 $c->model('user')
155 ->search(
156 "(&(uid=$uid)(objectclass=inetOrgPerson)(!(objectClass=posixAccount)))"
157 );
158 if ( $mesg->entries gt 1 ) {
159 push @errors, "More than one account matched user $uid";
160
161 #TODO forward to error page
162 }
163 my $entry = $mesg->entry;
164 $mesg = $c->model('user')->search("(objectclass=sambaUnixIdPool)");
165 if ( $mesg->entries gt 1 ) {
166 push @errors, "More than one ID Pool";
167
168 #TODO forward to error page
169 }
170 my $idpool = $mesg->entry;
171 my $uidnum = $idpool->uidNumber;
172 my $newuidnum = $uidnum+1;
173 $entry->add(
174 objectclass => [ 'posixAccount', 'ldapPublicKey' ],
175 loginShell => '/bin/bash',
176 gidNumber => $c->req->param('gid'),
177 uidNumber => $uidnum,
178 homeDirectory => "/home/$uid",
179 );
180 $idpool->replace( uidNumber => $newuidnum );
181
182 $idpool->update;
183 $mesg = $entry->update or $c->log->info("LDAP update failed: $!");
184 if ($mesg->code) {
185 push @errors, $mesg->error;
186 #reverse idpool update
187 $idpool->replace( uidNumber => $uidnum );
188 $mesg = $idpool->update;
189 $c->log->info("ERROR IdPool could not be reset to $uidnum");
190 }
191 }
192 }
193 my $mesg =
194 $c->model('user')
195 ->search("(&(objectClass=inetOrgPerson)(!(objectClass=posixAccount)))");
196 my @entries = $mesg->entries;
197 $c->stash( entries => \@entries );
198 push @errors, $mesg->error if $mesg->code;
199 $mesg = $c->model('user')->search("(objectClass=posixGroup)");
200 my @groups = $mesg->entries;
201 $c->stash( groups => \@groups );
202 $c->stash( errors => \@errors );
203 }
204
205 sub account_modify : Local {
206 my ( $self, $c, $user ) = @_;
207 $c->detach('/user/login') if not $c->user;
208 $c->stash( subpages => gensubpages('account') );
209 $c->assert_user_roles('Account Admins');
210 my @errors;
211 my $mesg;
212 if ( $user eq '' ) {
213 $c->forward( $c->uri_for('/account') );
214 $c->detach;
215 }
216 my $schemaldap = Net::LDAP->new( ${ $c->config }{'Model::Proxy'}{'host'} )
217 or warn "LDAP bind failed: $!";
218 $schemaldap->start_tls if ${ $c->config }{'Model::Proxy'}{'start_tls'};
219 $schemaldap->bind;
220 my $schema = $schemaldap->schema or die("Searching schema failed: $!");
221 my $attrdef;
222
223 my $entry;
224 $c->log->info("Searching for user $user");
225 $mesg =
226 $c->model('User')->search("(&(objectclass=inetOrgPerson)(uid=$user))");
227 $entry = $mesg->entry;
228
229 # Handle adding attributes immediately here, forwarding back to ourselves
230 if ( $c->req->param('operation') eq 'add' ) {
231 $entry->add( $c->req->param('attribute') => $c->req->param('value') );
232 $mesg = $entry->update;
233 push @errors, $mesg->error if $mesg->code;
234 $c->res->redirect( $c->uri_for( $c->req->uri ) . "/$user" );
235 }
236
237 my %mods;
238 my %params = %{ $c->req->parameters };
239 my $update = 0;
240 foreach my $req ( keys %params ) {
241 next if $req !~ /(.+)_new/;
242 my $attrname = $1;
243 next if $params{ $attrname . '_new' } eq $params{ $attrname . '_old' };
244 $c->log->info("Received update request for attribute $attrname");
245 $update = 1;
246 $attrdef = $schema->attribute($attrname)
247 or die("getting schema failed: $!");
248 if ( $$attrdef{'single-value'} ) {
249 $entry->replace( $attrname => $params{ $attrname . '_new' } )
250 or $c->log->info($!);
251 }
252 else {
253 $entry->delete( $attrname => $params{ $attrname . '_old' } );
254 $entry->add( $attrname => $params{ $attrname . '_new' } );
255 }
256 if ($update) {
257 $mesg = $entry->update;
258 push @{ ${ $c->stash }{'errors'} }, $mesg->error if $mesg->code;
259 }
260 }
261
262 $mesg =
263 $c->model('User')->search("(&(objectclass=inetOrgPerson)(uid=$user))");
264 $c->log->info( $mesg->error ) if $mesg->code;
265 $entry = $mesg->entry;
266 $c->log->info( $mesg->error ) if $mesg->code;
267
268 my @values;
269 my @attributes = $entry->attributes;
270 my @may;
271 my @addable_attrs = @attributes;
272 my @ocs;
273 my @must;
274 @ocs = $entry->get_value("objectClass");
275 foreach my $oc (@ocs) {
276 foreach my $attr ( $schema->must($oc) ) {
277 push @must, $$attr{'name'} if not grep /$$attr{'name'}/, @must;
278 }
279 }
280
281 foreach my $attr ( sort @attributes ) {
282 next if ( $attr eq "objectClass" );
283 next
284 if grep /$attr/,
285 @{ ${ $c->config }{'Controller::User'}{'skip_attrs'} };
286 my @vals = $entry->get_value($attr);
287 $attrdef = $schema->attribute($attr)
288 or die("getting schema failed: $!");
289 my %valhash = (
290 name => $attr,
291 values => \@vals,
292 desc => $$attrdef{'desc'},
293 );
294 if ( !grep /^$attr$/,
295 @{ ${ $c->config }{'Controller::User'}{'uneditable_attrs'} } )
296 {
297 $valhash{'editable'} = 1;
298 }
299 if ( !$$attrdef{'single-value'} && $valhash{'editable'} ) {
300 $valhash{'addable'} = 1;
301 }
302 if ( !grep /$attr/, @must ) { $valhash{'removable'} = 1; }
303 push @values, \%valhash;
304 }
305 foreach my $oc (@ocs) {
306 foreach my $attrdef ( $schema->may($oc) ) {
307 my $attrname = $$attrdef{'name'};
308 grep /$attrname/, @may
309 or grep /$attrname/, @attributes
310 or grep /$attrname/,
311 @{ ${ $c->config }{'Controller::User'}{'uneditable_attrs'} }
312 or grep /$attrname/,
313 @{ ${ $c->config }{'Controller::User'}{'skip_attrs'} }
314 or push @may, $attrname;
315 }
316 }
317 @may = sort @may;
318 my @available_ocs = $schema->all_objectclasses;
319 my @offer_ocs;
320 foreach my $oc (@available_ocs) {
321 my $ocname = $$oc{name};
322 next if grep /$ocname/, @ocs;
323 next if not $$oc{auxiliary};
324 push @offer_ocs, $ocname;
325 }
326 @offer_ocs = sort @offer_ocs;
327 my @groups;
328 if ( grep /posixAccount/, @offer_ocs ) {
329 my $mesg = $c->model('user')->search('objectclass=posixGroup');
330 foreach my $group ( $mesg->entries ) {
331 push @groups,
332 {
333 name => $group->cn,
334 gidNumber => $group->gidNumber,
335 };
336 }
337 }
338
339 $c->stash(
340 {
341 username => $user,
342 values => \@values,
343 attrdef => $attrdef,
344 may => \@may,
345 must => \@must,
346 offer_ocs => \@offer_ocs,
347 dn => $entry->dn,
348 uid => $entry->uid,
349 }
350 );
351 $c->stash( 'groups' => \@groups ) if (@groups);
352 }
353
354 sub account_modifydel : Local {
355 my ( $self, $c, $uid, $attr, $value ) = @_;
356 $c->detach('/user/login') if not $c->user;
357 $c->assert_user_roles('Account Admins');
358 $c->stash( subpages => gensubpages('account') );
359 my @errors;
360 my $mesg;
361 $mesg =
362 $c->model('user')->search("(&(objectClass=inetOrgPerson)(uid=$uid))");
363 push @errors, $mesg->error if $mesg->code;
364 $mesg = $mesg->entry->delete( $attr => $value )->update;
365 push @errors, $mesg->error if $mesg->code;
366 $c->res->redirect( $c->uri_for('/admin/account_modify') . "/$uid" );
367 }
368
369 sub account_group : Local {
370 my ( $self, $c, $uid ) = @_;
371 $c->detach('/user/login') if not $c->user;
372 $c->assert_user_roles('Account Admins');
373 $c->stash( subpages => gensubpages('account') );
374 $c->res->redirect($c->uri_for('/admin/account')) if $uid eq '';
375 my (@errors,@newgroups,@groups);
376 my ($mesg,$entry,$dn);
377
378 $mesg = $c->model('user')->search("(&(objectclass=inetOrgperson)(uid=$uid))");
379 $entry = $mesg->entry;
380 $dn = $entry->dn;
381 if (defined $c->req->param('op')) {
382 my $group = $c->req->param('group');
383 $mesg = $c->model('user')->search("(&(objectclass=groupOfNames)(cn=$group))");
384 $entry = $mesg->entry;
385 $entry->delete(member => $dn) if ($c->req->param('op') eq 'delete');
386 $entry->add(member => $dn) if ($c->req->param('op') eq 'add');
387 $mesg = $entry->update if ($entry->changes);
388 push @errors,$mesg->error if $mesg->code;
389 }
390
391
392 $mesg = $c->model('user')->search("(&(objectclass=groupOfNames)(member=$dn))");
393 @groups = $mesg->entries;
394 $mesg = $c->model('user')->search("(&(objectclass=groupOfNames)(!(member=$dn)))");
395 @newgroups = $mesg->entries;
396 $c->stash(
397 uid => $uid,
398 groups => \@groups,
399 newgroups => \@newgroups,
400 );
401 }
402
403 sub account_addoc : Local {
404 my ( $self, $c ) = @_;
405 $c->detach('/user/login') if not $c->user;
406 $c->assert_user_roles('Account Admins');
407 $c->stash( subpages => gensubpages('account') );
408 my $objectclass = $c->req->param('objectclass')
409 or $c->detach('/admin/group');
410 my $dn = $c->req->param('dn');
411 my $uid = $c->req->param('uid');
412 my ( @errors, @must, @may );
413 my $mesg;
414 my $schemaldap = Net::LDAP->new( ${ $c->config }{'Model::Proxy'}{'host'} )
415 or warn "LDAP bind failed: $!";
416 $mesg = $schemaldap->start_tls
417 if ${ $c->config }{'Model::Proxy'}{'start_tls'};
418 push @errors, $mesg->error if $mesg->code;
419 $schemaldap->bind;
420 push @errors, $mesg->error if $mesg->code;
421 my $schema = $schemaldap->schema or die("Searching schema failed: $!");
422 $mesg =
423 $c->model('user')->search("(&(objectclass=inetOrgPerson)(uid=$uid))");
424 $c->log->info( $mesg->error ) if $mesg->code;
425 my $entry = $mesg->entry;
426 $c->log->info( $mesg->error ) if $mesg->code;
427
428 foreach my $attr ( $schema->must($objectclass) ) {
429 push @must, $$attr{name} if not $entry->get_value( $$attr{name} );
430 }
431 foreach my $attr ( $schema->may($objectclass) ) {
432 push @may, $$attr{name} if not $entry->get_value( $$attr{name} );
433 }
434
435 # if we have all the musts as params
436 my $haveall = 1;
437 foreach my $addattr (@must) {
438 if ( defined $c->req->param($addattr) ) {
439 $entry->add( $addattr => $c->req->param($addattr) );
440 }
441 else {
442 $c->log->info("Missing attribute $addattr");
443 $haveall = 0;
444 }
445 }
446 if ($haveall) {
447 $entry->add( objectClass => [$objectclass] );
448 $c->log->info("About to push updates to $dn");
449 $c->log->info( Dumper( \$entry->changes ) );
450 $mesg = $entry->update;
451 push @errors, $mesg->error if $mesg->code;
452 $c->stash( template => 'admin/account.tt', errors => @errors );
453
454 #$c->detach('account_modify');
455 $c->res->redirect( $c->uri_for('/admin/account_modify') . "/" . $uid );
456 $c->detach;
457 }
458 $c->stash(
459 may => \@may,
460 must => \@must,
461 oc => $objectclass,
462 dn => $dn,
463 uid => $uid,
464 );
465 }
466
467 sub password : Local {
468 my ($self,$c,$uid) = @_;
469 $c->detach('/user/login') if not $c->user;
470 $c->assert_user_roles('Account Admins');
471 $c->stash( subpages => gensubpages('account') );
472 my (@errors,@mail);
473 my ($mesg,$entry,$newpass);
474 $c->res->redirect($c->uri_for('/admin/account')) if $uid eq '';
475
476 # Find the user so we have their email now for confirmation, or can easily
477 # set password if reset has been confirmed
478 $mesg = $c->model('user')->search("uid=$uid");
479 push @errors,$mesg->error if $mesg->code;
480 if ($mesg->entries gt 1) {
481 push @errors,'More than one entry matched';
482 $c->detach;
483 }
484 $entry = $mesg->entry;
485 @mail = $entry->mail;
486
487 if (!$c->req->param('txnid')) {
488 my $txnid = Data::UUID->new->create_str();
489 $c->session(txnid => $txnid);
490 $c->stash( uid => $uid, txnid => $txnid, mails => @mail);
491 return 1;
492 }
493 if ($c->req->param('txnid') != $c->session->{txnid}) {
494 push @errors,'Transaction ID mismatch';
495 $c->detach;
496 }
497 $newpass = Data::UUID->new->create_str();
498 my $pp = Net::LDAP::Control::PasswordPolicy->new;
499 $mesg = $entry->replace( userPassword => $newpass,pwdReset => 'TRUE' )->update;
500 if ($mesg->code) {
501 push @errors,"Password reset failed: " . $mesg->error;
502 $c->detach;
503 }
504 $c->stash->{email} = {
505 to => join ',',@mail,
506 subject => $c->config->{apptitle} . " - " . $c->loc('password reset'),
507 from => $c->config->{emailfrom},
508 template => 'admin/password.tt',
509 };
510 $c->forward( $c->view('Email::Template') );
511 }
512
513
514 sub group : Local {
515 my ( $self, $c ) = @_;
516 $c->detach('/user/login') if not $c->user;
517 $c->assert_user_roles('Account Admins');
518 $c->stash( subpages => gensubpages('account') );
519 my @errors;
520 return if not $c->req->param('attribute') and not $c->req->param('value');
521 my $attribute = $c->req->param('attribute');
522 $attribute =~ s/[^\w\d]//g;
523 my $value = $c->req->param('value');
524 $value =~ s/[^\w\d\*]//g;
525 my $mesg =
526 $c->model('user')
527 ->search("(&(objectclass=posixGroup)($attribute=$value))");
528 push @errors, $mesg->error if $mesg->code;
529 my @entries = $mesg->entries;
530 push @errors, $mesg->error if $mesg->code;
531 $c->stash(
532 entries => \@entries,
533 errors => \@errors,
534 );
535 }
536
537 sub group_modify : Local {
538 my ( $self, $c, $group ) = @_;
539 $c->detach('/user/login') if not $c->user;
540 $c->assert_user_roles('Account Admins');
541 $c->stash( subpages => gensubpages('account') );
542 my @errors;
543 $c->detach('/admin/group') if $group eq '';
544 if ( $group !~ /^[\w\d]*$/ ) {
545 push @errors, "Group contains illegal characters";
546 $c->detach('admin/group');
547 }
548 my $mesg =
549 $c->model('user')->search("(&(objectClass=posixGroup)(cn=$group))");
550 if ( $mesg->entries gt 1 ) {
551 push @errors, 'More than one entry matched';
552 $c->detach('/admin/group');
553 }
554 $c->stash( group => $mesg->entry );
555 }
556
557 =head2 index
558
559 =cut
560
561 sub index : Path : Args(0) {
562 my ( $self, $c ) = @_;
563 $c->stash( pages => roles2pages( $c->user->roles ) );
564
565 #$c->response->body("Matched CatDap::Controller::admin in admin, roles $rolelist");
566 }
567
568 sub roles2pages : Private {
569 my @roles = @_;
570 my @pages;
571 foreach my $role ( sort @roles ) {
572 if ( $role =~ /^(\w+) ?(\w*) (Admin|User)s$/ ) {
573 my $page = lc("/$3/$1$2");
574 push @pages, { page => lc("/$3/$1$2"), title => "$1 $2 $3" };
575 }
576 }
577 return \@pages;
578 }
579
580 sub gensubpages : Private {
581 my ($type) = @_;
582 my @subpagenames;
583 if ( $type eq 'account' ) {
584 @subpagenames = (
585 { page => 'account', title => "Users" },
586 { page => 'account_promote', title => "Promote" },
587 #{ page => 'account_unlock', title => "Unlock" },
588 { page => 'group', title => "Groups" },
589 );
590 }
591 return \@subpagenames;
592 }
593
594 =head1 AUTHOR
595
596 Buchan Milne
597
598 =head1 LICENSE
599
600 This library is free software. You can redistribute it and/or modify
601 it under the same terms as Perl itself.
602
603 =cut
604
605 __PACKAGE__->meta->make_immutable;
606
607 1;

  ViewVC Help
Powered by ViewVC 1.1.30