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   8738040 use strict;
  4         12  
  4         180  
3 4     4   24 use warnings;
  4         8  
  4         136  
4 4     4   20 use Carp;
  4         6  
  4         414  
5 4     4   26 use Data::Dump qw( dump );
  4         10  
  4         178  
6 4     4   3600 use Digest::SHA1;
  4         865970  
  4         302  
7 4     4   664934 use MIME::Base64;
  4         276832  
  4         416  
8 4     4   44 use base qw( Net::LDAP::Class::User );
  4         10  
  4         3972  
9 4         28 use Net::LDAP::Class::MethodMaker ( 'scalar --get_set_init' =>
10 4     4   32 [qw( default_shell default_home_dir default_email_suffix )], );
  4         8  
11              
12             our $VERSION = '0.26';
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 374 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 359 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 713 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 421 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 8832 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 54 my $self = shift;
119 27         98 my %opts = @_;
120 27 50 33     191 my $uid = delete $opts{uidNumber} || $self->uidNumber
121             or croak "uidNumber required to create()";
122 27 50 33     393 my $username = delete $opts{uid} || $self->uid
123             or croak "uid required to create()";
124              
125 27         159 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         245 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         80 for my $name ( keys %{ $self->{_not_yet_set} } ) {
  27         401  
151              
152             #warn "set $name => $self->{_not_yet_set}->{$name}";
153 87 50       171 unless ( exists $attr{$name} ) {
154 0         0 $attr{$name} = delete $self->{_not_yet_set}->{$name};
155             }
156             else {
157 87         244 $attr{$name} = $self->{_not_yet_set}->{$name};
158             }
159             }
160              
161 27         209 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       261 if ( exists $self->{groups} ) {
170 21         87 for my $group ( @{ $self->{groups} } ) {
  21         95  
171 21         42 my @newUids;
172 21 50       145 if ( !$group->read ) {
173 0         0 croak
174             "You must create group $group before you add User $self to it";
175             }
176 21 50       155 if ( $self->debug ) {
177 0         0 warn "POSIX group $group has memberUid: "
178             . Data::Dump::dump( $group->memberUid );
179             }
180 21 100       245 if ( $group->memberUid ) {
181 19         102 @newUids = ( $group->memberUid, $username );
182             }
183             else {
184 2         10 @newUids = ($username);
185             }
186 21         147 my $group_name = $group->cn;
187 21         102 my $group_dn = $group->base_dn;
188 21         183 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         364 push( @actions, update => $action );
198             }
199             }
200              
201 27         497 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     170 unless ( $self->gidNumber or $self->group ) {
221 0         0 croak "group or gidNumber required";
222             }
223 30         84 my ( $group, $gid );
224              
225 30         128 my $group_class = $self->group_class;
226              
227 30   33     324 $group = $self->group
228             || $group_class->new(
229             gidNumber => $self->gidNumber,
230             ldap => $self->ldap
231             )->read;
232              
233 30 50       108 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     459 if ( ref $group and $group->isa('Net::LDAP::Class::Group::POSIX') ) {
240 30         161 $gid = $group->gidNumber;
241 30         6261 $group = $group->cn;
242             }
243 30   0     274 $gid ||= $self->gidNumber || $self->group->gidNumber;
      33        
