source: LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Ad.pm @ 4

Last change on this file since 4 was 4, checked in by nanardon, 15 years ago
  • add Ad base from ealier work
  • Property svn:keywords set to Id Rev
File size: 13.8 KB
Line 
1package LATMOS::Accounts::Bases::Ad;
2
3use 5.010000;
4use strict;
5use warnings;
6
7use base qw(Net::LDAP LATMOS::Accounts::Bases);
8use Net::LDAP::Entry;
9use Net::LDAP::Control::Paged;
10use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED ); 
11use Net::LDAP::Util     qw( escape_filter_value );
12
13our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
14
15
16=head1 NAME
17
18LATMOS::Ad - Perl extension for blah blah blah
19
20=head1 SYNOPSIS
21
22  use LATMOS::Ad;
23  blah blah blah
24
25=head1 DESCRIPTION
26
27Stub documentation for LATMOS::Ad, created by h2xs. It looks like the
28author of the extension was negligent enough to leave the stub
29unedited.
30
31Blah blah blah.
32
33=head1 FUNCTIONS
34
35=cut
36
37=head2 new(%options)
38
39Create a new LATMOS::Ad object for windows AD $domain.
40
41domain / server: either the Ad domain or directly the server
42
43ldap_args is an optionnal list of arguments to pass to L<Net::LDAP>.
44
45=cut
46
47sub new {
48    my ($class, %options) = @_;
49   
50    my $ldap;
51    my ($domain, $server) = ($options{domain}, $options{server});
52
53    # At this point, if still no $server, DNS search
54    foreach my $tryserv (
55        $server
56        ? ($server)
57        : _query_zone_ads($domain)) {
58        $ldap = Net::LDAP->new($tryserv, @_) and last;
59    }
60
61    $ldap or return; # connot connect to any ldap :\
62
63    $ldap->{_ad_domain} = $domain; # possible to find domain from AD ?
64    $ldap->{_top_dn} = join(',', map { "dc=$_" } split('\.', $domain));
65    $ldap->{_lat_config} = { %options };
66
67    bless($ldap, $class);
68}
69
70sub bind {
71    my ($self, $login, %params) = @_;
72
73    if (!$login) {
74        $login = $self->config->val('ldap', 'login');
75        $login =~ m/@/ or $login .= '@' . $self->ad_domain;
76        $params{password} ||= $self->config->val('ldap', 'password');
77    }
78
79    $self->SUPER::bind($login, %params);
80}
81
82sub _query_zone_ads {
83    my ($zone) = @_;
84    require Net::DNS;
85    my @urllist;
86
87    # TODO: see how to find ldaps:// server
88
89    my $resolver = Net::DNS::Resolver->new;
90    my $query = $resolver->query("_ldap._tcp.dc._msdcs.$zone", "SRV");
91    foreach my $rr (
92        sort { $a->priority <=> $b->priority || $a->weight <=> $b->weight }
93        grep { $_->type eq 'SRV' } $query->answer) {
94        push(@urllist, 'ldap://' . $rr->target . ':' . $rr->port . '/');
95    }
96
97    @urllist
98}
99
100=head2 top_dn
101
102Return the TOP DN of the AD zone.
103
104=cut
105
106sub top_dn {
107    return $_[0]->{_top_dn}
108}
109
110=head2 ad_domain
111
112Return the active directory zone
113
114=cut
115
116sub ad_domain {
117    return $_[0]->{_ad_domain}
118}
119
120=head2 unlimited_search
121
122By default, ldap servers limit results to avoid deni of services.
123LDAP protocol provide a paging feature to fetch all results.
124
125This function works like Net::LDAP::search functions, but return nothing,
126use "callback" option to get entries.
127
128=cut
129
130sub unlimited_search {
131    my $self = shift;
132    my @args = @_;
133
134    my ($page, $cookie) = (Net::LDAP::Control::Paged->new( size => 100 ));
135
136    while (1) {
137        my $search = $self->search(
138            @args,
139            control => [ $page ],
140        );
141        if ($search->code) {
142            return $search;
143        }
144
145        ### After foreach loops ends, client checks LDAP server reponse of how many search results total.
146        ### This is a control to end the infinite while loop if there are no search results to go through
147        ### If there are search results, this control will always return the total number of results. It is
148        ### never decremented
149        my ($resp) = $search->control( LDAP_CONTROL_PAGED ) or last;
150
151
152        ### Obtaining the cookie from the search result. When no more results, cookie will be NULL
153        ### and infinite while loop will terminate.
154        $cookie = $resp->cookie or last;
155
156        ### Sets cookie so server knows the next search result to send
157        $page->cookie($cookie);
158    }
159    ### This is a control to check for abnormal exit of the while loop.
160    ### If this occurs ### we need to tell the LDAP server that remaining
161    ### search results are no longer needed ### by sending a search request with a page size of 0
162    if ($cookie)    {
163        $page->cookie($cookie);
164        $page->size(0);
165        $self->search(@args, control => [ $page ]);
166    }
167    return 1;
168}
169
170sub _find_next_id {
171    my ($self, $field, $id, $min) = @_;
172
173    $id ||= 500;
174    my $fid = $id;
175    $self->unlimited_search(
176        base => $self->top_dn,
177        attrs => [ $field ],
178        filter => "($field>=$id)",
179        callback => sub {
180            my ($mesg, $entry) = @_;
181            $mesg && $mesg->code and die $mesg->error;
182            ref $entry eq 'Net::LDAP::Entry' or return;
183            $id = $entry->get_value($field) + 1 if ($id <= $entry->get_value($field));
184        }
185    );
186
187    return $id
188}
189
190=head2 find_next_uid($minuid)
191
192Find next free uidNumber
193
194=cut
195
196sub find_next_uid {
197    my ($self, $minid) = @_;
198    $self->_find_next_id('uidNumber', $minid, 500);
199}
200
201=head2 find_next_gid($minuid)
202
203Find next free gidNumber
204
205=cut
206
207sub find_next_gid {
208    my ($self, $minid) = @_;
209    $self->_find_next_id('gidNumber', $minid, 500);
210}
211
212sub _username_format {
213    my ($name) = @_;
214
215    $name =~ s/[^\w]//g; # strip non alphanumeric
216    $name = substr($name, 0, 8); # keep 8 max
217    $name
218}
219
220sub find_username {
221    my ($self, $lastname, $firstname) = @_;
222    if (!ref $self) { # if call w/o ldap connection
223        ($self, $lastname, $firstname) = (undef, $self, $lastname);
224    }
225
226    my $username = _username_format($lastname);
227    if (!$self) { return $username }
228
229    if (!$self->get_user($lastname, attrs => [ 'cn' ])) {
230        return $username;
231    }
232
233    foreach $username (map { _username_format($_) } (
234            $lastname . substr($firstname, 0, 1),
235            $lastname . $firstname,
236        )) {
237        !$self->get_user($username, attrs => [ 'cn' ]) and return $username;
238    }
239
240    undef
241}
242
243sub _defaults_user_attrs {
244    my ($self, $entry, $attrs) = @_;
245
246    foreach my $attr (keys %{ $attrs || {} }) {
247        my $val = $attrs->{$attr};
248
249        $attr =~ /^homeDirectory$/ and do {
250            $entry->replace('unixHomeDirectory', $val);
251        };
252
253        $attr =~ /^(givenName|sn)$/ and do {
254            $entry->replace('displayName',
255                join(' ', map { ucfirst($_ || '') } (
256                    ($attrs->{'givenName'} || $entry->get_value('givenName')), # first name
257                    ($attrs->{'sn'} || $entry->get_value('sn')),           # last name
258                ))
259            );
260            $entry->replace('gecos', # TODO reencode to aovid accents
261                join(' ', map { ucfirst($_ || '') } (
262                    ($attrs->{'givenName'} || $entry->get_value('givenName')), # first name
263                    ($attrs->{'sn'} || $entry->get_value('sn')),           # last name
264                ))
265            );
266            $attr eq 'sn' and do { # TODO generate clean login here / UNIX uid
267                $entry->add('sAMAccountName' => $val) unless ($entry->exists('sAMAccountName'));
268            };
269        };
270
271        # nothing special to do
272        $entry->replace($attr => $val);
273    }
274}
275
276sub _defaults_group_attrs {
277    my ($self, $entry, $attrs) = @_;
278    foreach my $attr (keys %{ $attrs || {} }) {
279        my $val = $attrs->{$attr};
280        $entry->replace($attr => $val);
281    }
282}
283
284=head2 lists_users($sub, @search_args)
285
286Fetch all user and call $sub for each entry.
287
288Return ldap error message on error.
289
290$sub is call as explain in L<Net::LDAP>
291
292=cut
293
294sub lists_users {
295    my ($self, $sub, @search_args) = @_;
296
297    eval {
298    my $xx = $self->unlimited_search(
299        @search_args,
300        base => $self->top_dn,
301        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)))",
302        callback => sub {
303            my ($mesg, $entry) = @_;
304            $entry or return;
305            ref $entry eq 'Net::LDAP::Entry' or return;
306            $sub->($mesg, $entry);
307        },
308    );
309    };
310
311    return $@;
312}
313
314=head2 get_user($username)
315
316Return the entry for user $username
317
318=cut
319
320sub get_user {
321    my ($self, $username, @search_args) = @_;
322
323    my $mesg = $self->search(
324        @search_args,
325        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
326        base => $self->top_dn,
327    );
328
329    $mesg->code and return;
330
331    my ($entry, @others) = $mesg->entries;
332
333    return if(@others); # we cannot have multiple entries...
334    $entry
335}
336
337=head2 create_user($param)
338
339Create a new user with value in hashref $param
340
341=cut
342
343sub create_user {
344    my ($self, $param) = @_;
345
346    my $entry = Net::LDAP::Entry->new();
347
348    my $username = $param->{sn};
349
350    # TODO generate clean username
351    $entry->dn("cn=$username,cn=Users," . $self->top_dn);
352
353    $param->{uidNumber} ||= $self->find_next_uid;
354
355    $self->_defaults_user_attrs(
356        $entry,
357        {
358            objectClass => [ qw(top person organizationalPerson user)],
359            userAccountControl => 544,
360            accountExpires => '9223372036854775807', # TODO don't hardcode
361            homeDirectory => "/net/nfs/home/$username", # TODO no hardcode path
362            userPrincipalName => "$username\@" . $self->ad_domain,
363            %{ $param || {}},
364        }
365    );
366
367    my $mesg = $self->add($entry);
368
369    if ($mesg->code) {
370        warn $mesg->error;
371        return;
372    } else { return 1 };
373}
374
375=head2 modify_user($username, $param)
376
377=cut
378
379sub modify_user {
380    my ($self, $username, $param) = @_;
381
382    my $mesg = $self->search(
383        base => $self->{_top_dn},
384        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
385    );
386
387    $mesg->code and do {
388        warn $mesg->error;
389        return;
390    };
391
392    my ($entry) = $mesg->entries; # TODO hopefully only one...
393
394    $self->_defaults_user_attrs(
395        $entry,
396        $param,
397    );
398
399    $mesg = $entry->update($self);
400
401    if ($mesg->code) {
402        warn $mesg->error;
403        return;
404    } else { return 1 }
405}
406
407=head2 delete_user($username)
408
409=cut
410
411sub delete_user {
412    my ($self, $username) = @_;
413
414    my $mesg = $self->search(
415        base => $self->{_top_dn},
416        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
417    );
418
419    $mesg->code and do {
420        warn $mesg->error;
421        return;
422    };
423
424    my ($entry) = $mesg->entries; # TODO hopefully one
425
426    $mesg = $self->delete($entry->dn);
427
428    if ($mesg->code) {
429        warn $mesg->error;
430        return;
431    } else { return 1 }
432}
433
434=head2 lists_groups
435
436=cut
437
438sub lists_groups {
439    my ($self, $sub, @search_args) = @_;
440
441    eval {
442    my $xx = $self->unlimited_search(
443        @search_args,
444        base => $self->top_dn,
445        filter => "(ObjectClass=group)",
446        callback => sub {
447            my ($mesg, $entry) = @_;
448            $mesg && $mesg->code and die $mesg->error;
449            $entry or return;
450            ref $entry eq 'Net::LDAP::Entry' or return;
451            $sub->($mesg, $entry);
452        },
453    );
454    };
455
456    return $@;
457}
458
459=head2 get_group
460
461=cut
462
463sub get_group {
464    my ($self, $groupname, @search_args) = @_;
465
466    my $mesg = $self->search(
467        @search_args,
468        filter => "(&(ObjectClass=group) (cn=$groupname))",
469        base => $self->top_dn,
470    );
471
472    $mesg->code and return;
473
474    my ($entry, @others) = $mesg->entries;
475
476    return if(@others); # we cannot have multiple entries...
477    $entry
478}
479
480=head2 create_group
481
482=cut
483
484sub create_group {
485    my ($self, $param) = @_;
486
487    my $entry = Net::LDAP::Entry->new;
488
489    my $groupname = $param->{name};
490    $entry->dn("cn=$groupname,cn=Users," . $self->top_dn);
491
492    $param->{gidNumber} ||= $self->find_next_gid;
493
494    $self->_defaults_group_attrs($entry,
495        {
496            objectClass => [ qw(top group) ],
497            %{ $param || {} },
498        }
499    );
500
501    my $mesg = $self->add($entry);
502
503    if ($mesg->code) {
504        warn $mesg->error;
505        return;
506    } else { return 1 };
507}
508
509=head2 delete_group
510
511=cut
512
513sub delete_group {
514    my ($self, $groupname) = @_;
515   
516    my $mesg = $self->search(
517        base => $self->{_top_dn},
518        filter => "(&(ObjectClass=group) (cn=$groupname))",
519    );
520
521    $mesg->code and do {
522        warn $mesg->error;
523        return;
524    };
525
526    my ($entry) = $mesg->entries; # TODO hopefully one
527
528    $mesg = $self->delete($entry->dn);
529
530    if ($mesg->code) {
531        warn $mesg->error;
532        return;
533    } else { return 1 }
534}
535
536sub get_group_users {
537    my ($self, $groupname, @searchargs) = @_;
538    my $gr = $self->get_group($groupname, attrs => [ qw(cn member) ]);
539
540    my @res;
541    foreach my $dnu (@{ $gr->get_value('member', asref => 1) || [] }) {
542        my $mesg = $self->search(
543            filter => '(objectClass=*)', # TODO can we get something else than user ?
544            @searchargs,
545            base => $dnu,
546        );
547
548        $mesg->code and return; # ensure error is propagate here
549        foreach my $entry ($mesg->entries) {
550           push(@res, $entry);
551       } 
552    }
553    @res
554}
555
556sub get_user_groups {
557    my ($self, $username, @searchargs) = @_;
558    my $user = $self->get_user($username);
559
560    my @res;
561    $self->unlimited_search(
562        base => $self->top_dn,
563        filter => sprintf(
564            '(&(objectClass=group)(member=%s))',
565            escape_filter_value($user->dn),
566        ),
567        @searchargs,
568        callback => sub {
569            my ($mesg, $entry) = @_;
570            ref $entry eq 'Net::LDAP::Entry' or return;
571            push(@res, $entry);
572        },
573    );
574
575    @res
576}
577
578sub add_user_group {
579    my ($self, $username, $groupname) = @_;
580
581    my $user = $self->get_user($username) or return;
582    my $group = $self->get_group($groupname) or return;
583
584    $group->add(member => $user->dn);
585
586    my $mesg = $group->update($self);
587    if ($mesg->code) {
588        warn $mesg->error;
589        return;
590    } else { return 1 };
591}
592
5931;
594
595__END__
596
597=head1 SEE ALSO
598
599=head1 AUTHOR
600
601Olivier Thauvin, E<lt>olivier.thauvin@aerov.jussieu.frE<gt>
602
603=head1 COPYRIGHT AND LICENSE
604
605Copyright (C) 2008 CNRS SA/CETP/LATMOS
606
607This library is free software; you can redistribute it and/or modify
608it under the same terms as Perl itself, either Perl version 5.10.0 or,
609at your option, any later version of Perl 5 you may have available.
610
611
612=cut
Note: See TracBrowser for help on using the repository browser.