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

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