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

  ViewVC Help
Powered by ViewVC 1.1.30