244              
245             # set name
246 30 0 33     150 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     109 my @name_parts = split( m/\s+/, $self->gecos || '' );
252 30         176 my $givenName = $self->givenName;
253 30 100       355 $givenName = shift(@name_parts) unless defined $givenName;
254 30         144 my $sn = $self->sn;
255 30 100       150 $sn = join( ' ', @name_parts ) unless defined $sn;
256 30         105 my $gecos = $self->gecos;
257 30 50       134 $gecos = join( ' ', $givenName, $sn ) unless defined $gecos;
258              
259 30         243 my $email = $self->mail;
260 30 100       204 $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     162 my $hash = $self->userPassword || $self->new_password;
266              
267 30         2132 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 8 my $self = shift;
278 3         8 my %opts = @_; # currently unused
279 3         18 my $uid = $self->uidNumber;
280 3         14 my $username = $self->uid;
281              
282 3 50 33     76 unless ( $username and $uid ) {
283 0         0 croak "must have uid and uidNumber set to update";
284             }
285              
286 3         7 my @actions;
287              
288 3         18 my ( $group, $gid, $givenName, $sn, $gecos, $email, $hash )
289             = $self->setup_for_write;
290              
291 3         25 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         75 my %replace;
307 3         9 for my $attr ( keys %{ $self->{_was_set} } ) {
  3         25  
308              
309 1         22 my $old = $self->{_was_set}->{$attr}->{old};
310 1   33     4 my $new = $self->{_was_set}->{$attr}->{new} || $derived{$attr};
311              
312 1 50 33     78 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         5 $replace{$attr} = $new;
324             }
325              
326             }
327              
328 3 100       17 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     350 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         9 my $newgroup = $self->group->cn;
354 2         14 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         10 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       35 if ( exists $self->{groups} ) {
390              
391 2         46 my $existing_groups = $self->fetch_groups;
392 2         13 my %existing = map { $_->gidNumber => $_ } @$existing_groups;
  3         21  
393              
394             # the delete $self->{groups} has helpful side effect of clearing
395             # cache.
396 2         6 my %new = map { $_->gidNumber => $_ } @{ delete $self->{groups} };
  2         11  
  2         11  
397              
398             # which should be added
399 2         22 my @to_add;
400 2         11 for my $gid ( keys %new ) {
401 2 100       14 if ( !exists $existing{$gid} ) {
402 1         9 my @newUids = ( $new{$gid}->memberUid, $self->uid );
403 1         9 my $group_name = $new{$gid}->cn;
404 1         9 my $group_dn = $new{$gid}->base_dn;
405 1         18 my $action = {
406             search => [
407             base => "ou=Group,$group_dn",
408             scope => "one",
409             filter => "(cn=$group_name)",
410             attrs => $new{$gid}->attributes,
411             ],
412             replace => { memberUid => [@newUids] }
413             };
414 1         11 push( @to_add, update => $action );
415             }
416             }
417              
418             # which should be removed
419 2         8 my @to_rm;
420 2         9 for my $gid ( keys %existing ) {
421 3 100       16 if ( !exists $new{$gid} ) {
422             my @newUids
423 2         10 = grep { $_ ne $self->uid } $existing{$gid}->memberUid;
  2         10  
424 2         9 my $group_name = $existing{$gid}->cn;
425 2         9 my $group_dn = $existing{$gid}->base_dn;
426 2         13 my $action = {
427             search => [
428             base => "ou=Group,$group_dn",
429             scope => "one",
430             filter => "(cn=$group_name)",
431             attrs => $existing{$gid}->attributes,
432             ],
433             replace => { memberUid => [@newUids] }
434             };
435 2         7 push( @to_rm, update => $action );
436             }
437             }
438              
439 2 50       16 carp "to_add: " . dump( \@to_add ) if $self->debug;
440 2 50       14 carp "to_rm: " . dump( \@to_rm ) if $self->debug;
441              
442 2         47 push( @actions, @to_add, @to_rm );
443              
444             }
445              
446 3 50       18 if ( !@actions ) {
447 0         0 warn "no fields have changed for User $username. Skipping update().";
448 0         0 return;
449             }
450              
451 3 50       18 carp "updating User with actions: " . Data::Dump::dump( \@actions )
452             if $self->debug;
453              
454 3         256 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 32 my $self = shift;
498 12 50       50 my $class = $self->group_class or croak "group_class() required";
499              
500 12 50       131 if ( !$self->gidNumber ) {
501 0         0 croak "cannot fetch group without a gidNumber set";
502             }
503              
504             # get groups too
505 12         48 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 9 my $self = shift;
522 4 50       21 my $class = $self->group_class or croak "group_class required";
523 4         55 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       213 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 3 my $self = shift;
543 1 50       7 my $group_class = $self->group_class or croak "group_class required";
544 1   33     16 my $uid = $self->uid || $self->read->uid;
545 1         7 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 5 my $self = shift;
562 2         12 $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 4 my $self = shift;
578 1         3 my $attribute = 'userPassword';
579              
580 1 50 33     6 if ( !defined $self->ldap_entry && grep { $_ eq $attribute }
  0         0  
  0         0  
581             @{ $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 0 0       0 exists $self->{_not_yet_set}->{$attribute}
589             ? $self->{_not_yet_set}->{$attribute}
590             : undef;
591              
592             }
593              
594 1 50       15 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 0 0       0 $self->{_was_set}->{$attribute}->{old}
604             = @old > 1 ? \@old : $old[0];
605             }
606             }
607              
608 1         5 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 58 my $self = shift;
621 27         138 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 56 my $self = shift;
637 27 50       83 my $string = shift or croak "string required";
638 27 50       126 return $string if $string =~ m/^\{SSHA\}/;
639              
640 27         99 my $seed = $self->random_string;
641 27         422 my $sha1 = Digest::SHA1->new;
642 27         207 $sha1->add($string);
643 27         79 $sha1->add($seed);
644              
645 27         1092 return '{SSHA}' . encode_base64( $sha1->digest . $seed, '' );
646             }
647              
648             1;
649              
650             __END__