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

Last change on this file since 63 was 63, checked in by nanardon, 15 years ago
  • start Base::Ad from old work LATMOS::Ad
  • Property svn:keywords set to Id Rev
File size: 10.7 KB
Line 
1package LATMOS::Accounts::Bases::Ad;
2
3use 5.010000;
4use strict;
5use warnings;
6
7use base qw(LATMOS::Accounts::Bases);
8use Net::LDAP;
9use Net::LDAP::Entry;
10use Net::LDAP::Control::Paged;
11use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED ); 
12use Net::LDAP::Util     qw( escape_filter_value );
13
14our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
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    $options{domain} or return;
51    my $self = {
52        _server => $options{server},
53        _ad_domain => $options{domain},
54        _top_dn => join(',', map { "dc=$_" } split('\.', $options{domain})),
55        _login => $options{login},
56        _password => $options{password},
57        _ssl => $options{ssl},
58    };
59
60    bless($self, $class);
61}
62
63sub load {
64    my ($self) = @_;
65    # At this point, if still no $server, DNS search
66
67    my $ldap;
68    foreach my $tryserv (
69        $self->{server}
70        ? ($self->_ldap_url($self->{_server}))
71        : $self->_query_zone_ads) {
72        $ldap = Net::LDAP->new(
73            $tryserv,
74        ) and last;
75    }
76
77    $ldap or return; # connot connect to any ldap :\
78
79    my $login = $self->{_login};
80    $login =~ m/@/ or $login .= '@' . $self->ad_domain;
81
82    my $msg = $ldap->bind($login, password => $self->{_password}) or return;
83    $msg->code and do {
84        warn $msg->error;
85        return;
86    };
87
88    $self->{_ldap} = $ldap;
89    return 1;
90}
91
92sub _ldap_url {
93    my ($self, $host, $port) = @_;
94
95    sprintf(
96        '%s://%s%s/',
97        $self->{_ssl} ? 'ldaps' : 'ldap',
98        $host,
99        $port ? ":$port" : '',
100    )
101}
102
103sub _query_zone_ads {
104    my ($self) = @_;
105    require Net::DNS;
106    my @urllist;
107
108    my $resolver = Net::DNS::Resolver->new;
109    my $query = $resolver->query("_ldap._tcp.dc._msdcs." . $self->ad_domain, "SRV");
110    foreach my $rr (
111        sort { $a->priority <=> $b->priority || $a->weight <=> $b->weight }
112        grep { $_->type eq 'SRV' } $query->answer) {
113        push(@urllist, $self->_ldap_url($rr->target, $rr->port));
114    }
115
116    @urllist
117}
118
119sub ldap {
120    return $_[0]->{_ldap};
121}
122
123=head2 top_dn
124
125Return the TOP DN of the AD zone.
126
127=cut
128
129sub top_dn {
130    return $_[0]->{_top_dn}
131}
132
133=head2 ad_domain
134
135Return the active directory zone
136
137=cut
138
139sub ad_domain {
140    return $_[0]->{_ad_domain}
141}
142
143=head2 unlimited_search
144
145By default, ldap servers limit results to avoid deni of services.
146LDAP protocol provide a paging feature to fetch all results.
147
148This function works like Net::LDAP::search functions, but return nothing,
149use "callback" option to get entries.
150
151=cut
152
153sub _unlimited_search {
154    my $self = shift;
155    my @args = @_;
156
157    my ($page, $cookie) = (Net::LDAP::Control::Paged->new( size => 100 ));
158
159    while (1) {
160        my $search = $self->ldap->search(
161            @args,
162            control => [ $page ],
163        );
164        if ($search->code) {
165            return $search;
166        }
167
168        ### After foreach loops ends, client checks LDAP server reponse of how many search results total.
169        ### This is a control to end the infinite while loop if there are no search results to go through
170        ### If there are search results, this control will always return the total number of results. It is
171        ### never decremented
172        my ($resp) = $search->control( LDAP_CONTROL_PAGED ) or last;
173
174
175        ### Obtaining the cookie from the search result. When no more results, cookie will be NULL
176        ### and infinite while loop will terminate.
177        $cookie = $resp->cookie or last;
178
179        ### Sets cookie so server knows the next search result to send
180        $page->cookie($cookie);
181    }
182    ### This is a control to check for abnormal exit of the while loop.
183    ### If this occurs ### we need to tell the LDAP server that remaining
184    ### search results are no longer needed ### by sending a search request with a page size of 0
185    if ($cookie)    {
186        $page->cookie($cookie);
187        $page->size(0);
188        $self->search(@args, control => [ $page ]);
189    }
190    return 1;
191}
192
193sub find_username {
194    my ($self, $lastname, $firstname) = @_;
195    if (!ref $self) { # if call w/o ldap connection
196        ($self, $lastname, $firstname) = (undef, $self, $lastname);
197    }
198
199    my $username = _username_format($lastname);
200    if (!$self) { return $username }
201
202    if (!$self->get_user($lastname, attrs => [ 'cn' ])) {
203        return $username;
204    }
205
206    foreach $username (map { _username_format($_) } (
207            $lastname . substr($firstname, 0, 1),
208            $lastname . $firstname,
209        )) {
210        !$self->get_user($username, attrs => [ 'cn' ]) and return $username;
211    }
212
213    undef
214}
215
216sub _defaults_user_attrs {
217    my ($self, $entry, $attrs) = @_;
218
219    foreach my $attr (keys %{ $attrs || {} }) {
220        my $val = $attrs->{$attr};
221
222        $attr =~ /^homeDirectory$/ and do {
223            $entry->replace('unixHomeDirectory', $val);
224        };
225
226        $attr =~ /^(givenName|sn)$/ and do {
227            $entry->replace('displayName',
228                join(' ', map { ucfirst($_ || '') } (
229                    ($attrs->{'givenName'} || $entry->get_value('givenName')), # first name
230                    ($attrs->{'sn'} || $entry->get_value('sn')),           # last name
231                ))
232            );
233            $entry->replace('gecos', # TODO reencode to aovid accents
234                join(' ', map { ucfirst($_ || '') } (
235                    ($attrs->{'givenName'} || $entry->get_value('givenName')), # first name
236                    ($attrs->{'sn'} || $entry->get_value('sn')),           # last name
237                ))
238            );
239            $attr eq 'sn' and do { # TODO generate clean login here / UNIX uid
240                $entry->add('sAMAccountName' => $val) unless ($entry->exists('sAMAccountName'));
241            };
242        };
243
244        # nothing special to do
245        $entry->replace($attr => $val);
246    }
247}
248
249sub _defaults_group_attrs {
250    my ($self, $entry, $attrs) = @_;
251    foreach my $attr (keys %{ $attrs || {} }) {
252        my $val = $attrs->{$attr};
253        $entry->replace($attr => $val);
254    }
255}
256
257=head2 get_user($username)
258
259Return the entry for user $username
260
261=cut
262
263sub get_user {
264    my ($self, $username, @search_args) = @_;
265
266    my $mesg = $self->search(
267        @search_args,
268        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
269        base => $self->top_dn,
270    );
271
272    $mesg->code and return;
273
274    my ($entry, @others) = $mesg->entries;
275
276    return if(@others); # we cannot have multiple entries...
277    $entry
278}
279
280=head2 modify_user($username, $param)
281
282=cut
283
284sub modify_user {
285    my ($self, $username, $param) = @_;
286
287    my $mesg = $self->search(
288        base => $self->{_top_dn},
289        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
290    );
291
292    $mesg->code and do {
293        warn $mesg->error;
294        return;
295    };
296
297    my ($entry) = $mesg->entries; # TODO hopefully only one...
298
299    $self->_defaults_user_attrs(
300        $entry,
301        $param,
302    );
303
304    $mesg = $entry->update($self);
305
306    if ($mesg->code) {
307        warn $mesg->error;
308        return;
309    } else { return 1 }
310}
311
312=head2 delete_user($username)
313
314=cut
315
316sub delete_user {
317    my ($self, $username) = @_;
318
319    my $mesg = $self->search(
320        base => $self->{_top_dn},
321        filter => "(&(ObjectClass=user) (!(ObjectClass=computer)) (cn=$username))",
322    );
323
324    $mesg->code and do {
325        warn $mesg->error;
326        return;
327    };
328
329    my ($entry) = $mesg->entries; # TODO hopefully one
330
331    $mesg = $self->delete($entry->dn);
332
333    if ($mesg->code) {
334        warn $mesg->error;
335        return;
336    } else { return 1 }
337}
338
339=head2 create_group
340
341=cut
342
343sub create_group {
344    my ($self, $param) = @_;
345
346    my $entry = Net::LDAP::Entry->new;
347
348    my $groupname = $param->{name};
349    $entry->dn("cn=$groupname,cn=Users," . $self->top_dn);
350
351    $param->{gidNumber} ||= $self->find_next_gid;
352
353    $self->_defaults_group_attrs($entry,
354        {
355            objectClass => [ qw(top group) ],
356            %{ $param || {} },
357        }
358    );
359
360    my $mesg = $self->add($entry);
361
362    if ($mesg->code) {
363        warn $mesg->error;
364        return;
365    } else { return 1 };
366}
367
368=head2 delete_group
369
370=cut
371
372sub delete_group {
373    my ($self, $groupname) = @_;
374   
375    my $mesg = $self->search(
376        base => $self->{_top_dn},
377        filter => "(&(ObjectClass=group) (cn=$groupname))",
378    );
379
380    $mesg->code and do {
381        warn $mesg->error;
382        return;
383    };
384
385    my ($entry) = $mesg->entries; # TODO hopefully one
386
387    $mesg = $self->delete($entry->dn);
388
389    if ($mesg->code) {
390        warn $mesg->error;
391        return;
392    } else { return 1 }
393}
394
395sub get_group_users {
396    my ($self, $groupname, @searchargs) = @_;
397    my $gr = $self->get_group($groupname, attrs => [ qw(cn member) ]);
398
399    my @res;
400    foreach my $dnu (@{ $gr->get_value('member', asref => 1) || [] }) {
401        my $mesg = $self->search(
402            filter => '(objectClass=*)', # TODO can we get something else than user ?
403            @searchargs,
404            base => $dnu,
405        );
406
407        $mesg->code and return; # ensure error is propagate here
408        foreach my $entry ($mesg->entries) {
409           push(@res, $entry);
410       } 
411    }
412    @res
413}
414
415sub get_user_groups {
416    my ($self, $username, @searchargs) = @_;
417    my $user = $self->get_user($username);
418
419    my @res;
420    $self->unlimited_search(
421        base => $self->top_dn,
422        filter => sprintf(
423            '(&(objectClass=group)(member=%s))',
424            escape_filter_value($user->dn),
425        ),
426        @searchargs,
427        callback => sub {
428            my ($mesg, $entry) = @_;
429            ref $entry eq 'Net::LDAP::Entry' or return;
430            push(@res, $entry);
431        },
432    );
433
434    @res
435}
436
437sub add_user_group {
438    my ($self, $username, $groupname) = @_;
439
440    my $user = $self->get_user($username) or return;
441    my $group = $self->get_group($groupname) or return;
442
443    $group->add(member => $user->dn);
444
445    my $mesg = $group->update($self);
446    if ($mesg->code) {
447        warn $mesg->error;
448        return;
449    } else { return 1 };
450}
451
4521;
453
454__END__
455
456=head1 SEE ALSO
457
458=head1 AUTHOR
459
460Olivier Thauvin, E<lt>olivier.thauvin@aerov.jussieu.frE<gt>
461
462=head1 COPYRIGHT AND LICENSE
463
464Copyright (C) 2008 CNRS SA/CETP/LATMOS
465
466This library is free software; you can redistribute it and/or modify
467it under the same terms as Perl itself, either Perl version 5.10.0 or,
468at your option, any later version of Perl 5 you may have available.
469
470
471=cut
Note: See TracBrowser for help on using the repository browser.