File Coverage

blib/lib/Net/LDAP/Class/User/POSIX.pm
Criterion Covered Total %
statement 156 186 83.8
branch 47 86 54.6
condition 21 59 35.5
subroutine 23 24 95.8
pod 16 16 100.0
total 263 371 70.8


line stmt bran cond sub pod time code
1             package Net::LDAP::Class::User::POSIX;
2 4     4   623498 use strict;
  4         8  
  4         90  
3 4     4   10 use warnings;
  4         6  
  4         74  
4 4     4   12 use Carp;
  4         6  
  4         176  
5 4     4   12 use Data::Dump qw( dump );
  4         4  
  4         122  
6 4     4   1546 use Digest::SHA1;
  4         1916  
  4         134  
7 4     4   1468 use MIME::Base64;
  4         1664  
  4         180  
8 4     4   18 use base qw( Net::LDAP::Class::User );
  4         4  
  4         1426  
9 4         14 use Net::LDAP::Class::MethodMaker ( 'scalar --get_set_init' =>
10 4     4   20 [qw( default_shell default_home_dir default_email_suffix )], );
  4         4  
11              
12             our $VERSION = '0.27';
13              
14             # see http://www.ietf.org/rfc/rfc2307.txt
15              
16             =head1 NAME
17              
18             Net::LDAP::Class::User::POSIX - user class for POSIX LDAP schema
19              
20             =head1 SYNOPSIS
21              
22             # subclass this class for your local LDAP
23             package MyLDAPUser;
24             use base qw( Net::LDAP::Class::User::POSIX );
25            
26             __PACKAGE__->metadata->setup(
27             base_dn => 'dc=mycompany,dc=com',
28             attributes => __PACKAGE__->POSIX_attributes,
29             unique_attributes => __PACKAGE__->POSIX_unique_attributes,
30             );
31            
32             1;
33            
34             # then use your class
35             my $ldap = get_and_bind_LDAP_object(); # you write this
36            
37             use MyLDAPUser;
38             my $user = MyLDAPUser->new( ldap => $ldap, uid => 'foobar' );
39             $user->read_or_create;
40             for my $group ($user->group, @{ $user->groups }) {
41             printf("user %s in group %s\n", $user, $group);
42             }
43              
44             =head1 DESCRIPTION
45              
46             Net::LDAP::Class::User::POSIX isa Net::LDAP::Class::User implementing
47             the POSIX LDAP schema.
48              
49             =head1 CLASS METHODS
50              
51             =head2 POSIX_attributes
52              
53             Returns array ref of default POSIX attributes.
54              
55             =cut
56              
57             sub POSIX_attributes {
58              
59             return [
60 2     2 1 152 qw(
61             uid userPassword uidNumber gidNumber
62             gecos cn mail sn givenName pwdChangedTime
63             homeDirectory loginShell
64             )
65             ];
66              
67             }
68              
69             =head2 POSIX_unique_attributes
70              
71             Returns array ref of unique POSIX attributes: B and B.
72              
73             =cut
74              
75             sub POSIX_unique_attributes {
76 2     2 1 21 return [qw( uid uidNumber )];
77             }
78              
79             =head1 OBJECT METHODS
80              
81             All the init_* methods can be specified to the new() constructor without
82             the init_ prefix.
83              
84             =head2 init_default_shell
85              
86             Returns B.
87              
88             =cut
89              
90 28     28 1 410 sub init_default_shell {'/bin/bash'}
91              
92             =head2 init_default_home_dir
93              
94             Returns B.
95              
96             =cut
97              
98 28     28 1 274 sub init_default_home_dir {'/home'}
99              
100             =head2 init_default_email_suffix
101              
102             Returns an empty string.
103              
104             =cut
105              
106 27     27 1 195 sub init_default_email_suffix {''}
107              
108             =head2 action_for_create([ uid => I, uidNumber => I ])
109              
110             Returns hash ref suitable for creating a Net::LDAP::Batch::Action::Add.
111              
112             May be called as a class method with explicit B and B
113             key/value pairs.
114              
115             =cut
116              
117             sub action_for_create {
118 27     27 1 36 my $self = shift;
119 27         113 my %opts = @_;
120 27 50 33     136 my $uid = delete $opts{uidNumber} || $self->uidNumber
121             or croak "uidNumber required to create()";
122 27 50 33     225 my $username = delete $opts{uid} || $self->uid
123             or croak "uid required to create()";
124              
125 27         92 my ( $group, $gid, $givenName, $sn, $gecos, $email, $hash )
126             = $self->setup_for_write;
127              
128             # default attributes
129             # note that not setting a homeDirectory or sn is a schema error
130 27         188 my %attr = (
131             objectClass => [ "top", "person", "posixAccount" ],
132             cn => $username,
133             givenName => $givenName,
134             sn => $sn,
135             uid => $username,
136             userPassword => "$hash",
137             uidNumber => $uid,
138             gidNumber => $gid,
139             gecos => $gecos,
140             homeDirectory => $self->default_home_dir . "/$username",
141             loginShell => $self->default_shell,
142             shadowMin => "-1",
143             shadowMax => "99999",
144             shadowWarning => "7",
145             shadowLastChange => "13767",
146             mail => $email
147             );
148              
149             # mix in whatever has been set
150 27         40 for my $name ( keys %{ $self->{_not_yet_set} } ) {
  27         127  
151              
152             #warn "set $name => $self->{_not_yet_set}->{$name}";
153 87 50       138 unless ( exists $attr{$name} ) {
154 0         0 $attr{$name} = delete $self->{_not_yet_set}->{$name};
155             }
156             else {
157 87         121 $attr{$name} = $self->{_not_yet_set}->{$name};
158             }
159             }
160              
161 27         142 my @actions = (
162             add => {
163             dn => "uid=$username,ou=$group,ou=People," . $self->base_dn,
164             attr => [%attr],
165             }
166             );
167              
168             # secondary groups
169 27 100       123 if ( exists $self->{groups} ) {
170 21         32 for my $group ( @{ $self->{groups} } ) {
  21         54  
171 21         20 my @newUids;
172 21 50       84 if ( !$group->read ) {
173 0         0 croak
174             "You must create group $group before you add User $self to it";
175             }
176 21 50       94 if ( $self->debug ) {
177 0         0 warn "POSIX group $group has memberUid: "
178             . Data::Dump::dump( $group->memberUid );
179             }
180 21 100       110 if ( $group->memberUid ) {
181 19         51 @newUids = ( $group->memberUid, $username );
182             }
183             else {
184 2         7 @newUids = ($username);
185             }
186 21         93 my $group_name = $group->cn;
187 21         81 my $group_dn = $group->base_dn;
188 21         149 my $action = {
189             search => [
190             base => "ou=Group,$group_dn",
191             scope => "sub",
192             filter => "(cn=$group_name)",
193             attrs => $group->attributes,
194             ],
195             replace => { memberUid => [@newUids] }
196             };
197 21         114 push( @actions, update => $action );
198             }
199             }
200              
201 27         338 return @actions;
202             }
203              
204             =head2 setup_for_write
205              
206             Utility method for generating default values for
207             various attributes. Called by both action_for_create()
208             and action_for_update().
209              
210             Returns array of values in this order:
211              
212             $groupname, $gid, $givenName, $sn, $gecos, $email, $passwordHash
213              
214             =cut
215              
216             sub setup_for_write {
217 30     30 1 51 my $self = shift;
218              
219             # must find the group name first so we can set up dn correctly
220 30 50 66     72 unless ( $self->gidNumber or $self->group ) {
221 0         0 croak "group or gidNumber required";
222             }
223 30         51 my ( $group, $gid );
224              
225 30         75 my $group_class = $self->group_class;
226              
227 30   33     217 $group = $self->group
228             || $group_class->new(
229             gidNumber => $self->gidNumber,
230             ldap => $self->ldap
231             )->read;
232              
233 30 50       88 if ( !defined $group ) {
234 0         0 croak "group "
235             . $self->gidNumber
236             . " is not yet in LDAP. Must add it before creating User";
237             }
238              
239 30 50 33     225 if ( ref $group and $group->isa('Net::LDAP::Class::Group::POSIX') ) {
240 30         105 $gid = $group->gidNumber;
241 30         101 $group = $group->cn;
242             }
243 30   0     127 $gid ||= $self->gidNumber || $self->group->gidNumber;
      33        
244              
245             # set name
246 30 0 33     103 unless ( $self->gecos || $self->sn || $self->givenName ) {
      33        
247 0         0 croak "either gecos, sn or givenName must be set";
248             }
249              
250             # the name logic breaks horribly here for anything but trivial cases.
251 30   50     84 my @name_parts = split( m/\s+/, $self->gecos || '' );
252 30         107 my $givenName = $self->givenName;
253 30 100       101 $givenName = shift(@name_parts) unless defined $givenName;
254 30         96 my $sn = $self->sn;
255 30 100       108 $sn = join( ' ', @name_parts ) unless defined $sn;
256 30         62 my $gecos = $self->gecos;
257 30 50       71 $gecos = join( ' ', $givenName, $sn ) unless defined $gecos;
258              
259 30         99 my $email = $self->mail;
260 30 100       133 $email = ( $self->username . $self->default_email_suffix )
261             unless defined $email;
262              
263             # set password if not set.
264             # this is useful for default random passwords.
265 30   66     92 my $hash = $self->userPassword || $self->new_password;
266              
267 30         165 return ( $group, $gid, $givenName, $sn, $gecos, $email, $hash );
268             }
269              
270             =head2 action_for_update
271              
272             Returns array ref suitable for creating a Net::LDAP::Batch::Action::Update.
273              
274             =cut
275              
276             sub action_for_update {
277 3     3 1 3 my $self = shift;
278 3         5 my %opts = @_; # currently unused
279 3         10 my $uid = $self->uidNumber;
280 3         7 my $username = $self->uid;
281              
282 3 50 33     30 unless ( $username and $uid ) {
283 0         0 croak "must have uid and uidNumber set to update";
284             }
285              
286 3         1 my @actions;
287              
288 3         9 my ( $group, $gid, $givenName, $sn, $gecos, $email, $hash )
289             = $self->setup_for_write;
290              
291 3         15 my %derived = (
292             cn => $username,
293             givenName => $givenName,
294             sn => $sn,
295             uid => $username,
296             userPassword => $hash,
297             uidNumber => $uid,
298             gidNumber => $gid,
299             gecos => $gecos,
300             mail => $email,
301             homeDirectory => $self->default_home_dir . "/$username",
302             loginShell => $self->default_shell,
303             );
304              
305             # which fields have changed.
306 3         37 my %replace;
307 3         6 for my $attr ( keys %{ $self->{_was_set} } ) {
  3         14  
308              
309 1         3 my $old = $self->{_was_set}->{$attr}->{old};
310 1   33     13 my $new = $self->{_was_set}->{$attr}->{new} || $derived{$attr};
311              
312 1 50 33     21 if ( defined($old) and !defined($new) ) {
    50 33        
    50 33        
    50          
313 0         0 $replace{$attr} = undef;
314             }
315             elsif ( !defined($old) and defined($new) ) {
316 0         0 $replace{$attr} = $new;
317             }
318             elsif ( !defined($old) and !defined($new) ) {
319              
320             #$replace{$attr} = undef;
321             }
322             elsif ( $old ne $new ) {
323 1         4 $replace{$attr} = $new;
324             }
325              
326             }
327              
328 3 100       10 if (%replace) {
329 1         6 push(
330             @actions,
331             update => {
332             search => [
333             base => "ou=People," . $self->base_dn,
334             scope => "sub",
335             filter => "(uid=$username)",
336             attrs => $self->attributes,
337             ],
338             replace => \%replace
339             }
340             );
341             }
342              
343             # what group(s) have changed?
344             # compare primary group first
345             # this assumes that setting group() is preferred to
346             # explicitly setting gidNumber.
347 3 100 66     20 if ( !exists $replace{gidNumber}
348             and $self->group->gidNumber != $self->gidNumber )
349             {
350              
351             # primary group has changed
352             # must set gidNumber and change dn in two steps.
353 2         7 my $newgroup = $self->group->cn;
354 2         9 push(
355             @actions,
356             update => [
357             { search => [
358             base => "ou=People," . $self->base_dn,
359             scope => "sub",
360             filter => "(uid=$username)",
361             attrs => $self->attributes,
362             ],
363             replace => { gidNumber => $self->group->gidNumber },
364             },
365             { dn => {
366             'newrdn' => "uid=$username",
367             'deleteoldrdn' => 1,
368             'newsuperior' => "ou=$newgroup,ou=People,"
369             . $self->group->base_dn,
370             },
371             search => [
372             base => "ou=People," . $self->base_dn,
373             scope => "sub",
374             filter => "(uid=$username)",
375             attrs => $self->attributes,
376             ],
377             }
378             ],
379             );
380              
381             # clear so next access re-fetches
382 2         7 delete $self->{group};
383              
384             }
385              
386             # next, secondary group membership.
387             # check if any have been set explicitly,
388             # since otherwise there is nothing to be done.
389 3 100       24 if ( exists $self->{groups} ) {
390              
391 2         17 my $existing_groups = $self->fetch_groups;
392 2         5 my %existing = map { $_->gidNumber => $_ } @$existing_groups;
  3         12  
393              
394             # the delete $self->{groups} has helpful side effect of clearing
395             # cache.
396 2         3 my %new = map { $_->gidNumber => $_ } @{ delete $self->{groups} };
  2         4  
  2         6  
397              
398             # which should be added
399 2         4 my @to_add;
400 2         8 for my $gid ( keys %new ) {
401 2 100       5 if ( !exists $existing{$gid} ) {
402 1         4 my @newUids = ( $new{$gid}->memberUid, $self->uid );
403 1         4 my $group_name = $new{$gid}->cn;
404 1         4 my $group_dn = $new{$gid}->base_dn;
405             my $action = {
406             search => [
407             base => "ou=Group,$group_dn",
408             scope => "one",
409             filter => "(cn=$group_name)",
410 1         7 attrs => $new{$gid}->attributes,
411             ],
412             replace => { memberUid => [@newUids] }
413             };
414 1         3 push( @to_add, update => $action );
415             }
416             }
417              
418             # which should be removed
419 2         3 my @to_rm;
420 2         6 for my $gid ( keys %existing ) {
421 3 100       9 if ( !exists $new{$gid} ) {
422             my @newUids
423 2         5 = grep { $_ ne $self->uid } $existing{$gid}->memberUid;
  2         7  
424 2         6 my $group_name = $existing{$gid}->cn;
425 2         6 my $group_dn = $existing{$gid}->base_dn;
426             my $action = {
427             search => [
428             base => "ou=Group,$group_dn",
429             scope => "one",
430             filter => "(cn=$group_name)",
431 2         10 attrs => $existing{$gid}->attributes,
432             ],
433             replace => { memberUid => [@newUids] }
434             };
435 2         5 push( @to_rm, update => $action );
436             }
437             }
438              
439 2 50       7 carp "to_add: " . dump( \@to_add ) if $self->debug;
440 2 50       13 carp "to_rm: " . dump( \@to_rm ) if $self->debug;
441              
442 2         16 push( @actions, @to_add, @to_rm );
443              
444             }
445              
446 3 50       9 if ( !@actions ) {
447 0         0 warn "no fields have changed for User $username. Skipping update().";
448 0         0 return;
449             }
450              
451 3 50       8 carp "updating User with actions: " . Data::Dump::dump( \@actions )
452             if $self->debug;
453              
454 3         30 return @actions;
455              
456             }
457              
458             =head2 action_for_delete
459              
460             Returns hash ref suitable for creating a Net::LDAP::Batch::Action::Delete.
461              
462             =cut
463              
464             sub action_for_delete {
465 0     0 1 0 my $self = shift;
466 0         0 my %opts = @_;
467 0   0     0 my $username = delete $opts{uid} || $self->uid;
468              
469 0 0       0 if ( !$username ) {
470 0         0 croak "uid required to delete a User";
471             }
472              
473             # delete the user
474 0         0 my @actions = (
475             delete => {
476             search => [
477             base => "ou=People," . $self->base_dn,
478             scope => "sub",
479             filter => "(uid=$username)",
480             attrs => $self->attributes,
481             ]
482             }
483             );
484              
485 0         0 return @actions;
486             }
487              
488             =head2 fetch_group
489              
490             Required MethodMaker method for retrieving primary group from LDAP.
491              
492             Returns an object of type group_class().
493              
494             =cut
495              
496             sub fetch_group {
497 12     12 1 18 my $self = shift;
498 12 50       25 my $class = $self->group_class or croak "group_class() required";
499              
500 12 50       79 if ( !$self->gidNumber ) {
501 0         0 croak "cannot fetch group without a gidNumber set";
502             }
503              
504             # get groups too
505 12         33 return $class->new(
506             gidNumber => $self->gidNumber,
507             ldap => $self->ldap
508             )->read;
509             }
510              
511             =head2 fetch_groups
512              
513             Required MethodMaker method for retrieving secondary groups from LDAP.
514              
515             Returns array or array ref (based on context) of objects of type
516             group_class().
517              
518             =cut
519              
520             sub fetch_groups {
521 4     4 1 6 my $self = shift;
522 4 50       12 my $class = $self->group_class or croak "group_class required";
523 4         32 my @g = $class->find(
524             ldap => $self->ldap,
525             base_dn => 'ou=Group,' . $self->group->base_dn,
526             filter => "(memberUid=" . $self->uid . ")",
527             );
528 4 50       58 return wantarray ? @g : \@g;
529             }
530              
531             =head2 groups_iterator([I])
532              
533             Returns a Net::LDAP::Class::Iterator object for same data
534             as fetch_groups().
535              
536             See the advice in L about iterators
537             versus arrays.
538              
539             =cut
540              
541             sub groups_iterator {
542 1     1 1 2 my $self = shift;
543 1 50       4 my $group_class = $self->group_class or croak "group_class required";
544 1   33     10 my $uid = $self->uid || $self->read->uid;
545 1         4 return Net::LDAP::Class::Iterator->new(
546             class => $group_class,
547             base_dn => 'ou=Group,' . $self->group->base_dn,
548             filter => "(memberUid=$uid)",
549             ldap => $self->ldap,
550             @_
551             );
552             }
553              
554             =head2 gid
555              
556             Alias for gidNumber() attribute.
557              
558             =cut
559              
560             sub gid {
561 2     2 1 3 my $self = shift;
562 2         7 $self->gidNumber(@_);
563             }
564              
565             =head2 password([I])
566              
567             Convenience wrapper around userPassword() attribute method.
568              
569             This method will SHA-1-hashify I using ssha_hash()
570             and set the hash
571             in the ldap_entry(). If no argument is supplied, returns the hash
572             string set in ldap_entry() (if any).
573              
574             =cut
575              
576             sub password {
577 1     1 1 3 my $self = shift;
578 1         12 my $attribute = 'userPassword';
579              
580 1 50 33     5 if ( !defined $self->ldap_entry && grep { $_ eq $attribute }
  0         0  
581 0         0 @{ $self->attributes } )
582             {
583              
584 0 0       0 if ( scalar @_ ) {
585 0         0 $self->{_not_yet_set}->{$attribute} = $self->ssha_hash( $_[0] );
586             }
587             return
588             exists $self->{_not_yet_set}->{$attribute}
589 0 0       0 ? $self->{_not_yet_set}->{$attribute}
590             : undef;
591              
592             }
593              
594 1 50       14 if (@_) {
595 0         0 my $hash = $self->ssha_hash( $_[0] );
596 0         0 my @old = $self->ldap_entry->get_value($attribute);
597 0         0 $self->ldap_entry->replace( $attribute, $hash );
598 0         0 $self->{_was_set}->{$attribute}->{new} = $hash;
599              
600             # do not overwrite an existing 'old' value, since we might need to know
601             # what was originally in the ldap_entry in order to replace it.
602 0 0       0 unless ( exists $self->{_was_set}->{$attribute}->{old} ) {
603             $self->{_was_set}->{$attribute}->{old}
604 0 0       0 = @old > 1 ? \@old : $old[0];
605             }
606             }
607              
608 1         3 return $self->ldap_entry->get_value($attribute);
609             }
610              
611             =head2 new_password([I])
612              
613             Returns a SHA-1-hashed password from a random string of length I.
614             Default length is 8 characters. This method is just a simple
615             wrapper around ssha_hash() and random_string().
616              
617             =cut
618              
619             sub new_password {
620 27     27 1 34 my $self = shift;
621 27         108 return $self->ssha_hash( $self->random_string(@_) );
622             }
623              
624             =head2 ssha_hash( I )
625              
626             Returns seeded hash of I using SHA-1. See
627             http://www.openldap.org/faq/data/cache/347.html
628              
629             B The hash will contain the LDAP-required
630             C<{SSHA}> prefix. If the prefix is already present, will
631             return I untouched.
632              
633             =cut
634              
635             sub ssha_hash {
636 27     27 1 52 my $self = shift;
637 27 50       66 my $string = shift or croak "string required";
638 27 50       66 return $string if $string =~ m/^\{SSHA\}/;
639              
640 27         82 my $seed = $self->random_string;
641 27         172 my $sha1 = Digest::SHA1->new;
642 27         154 $sha1->add($string);
643 27         74 $sha1->add($seed);
644              
645 27         396 return '{SSHA}' . encode_base64( $sha1->digest . $seed, '' );
646             }
647              
648             1;
649              
650             __END__