source: LATMOS-Accounts/lib/LATMOS/Accounts/Bases.pm @ 777

Last change on this file since 777 was 777, checked in by nanardon, 14 years ago
  • fix la-sync over one single object: two pass are useless in this case so don't disconnect and reconnect, avoiding to change of ldap (ad) server
  • Property svn:keywords set to Id Rev
File size: 17.7 KB
Line 
1package LATMOS::Accounts::Bases;
2
3use 5.010000;
4use strict;
5use warnings;
6use LATMOS::Accounts::Bases::Objects;
7use LATMOS::Accounts::Log;
8use LATMOS::Accounts::Utils qw(exec_command to_ascii);
9
10our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
11
12=head1 NAME
13
14LATMOS::Accounts::Bases - Base class for account data bases
15
16=head1 SYNOPSIS
17
18  use LATMOS::Accounts::Bases;
19  my $base = LATMOS::Accounts::Bases->new('type', %options);
20  ...
21
22=head1 DESCRIPTION
23
24This module provide basic functions for various account base
25
26=head1 FUNTIONS
27
28=cut
29
30=head2 new($type, %options)
31
32Return, if success, a new data base account object, $type is
33account base type, %options to setup the base.
34
35=cut
36
37sub new {
38    my ($class, $type, %options) = @_;
39
40    my $pclass = ucfirst(lc($type));
41    eval "require LATMOS::Accounts::Bases::$pclass;";
42    if ($@) { return } # error message ?
43    my $base = "LATMOS::Accounts::Bases::$pclass"->new(%options);
44    $base->{_type} = lc($pclass);
45    $base->{_label} = $options{label};
46    $base->{_options} = { %options };
47    $base->{wexported} = 0;
48    $base->{defattr} = $options{defattr};
49    $base->{_acls} = $options{acls};
50    $base->{_allowed_values} = $options{allowed_values};
51    la_log(LA_DEBUG, 'Instanciate base %s (%s)', ($options{label} || 'N/A'), $pclass);
52    $base
53}
54
55sub wexported {
56    my ($self, $wexported) = @_;
57    my $old = $self->{wexported};
58    if (defined($wexported)) {
59        $self->{wexported} = $wexported;
60        $self->log(LA_DEBUG, "Switching exported mode: %s => %s", $old,
61            $wexported);
62    }
63    return($old || 0);
64}
65
66sub log {
67    my ($self, $level, $msg, @args) = @_;
68    my $prefix = 'Base(' . $self->type . '/' . $self->label . ')';
69    LATMOS::Accounts::Log::la_log($level, "$prefix $msg", @args);
70}
71
72sub label {
73    $_[0]->{_label} || 'NoLabel';
74}
75
76sub type {
77    $_[0]->{_type};
78}
79
80sub allowed_values {
81    $_[0]->{_allowed_values}
82}
83
84sub obj_attr_allowed_values {
85    my ($self, $otype, $attr) = @_;
86    if ($self->allowed_values) {
87        return $self->allowed_values->val("$otype.$attr", 'allowed');
88    }
89    return();
90}
91
92sub check_allowed_values {
93    my ($self, $otype, $attr, $attrvalues) = @_;
94    $self->allowed_values or return 1;
95    my @values = ref $attrvalues ? @{ $attrvalues } : $attrvalues;
96    foreach my $value (@values) {
97        $value or next;
98        if (my @allowed = $self->allowed_values->val("$otype.$attr", 'allowed')) {
99            grep { $value eq $_ } @allowed or do {
100                $self->log(LA_ERR,
101                    "value `%s' is not allow for %s.%s per configuration (allowed_values)",
102                    $value, $otype, $attr
103                );
104                return;
105            };
106        }
107    }
108    return 1;
109}
110
111sub _load_obj_class {
112    my ($self, $otype) = @_;
113
114    # finding perl class:
115    my $pclass = ref $self;
116    $pclass .= '::' . ucfirst(lc($otype));
117    eval "require $pclass;";
118    if ($@) {
119        $self->log(LA_DEBUG, 'Cannot load perl class %s', $pclass);
120        return
121    } # error message ?
122    return $pclass;
123}
124
125=head2 list_canonical_fields($otype, $for)
126
127Return the list of supported fields by the database for object type $otype.
128
129Optionnal $for specify the goal for which the list is requested, only supported
130fields will be returns
131
132=cut
133
134sub list_canonical_fields {
135    my ($self, $otype, $for) = @_;
136    $for ||= 'rw';
137    my $pclass = $self->_load_obj_class($otype) or return;
138    sort $pclass->_canonical_fields($self, $for);
139}
140
141sub delayed_fields {
142    my ($self, $otype, $for) = @_;
143    $for ||= 'rw';
144    my $pclass = $self->_load_obj_class($otype) or return;
145    $pclass->_delayed_fields($self, $for);
146}
147
148=head2 get_field_name($otype, $c_fields, $for)
149
150Return the internal fields name for $otype object for
151canonical fields $c_fields
152
153=cut
154
155sub get_field_name {
156    my ($self, $otype, $c_fields, $for) = @_;
157    $for ||= 'rw';
158    my $pclass = $self->_load_obj_class($otype) or return;
159    $pclass->_get_field_name($c_fields, $self, $for);
160}
161
162=head2 list_supported_objects(@otype)
163
164Return a list of supported object
165
166@type is an additionnal list of objects to check
167
168=cut
169
170sub list_supported_objects {
171    my ($self, @otype) = @_;
172    my %res;
173    foreach my $inc (@INC) {
174        my $sub = 'LATMOS::Accounts::Bases::' . ucfirst($self->type);
175        $sub =~ s/::/\//g;
176        foreach (glob("$inc/$sub/[A-Z]*.pm")) {
177            s/.*\///;
178            s/\.pm$//;
179            $res{lc($_)} = 1;
180        }
181    }
182    $res{$_} = 1 foreach(@otype);
183    my @sobj = grep { $self->is_supported_object($_) } keys %res;
184    la_log(LA_DEBUG, "Base %s supported objects: %s", $self->type, join(', ', @sobj));
185    return @sobj;
186}
187
188=head2 is_supported_object($otype)
189
190Return true is object type $otype is supported
191
192=cut
193
194sub is_supported_object {
195    my ($self, $otype) = @_;
196    return $self->_load_obj_class($otype) ? 1 : 0;
197}
198
199=head2 list_objects($otype)
200
201Return the list of UID for object of $otype.
202
203=cut
204
205sub list_objects {
206    my ($self, $otype) = @_;
207    my $pclass = $self->_load_obj_class($otype) or return;
208    $pclass->list($self);
209}
210
211=head2 get_object($type, $id)
212
213Return an object of $type (typically user or group) having identifier
214$id.
215
216=cut
217
218sub get_object {
219    my ($self, $otype, $id) = @_;
220
221    return LATMOS::Accounts::Bases::Objects->_new($self, $otype, $id);
222}
223
224=head2 create_object($type, $id, %data)
225
226Create and return an object of type $type with unique id
227$id having %data.
228
229This method should be provided by the data base handler.
230
231=cut
232
233sub create_object {
234    my ($self, $otype, $id, %data) = @_;
235    my $pclass = $self->_load_obj_class($otype);
236    if ($pclass->_create($self, $id, %data)) {
237        la_log(LA_INFO,
238            'Object %s (%s) created in base %s (%s)',
239            $id, $otype, $self->label, $self->type
240        );
241    } else {
242        la_log(LA_ERR,
243            'Object creation %s (%s) in base %s (%s) failed',
244            $id, $otype, $self->label, $self->type
245        );
246        return;
247    };
248    $self->get_object($otype, $id);
249}
250
251=head2 create_c_object($type, $id, %data)
252
253Create and return an object of type $type with unique id
254$id having %data using canonical fields
255
256=cut
257
258sub create_c_object {
259    my ($self, $otype, $id, %cdata) = @_;
260    $self->check_acl($otype, '@CREATE', 'w') or do {
261        $self->log(LA_WARN, 'permission denied to create object type %s',
262            $otype);
263        return;
264    };
265    if (my $chk = (lc($otype) eq 'user' || lc($otype) eq 'group')
266        ? LATMOS::Accounts::Utils::check_ug_validity($id)
267        : LATMOS::Accounts::Utils::check_oid_validity($id)) {
268        $self->log(LA_ERR, "Cannot create $otype with ID $id `%s:'", $chk);
269        return;
270    }
271    foreach my $cfield (keys %cdata) {
272        $self->check_allowed_values($otype, $cfield, $cdata{$cfield}) or do {
273            $self->log(LA_ERR, "Cannot create $otype, wrong value");
274            return;
275        };
276    }
277
278    $self->_create_c_object($otype, $id, %cdata);
279}
280
281sub _create_c_object {
282    my ($self, $otype, $id, %cdata) = @_;
283
284    # populating default value
285    foreach my $def (keys %{ $self->{defattr} || {}}) {
286        if ($def =~ /^$otype\.(.*)$/) {
287            $cdata{$1} = $self->{defattr}{$def} if(!$cdata{$1});
288        }
289    }
290    if (lc($otype) eq 'user') {
291        $cdata{homeDirectory} ||= $self->{defattr}{'user.homebase'} ?
292            $self->{defattr}{'user.homebase'} . "/$id" : '';
293        $cdata{uidNumber} ||= $self->find_next_numeric_id('user', 'uidNumber',
294            $self->{defattr}{'user.min_uid'}, $self->{defattr}{'user.max_uid'});
295        my $mailid = $cdata{givenName} && $cdata{sn}
296            ? sprintf('%s.%s',
297                to_ascii(lc($cdata{givenName})),
298                to_ascii(lc($cdata{sn})),)
299            : undef;
300
301        if ($mailid &&
302            $self->is_supported_object('aliases') &&
303            ! $self->get_object('aliases', $mailid)) {
304            if ($self->get_field_name($otype, 'mail', 'write')) {
305                if ($self->{defattr}{'user.maildomain'}) {
306                    $cdata{mail} ||= sprintf('%s@%s',
307                    $mailid,
308                    $self->{defattr}{'user.maildomain'});
309                }
310            }
311            if ($self->get_field_name($otype, 'aliases', 'write')) {
312                $cdata{aliases} ||= $mailid;
313            }
314            warn $self->get_field_name($otype, 'revaliases', 'write');
315            if ($self->get_field_name($otype, 'revaliases', 'write')) {
316                $cdata{revaliases} ||= $mailid;
317            }
318        }
319    } elsif (lc($otype) eq 'group') {
320        $cdata{gidNumber} ||= $self->find_next_numeric_id('group', 'gidNumber',
321            $self->{defattr}{'group.min_gid'}, $self->{defattr}{'group.max_gid'});
322    }
323    my %data;
324    foreach my $cfield (keys %cdata) {
325        my $field = $self->get_field_name($otype, $cfield, 'write') or next;
326        $data{$field} = $cdata{$cfield};
327    }
328    keys %data or return 0; # TODO: return an error ?
329    $self->create_object($otype, $id, %data);
330}
331
332=head2 delete_object($otype, $id)
333
334Destroy from data base object type $otype having id $id.
335
336=cut
337
338sub delete_object {
339    my ($self, $otype, $id) = @_;
340    my $obj = $self->get_object($otype, $id) or do {
341        $self->log(LA_WARN, 'Cannot delete %s/%s: no such object',
342            $otype, $id);
343        return;
344    };
345    $self->check_acl($obj, '@DELETE', 'w') or do {
346        $self->log(LA_WARN, 'permission denied to delete %s/%s',
347            $otype, $id);
348        return;
349    };
350    $self->_delete_object($otype, $id);
351}
352
353sub _delete_object {
354    my ($self, $otype, $id) = @_;
355    my $pclass = $self->_load_obj_class($otype);
356    $pclass->_delete($self, $id);
357}
358
359=head2 rename_object($otype, $id, $newid)
360
361Rename an object.
362
363=cut
364
365sub rename_object {
366    my ($self, $otype, $id, $newid) = @_;
367
368    my $obj = $self->get_object($otype, $id) or do {
369        $self->log(LA_WARN, 'Cannot rename %s/%s: no such object',
370            $otype, $id);
371        return;
372    };
373    if (my $chk = (lc($otype) eq 'user' || lc($otype) eq 'group')
374        ? LATMOS::Accounts::Utils::check_ug_validity($id)
375        : LATMOS::Accounts::Utils::check_oid_validity($id)) {
376        $self->log(LA_ERR, "Cannot rename $otype/$id to ID $newid `%s:'", $chk);
377        return;
378    }
379    $self->check_acl($obj, '@DELETE', 'w') &&
380    $self->check_acl($obj, '@CREATE', 'w') or do {
381        $self->log(LA_WARN, 'permission denied to rename %s/%s',
382            $otype, $id);
383        return;
384    };
385
386    $self->_rename_object($otype, $id, $newid);
387}
388
389sub _rename_object {
390    my ($self, $otype, $id, $newid) = @_;
391    my $pclass = $self->_load_obj_class($otype);
392    $pclass->can('_rename') or do {
393        $self->log(LA_ERR, 'rename object type %s is unsupported', $otype);
394        return;
395    };
396    $pclass->_rename($self, $id, $newid);
397}
398
399=head2 load
400
401Make account base loading data into memory if need.
402Should always be called, if database fetch data on the fly
403(SQL, LDAP), the function just return True.
404
405=cut
406
407sub load { 1 }
408
409=head2 is_transactionnal
410
411Return True is the database support commit and rollback
412
413=cut
414
415sub is_transactionnal {
416    my ($self) = @_;
417    return($self->can('_rollback') && $self->can('_commit'));
418}
419
420=head2 commit
421
422Save change into the database if change are not done immediately.
423This should always be called as you don't know when change are applied.
424
425Return always true if database does not support any transaction.
426
427The driver should provides a _commit functions to save data.
428
429=cut
430
431sub commit {
432    my ($self) = @_;
433    if ($self->can('_commit')) {
434        la_log(LA_DEBUG, 'Commiting data');
435        if (!(my $res = $self->_commit)) {
436            la_log(LA_ERR, "Commit error on %s", $_->label);
437            return $res;
438        }
439    }
440    return 1;
441}
442
443=head2 rollback
444
445If database support transaction, rollback changes. Return false
446if database does not support.
447
448If supported, driver should provides a _rollback functions
449
450=cut
451
452sub rollback {
453    my ($self) = @_;
454    if ($self->can('_rollback')) {
455       la_log(LA_DEBUG, 'Rolling back data');
456       return $self->_rollback;
457   } else {
458       return 0;
459   }
460}
461
462=head2 current_rev
463
464Return the current revision of the database
465
466Must be provide by base driver if incremental synchro is supported
467
468=cut
469
470sub current_rev { return }
471
472=head2 list_objects_from_rev($otype, $rev)
473
474Return the list of UID for object of $otype.
475
476=cut
477
478sub list_objects_from_rev {
479    my ($self, $otype, $rev) = @_;
480    my $pclass = $self->_load_obj_class($otype) or return;
481    if (defined($rev) && $pclass->can('list_from_rev')) {
482        return $pclass->list_from_rev($self, $rev);
483    } else {
484        # no support, return all objects...
485        return $self->list_objects($otype);
486    }
487}
488
489sub sync_object_from {
490    my ($self, $srcbase, $otype, $id, %options) = @_;
491
492    # is the object type supported by both
493    foreach ($self, $srcbase) {
494        $_->is_supported_object($otype) or return '';
495    }
496   
497    if (my $srcobj = $srcbase->get_object($otype, $id)) {
498        return $self->sync_object($srcobj, %options);
499    } elsif (!$options{nodelete}) {
500        $self->_delete_object($otype, $id) and return 'DELETED';
501    }
502    return;
503}
504
505=head2 sync_object
506
507Synchronise an object into this base
508
509=cut
510
511sub sync_object {
512    my ($self, $srcobj, %options) = @_;
513    $self->is_supported_object($srcobj->type) or return '';
514    my @fields = $options{attrs}
515        ? @{ $options{attrs} }
516        : $self->list_canonical_fields($srcobj->type, 'w');
517    my %data;
518    my %delayed = map { $_ => 1 } $self->delayed_fields($srcobj->type);
519    foreach (@fields) {
520        $srcobj->get_field_name($_, 'r') or next;
521        if (! $options{onepass}) {
522            if ($options{firstpass}) {
523                $delayed{$_} and next;
524            } else {
525                $delayed{$_} or next;
526            }
527        }
528        $data{$_} = $srcobj->_get_c_field($_);
529    }
530    keys %data or return '';
531    if (my $dstobj = $self->get_object($srcobj->type, $srcobj->id)) {
532        my $res = $dstobj->_set_c_fields(%data);
533        if (defined $res) {
534            return $res ? 'SYNCED' : '';
535        } else {
536            return;
537        }
538    } elsif(!$options{nocreate}) {
539        if ((! $options{firstpass}) && (!$options{onepass})) {
540            $self->log(LA_ERR, 'This is not first pass, creation wanted but denied');
541            return;
542        }
543        if ($self->_create_c_object($srcobj->type, $srcobj->id, %data)) {
544            return 'CREATED'
545        } else {
546            return;
547        }
548    } else {
549        # No error, but creation is denied
550        return 'Creation skipped';
551    }
552
553    return;
554}
555
556=head2 search_objects($otype, %filter)
557
558Search object according %filter. %filter is a list
559of field/value which should match.
560
561A default function is provided but each db driver can provide
562an optimize version.
563
564=cut
565
566sub search_objects {
567    my ($self, $otype, @filter) = @_;
568    my $pclass = $self->_load_obj_class($otype) or return;
569    $pclass->search($self, @filter);
570}
571
572sub attributes_summary {
573    my ($self, $otype, $attr) = @_;
574    my $pclass = $self->_load_obj_class($otype) or return;
575    $pclass->attributes_summary($self, $attr);
576}
577
578sub find_next_numeric_id {
579    my ($self, $otype, $field, $min, $max) = @_;
580    my $pclass = $self->_load_obj_class($otype) or return;
581    $pclass->find_next_numeric_id($self, $field, $min, $max);
582}
583
584sub authenticate_user {
585    my ($self, $username, $passwd) = @_;
586    $username or return;
587    my $uobj = $self->get_object('user', $username) or do {
588        la_log(LA_ERR, "Cannot authenticate non existing user $username");
589        return;
590    };
591
592    if ($self->get_field_name('user', 'exported', 'r')) {
593        if (!$uobj->_get_c_field('exported')) {
594            la_log(LA_ERR, "User $username found but currently unexported");
595            return;
596        }
597    }
598
599    if (my $expire = $uobj->_get_c_field('shadowExpire')) {
600        if ($expire > 0 && $expire < int(time / ( 3600 * 24 ))) {
601            la_log(LA_ERR, "Account $username has expired (%d / %d)",
602                $expire, int(time / ( 3600 * 24 )));
603            return;
604        }
605    }
606
607    if ($uobj->_get_c_field('locked')) {
608        la_log(LA_ERR, "Account $username is currently locked");
609        return;
610    }
611
612    my $password = $uobj->_get_c_field('userPassword') or do {
613        la_log(LA_ERR, "Cannot authenticate user $username having no passwd");
614        return;
615    };
616    if ($password eq crypt($passwd, $password)) { # crypt unix
617        la_log(LA_NOTICE, "User $username authenticated");
618        return 1;
619    } else {
620        la_log(LA_ERR, "Cannot authenticate user $username");
621        return 0;
622    }
623}
624
625sub connect {
626    my ($self, $username, $password) = @_;
627    my $auth = $self->authenticate_user($username, $password);
628    if ($auth) {
629        $self->{_user} = $username;
630        la_log(LA_DEBUG, "Connected as $username");
631    }
632    return $auth;
633}
634
635sub check_acl {
636    my ($self, $obj, $attr, $perm) = @_;
637    if ($self->{_acls}) {
638        my ($who, $groups) = ($self->{_user} || '');
639        if ($who && (my $uo = $self->get_object('user', $who))) {
640            $groups = [ $uo->_get_attributes('memberOf') ];
641        } else {
642            $who = '';
643        }
644        my $res = $self->{_acls}->check($obj, $attr, $perm, $who, $groups);
645        $self->log(LA_INFO, 'permission denied for "%s" to get %s.%s for %s',
646           $who, ref $obj ? $obj->id . '(' . $obj->type . ')' : $obj, $attr, $perm) if (!$res);
647        return $res;
648    } else {
649        # No acls, woot
650        return 1;
651    }
652}
653
654sub text_empty_dump {
655    my ($self, $fh, $otype, $options) = @_;
656    my $pclass = $self->_load_obj_class($otype) or return;
657    $pclass->text_dump($fh, $options, $self);
658}
659
6601;
661
662__END__
663
664=head1 SEE ALSO
665
666=head1 AUTHOR
667
668Thauvin Olivier, E<lt>olivier.thauvin@latmos.ipsl.fr<gt>
669
670=head1 COPYRIGHT AND LICENSE
671
672Copyright (C) 2009 by Thauvin Olivier
673
674This library is free software; you can redistribute it and/or modify
675it under the same terms as Perl itself, either Perl version 5.10.0 or,
676at your option, any later version of Perl 5 you may have available.
677
678=cut
Note: See TracBrowser for help on using the repository browser.