source: trunk/LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Objects.pm @ 2220

Last change on this file since 2220 was 2219, checked in by nanardon, 5 years ago

Add attribute caching when compute

  • Property svn:keywords set to Id Rev
File size: 20.5 KB
Line 
1package LATMOS::Accounts::Bases::Objects;
2
3use 5.010000;
4use strict;
5use warnings;
6
7use overload '""' => 'stringify';
8
9use LATMOS::Accounts::Log;
10use LATMOS::Accounts::Bases::Attributes;
11use Crypt::Cracklib;
12
13our $VERSION = (q$Rev: 2072 $ =~ /^Rev: (\d+) /)[0];
14
15=head1 NAME
16
17LATMOS::Accounts::Bases::Objects - Base class for account objects
18
19=head1 SYNOPSIS
20
21  use LATMOS::Accounts::Bases::Objects;
22  LATMOS::Accounts::Bases::Objects->new($base, $type, $id);
23
24=head1 DESCRIPTION
25
26=head1 FUNCTIONS
27
28=cut
29
30=head2 is_supported
31
32If exists, must return true or false if the object is supported or not
33
34=cut
35
36=head2 list($base)
37
38List object supported by this module existing in base $base
39
40Must be provide by object class
41
42    sub list {
43        my ($class, $base) = @_;
44    }
45
46=cut
47
48=head2 listReal($base)
49
50List object supported by this module existing in base $base
51
52Can be override by base driver. The result must exclude specials object such alias.
53
54=cut
55
56sub listReal {
57    my ($class, $base) = @_;
58    $class->list($base);
59}
60
61=head2 list_from_rev($base, $rev)
62
63List objects create or modified after base revision C<$rev>.
64
65=cut
66
67=head2 new($base, $id)
68
69Create a new object having $id as uid.
70
71=cut
72
73sub new {
74    my ($class, $base, $id, @args) = @_;
75    # So can be call as $class->SUPER::new()
76    bless {
77        _base => $base,
78        _type => lc(($class =~ m/::([^:]*)$/)[0]),
79        _id => $id,
80    }, $class;
81}
82
83=head2 _create($class, $base, $id, %data)
84
85Must create a new object in database.
86
87Is called if underling base does not override create_object
88
89    sub _create(
90        my ($class, $base, $id, %data)
91    }
92
93=cut
94
95=head2 type
96
97Return the type of the object
98
99=cut
100
101sub type {
102    my ($self) = @_;
103    if (ref $self) {
104        return $self->{_type}
105    } else {
106        return lc(($self =~ /::([^:]+)$/)[0]);
107    }
108}
109
110=head2 base
111
112Return the base handle for this object.
113
114=cut
115
116sub base {
117    return $_[0]->{_base}
118}
119
120=head2 id
121
122Must return the unique identifier for this object
123
124=cut
125
126sub id {
127    my ($self) = @_;
128    $self->{_id}
129}
130
131=head2 Iid
132
133Return internal id if different from Id
134
135=cut
136
137sub Iid {
138    my ($self) = @_;
139    $self->id
140}
141
142=head2 stringify
143
144Display object as a string
145
146=cut
147
148sub stringify {
149    my ($self) = @_;
150
151    return $self->id
152}
153
154=head2 list_canonical_fields($for)
155
156Object shortcut to get the list of field supported by the object.
157
158=cut
159
160sub list_canonical_fields {
161    my ($self, $for) = @_;
162    $for ||= 'rw';
163    $self->_canonical_fields($for);
164}
165
166=head2 attribute ($attribute)
167
168Return L<LATMOS::Accounts::Bases::Attributes> object for C<$attribute>
169
170=cut
171
172sub attribute {
173    my ($self, $attribute) = @_;
174
175    my $attrinfo;
176    if (! ref $attribute) {
177        $attrinfo = $self->_get_attr_schema(
178            $self->base)->{$attribute}
179        or return;
180        $attrinfo->{name} = $attribute;
181    } else {
182        $attrinfo = $attribute;
183    }
184
185    return LATMOS::Accounts::Bases::Attributes->new(
186        $attrinfo,
187        $self,
188    );
189}   
190
191sub _canonical_fields {
192    my ($class, $base, $for) = @_;
193    $for ||= 'rw';
194    my $info = $base->_get_attr_schema($class->type);
195    my @attrs = map { $base->attribute($class->type, $_) } keys %{$info || {}};
196    @attrs = grep { ! $_->ro } @attrs if($for =~ /w/);
197    @attrs = grep { $_->readable } @attrs if($for =~ /r/);
198    @attrs = grep { !$_->hidden }  @attrs unless($for =~ /a/);
199    map { $_->name } @attrs;
200}
201
202=head2 GetOtypeDef
203
204This function is called to provide sub object definition. Must be overwritten
205per object class when need.
206
207Must return a hashref listing each sub object type and their related key atribute:
208
209    return {
210        addresses => 'user',
211    }
212
213=cut
214
215sub GetOtypeDef {
216    my ($class) = @_;
217
218    return;
219}
220
221=head2 get_field($field)
222
223Return the value for $field, must be provide by data base.
224
225    sub get_field {
226        my ($self, $field)
227    }
228
229=cut
230
231=head2 get_c_field($cfield)
232
233Return the value for canonical field $cfield.
234
235Call driver specific get_field()
236
237=cut
238
239sub get_c_field {
240    my ($self, $cfield) = @_;
241    $self->base->check_acl($self, $cfield, 'r') or do {
242        $self->base->log(LA_ERR, "Permission denied to get %s/%s",
243            $self->id, $cfield
244        );
245        return;
246    };
247    return $self->_get_c_field($cfield);
248}
249
250=head2 get_attributes($attr)
251
252Like get_c_field but always return an array
253
254=cut
255
256sub get_attributes {
257    my ($self, $cfield) = @_;
258    my $res = $self->get_c_field($cfield);
259    if ($res) {
260        return(ref $res ? @{$res} : $res);
261    } else {
262        return;
263    }
264}
265
266sub _get_attributes {
267    my ($self, $cfield) = @_;
268    my $res = $self->_get_c_field($cfield);
269    if ($res) {
270        return(ref $res ? @{$res} : ($res));
271    } else {
272        return;
273    }
274}
275
276sub _get_c_field {
277    my ($self, $cfield) = @_;
278    my $attribute = $self->attribute($cfield) or do {
279        $self->base->log(LA_WARN, "Unknow attribute $cfield");
280        return;
281    };
282    $attribute->readable or do {
283        $self->base->log(LA_WARN, "Attribute $cfield is not readable");
284        return;
285    };
286    return $attribute->get; 
287}
288
289=head2 GetAttributeValue($cfield)
290
291Return the value to exposed to other base
292
293=cut
294
295sub GetAttributeValue {
296    my ($self, $cfield) = @_;
297
298    return $self->get_c_field($cfield);
299}
300
301
302=head2 queryformat ($fmt)
303
304Return formated string according C<$fmt>
305
306=cut
307
308sub queryformat {
309    my ($self, $fmt) = @_;
310    $fmt =~ s/\\n/\n/g;
311    $fmt =~ s/\\t/\t/g;
312    $fmt =~ s!
313        (?:%\{([^:}]*)(?::([^}]+))?\})
314        !
315        my $val = $self->get_c_field($1);
316        sprintf('%' . ($2 || 's'), ref $val ? join(',', @$val) : (defined($val) ? $val : ''))
317        !egx;
318    $fmt;
319}
320
321=head2 set_fields(%data)
322
323Set values for this object. %data is a list or peer field => values.
324
325    sub set_fields {
326        my ($self, %data) = @_;
327    }
328
329=cut
330
331=head2 checkValues ($base, $obj, %attributes)
332
333Allow to pre-check values when object are modified or created
334
335C<$obj> is either the new id at object creation or the object itself on modification.
336
337=cut
338
339sub checkValues {
340    my ($class, $base, $obj, %attributes) = @_;
341
342    return 1;
343}
344
345=head2 check_allowed_values ($attr, $values)
346
347Check if value C<$values> is allowed for attributes C<$attr>
348
349=cut
350
351sub check_allowed_values {
352    my ($self, $attr, $values) = @_;
353    $self->base->check_allowed_values($self->type, $attr, $values);
354}
355
356=head2 attr_allow_values ($attr)
357
358Return allowed for attribute C<$attr>
359
360=cut
361
362sub attr_allow_values {
363    my ($self, $attr) = @_;
364    return $self->base->obj_attr_allowed_values(
365        $self->type,
366        $attr,
367    );
368}
369
370=head2 set_c_fields(%data)
371
372Set values for this object. %data is a list or peer
373canonical field => values. Fields names are translated.
374
375=cut
376
377sub set_c_fields {
378    my ($self, %cdata) = @_;
379    foreach my $cfield (keys %cdata) {
380        $self->base->check_acl($self, $cfield, 'w') or do { 
381            $self->base->log(LA_ERR, "Cannot modified %s/%s: %s",
382                $self->type, $self->id, "permission denied");
383            return;
384        };
385    }
386
387    foreach my $cfield (keys %cdata) {
388        $self->check_allowed_values($cfield, $cdata{$cfield}) or do {
389            $self->base->log(LA_ERR, "Cannot modified %s/%s: %s",
390                $self->type, $self->id, "non authorized value");
391            return;
392        };
393    }
394
395    $self->_set_c_fields(%cdata);
396}
397
398sub _set_c_fields {
399    my ($self, %cdata) = @_;
400    my %data;
401    my $res = 0;
402    foreach my $cfield (keys %cdata) {
403        my $attribute = $self->attribute($cfield) or do {
404            $self->base->log(LA_ERR,
405                "Cannot set unsupported attribute %s to %s (%s)",
406                $cfield, $self->id, $self->type
407            );
408            return;
409        };
410        $attribute->ro and do {
411            $self->base->log(LA_ERR,
412                "Cannot set read-only attribute %s to %s (%s)",
413                $cfield, $self->id, $self->type
414            );
415            return;
416        };
417
418        if (!$attribute->checkinput($cdata{$cfield})) {
419            $self->base->log(LA_ERR,
420                "Value for attribute %s to %s (%s) does not match requirements",
421                $cfield, $self->id, $self->type
422            );
423            return;
424        };
425    }
426
427    if (!$self->checkValues($self->base, $self, %cdata)) {
428        my $last = LATMOS::Accounts::Log::lastmessage(LA_ERR);
429        $self->base->log(LA_ERR,
430            "Cannot update %s (%s): wrong value%s",
431            $self->id, $self->type,
432            ($last ? ": $last" : $last)
433        );
434        return;
435    }
436
437    my %updated = ();
438    foreach my $cfield (keys %cdata) {
439        my $attribute = $self->attribute($cfield) or do {
440            $self->base->log(LA_ERR,
441                "Cannot set unsupported attribute %s to %s (%s)",
442                $cfield, $self->id, $self->type
443            );
444            return;
445        };
446        if ($attribute->set($cdata{$cfield})) {
447            $updated{$cfield} = $attribute->monitored;
448        }
449    }
450
451    if (keys %updated) {
452        $self->PostSetAttribute() or do {
453            $self->base->log(LA_ERR, "PostSetAttribute failed when updating %s/%s",
454                $self->type, $self->id);
455            return;
456        };
457
458        $self->ReportChange('Update', 'Attributes %s have been updated', join(', ', sort keys %updated));
459        foreach (sort keys %updated) {
460            $self->ReportChange('Attributes', '%s set to %s', $_, 
461                (ref $cdata{$_}
462                    ? join(', ', sort @{ $cdata{$_} })
463                    : $cdata{$_}) || '(none)')
464                if ($updated{$_});
465        }
466    }
467    return scalar(keys %updated);
468}
469
470=head2 PostSetAttribute
471
472This function is call to compute data when object is modify.
473
474=cut
475
476sub PostSetAttribute {
477    my ($self) = @_;
478
479    return 1;
480}
481
482=head2 addAttributeValue($attribute, $value)
483
484Add a value to a multivalue attributes
485
486=cut
487
488sub _addAttributeValue {
489    my ($self, $attribute, @values) = @_;
490
491    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
492    $self->_set_c_fields($attribute => [ @oldvalues, @values ]);
493}
494
495sub addAttributeValue {
496    my ($self, $attribute, @values) = @_;
497
498    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
499    $self->set_c_fields($attribute => [ @oldvalues, @values ]);
500}
501
502=head2 delAttributeValue($attribute, $value)
503
504Remove a value to a multivalue attributes
505
506=cut
507
508sub _delAttributeValue {
509    my ($self, $attribute, @values) = @_;
510
511    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
512
513    foreach my $value (@values) {
514        @oldvalues = grep { $_ ne $value } @oldvalues;
515    }
516
517    $self->_set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
518}
519
520sub delAttributeValue {
521    my ($self, $attribute, @values) = @_;
522
523    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
524
525    foreach my $value (@values) {
526        @oldvalues = grep { $_ ne $value } @oldvalues;
527    }
528
529    $self->set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
530}
531
532=head2 set_password($password)
533
534Set the password into the database, $password is the clear version
535of the password.
536
537This function store it into userPassword canonical field if supported
538using crypt unix and md5 algorythm (crypt md5), the salt is 8 random
539caracters.
540
541The base driver should override it if another encryption is need.
542
543=cut
544
545sub set_password {
546    my ($self, $clear_pass) = @_;
547    if ($self->base->check_acl($self, 'userPassword', 'w')) {
548        if ($self->_set_password($clear_pass)) {
549             $self->ReportChange('Password', 'user password has changed');
550             return 1;
551        } else {
552            return;
553        }
554    } else {
555        $self->base->log(LA_ERROR, "Permission denied for %s to change its password",
556            $self->id);
557        return;
558    }
559}
560
561sub _set_password {
562    my ($self, $clear_pass) = @_;
563    if (my $attribute = $self->base->attribute($self->type, 'userPassword')) {
564        my $res = $self->set_fields($attribute->iname, $self->base->passCrypt($clear_pass));
565        $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id)
566            if($res);
567        return $res;
568    } else {
569        $self->base->log(LA_WARN,
570            "Cannot set password: userPassword attributes is unsupported");
571    }
572}
573
574=head2 check_password ($password)
575
576Check given password is secure using L<Crypt::Cracklib>
577
578=cut
579
580sub check_password {
581    my ( $self, $password ) = @_;
582    my $dictionary = $self->base->config('cracklib_dictionnary');
583
584    if ($password !~ /^[[:ascii:]]*$/) {
585       return "the password must contains ascii characters only";
586    }
587
588    return fascist_check($password, $dictionary);
589}
590
591=head2 InjectCryptPasswd($cryptpasswd)
592
593Inject a password encrypted using standard UNIX method.
594
595=cut
596
597sub InjectCryptPasswd {
598    my ($self, $cryptpasswd) = @_;
599
600    if ($self->can('_InjectCryptPasswd')) {
601        return $self->_InjectCryptPasswd($cryptpasswd);
602    } else {
603        $self->base->log(LA_ERR, 'Injecting unix crypt password is not supported');
604        return;
605    }
606}
607
608=head2 search ($base, @filter)
609
610Search object matching C<@filter>
611
612=cut
613
614sub search {
615    my ($class, $base, @filter) = @_;
616    my @results;
617    my %parsed_filter;
618    while (my $item = shift(@filter)) {
619        # attr=foo => no extra white space !
620        # \W is false, it is possible to have two char
621        my ($attr, $mode, $val) = $item =~ /^(\w+)(?:(\W)(.+))?$/ or next;
622        if (!$mode) {
623            $mode = '~';
624            $val = shift(@filter);
625        }
626        push(
627            @{$parsed_filter{$attr}},
628            {
629                attr => $attr,
630                mode => $mode,
631                val  => $val,
632            }
633        );
634    }
635    foreach my $id ($base->list_objects($class->type)) {
636        my $obj = $base->get_object($class->type, $id);
637        my $match = 1;
638        foreach my $field (keys %parsed_filter) {
639            $base->attribute($class->type, $field) or
640                la_log(LA_WARN, "Unsupported attribute %s", $field);
641            my $tmatch = 0;
642            foreach (@{$parsed_filter{$field}}) {
643                my $value = $_->{val};
644                my $fval = $obj->_get_c_field($field) || '';
645                if ($value eq '*') {
646                    if ($fval ne '') {
647                        $tmatch = 1;
648                        last;
649                    }
650                } elsif ($value eq '!') {
651                    if ($fval eq '') {
652                        $match = 1;
653                        last;
654                    }
655                } elsif ($_->{mode} eq '=') {
656                    if ($fval eq $value) {
657                        $tmatch = 1;
658                        last;
659                    }
660                } elsif($_->{mode} eq '~') {
661                    if ($fval =~ m/\Q$value\E/i) {
662                        $tmatch = 1;
663                        last;
664                    }
665                }
666            }
667            $match = 0 unless($tmatch);
668        }
669        push(@results, $id) if($match);
670    }
671    @results;
672}
673
674=head2 attributes_summary ($base, $attribute)
675
676Return list of values existing in base for C<$attribute>
677
678=cut
679
680sub attributes_summary {
681    my ($class, $base, $attribute) = @_;
682    my $attr = $base->attribute($class->type, $attribute) or do {
683        $base->log(LA_ERR, "Cannot instantiate %s attribute", $attribute);
684        return;
685    };
686    if (!$attr->readable) {
687        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
688        return;
689    }
690    if (!$base->check_acl($class->type, $attribute, 'r')) {
691        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
692        return;
693    }
694    my %values;
695    foreach my $id ($base->list_objects($class->type)) {
696        my $obj = $base->get_object($class->type, $id);
697        my $value = $obj->_get_c_field($attribute);
698        if ($value) {
699            if (ref $value) {
700                foreach (@$value) {
701                    $values{$_} = 1;
702                }
703            } else {
704                $values{$value} = 1;
705            }
706        }
707    }
708    return sort(keys %values);
709}
710
711=head2 attributes_summary_by_object ($base, $attribute)
712
713Return list of peer object <=> values
714
715=cut
716
717sub attributes_summary_by_object {
718    my ($class, $base, $attribute) = @_;
719    my $attr = $base->attribute($class->type, $attribute) or do {
720        $base->log(LA_ERR, "Cannot instantiate %s attribute", $attribute);
721        return;
722    };
723    if (!$attr->readable) {
724        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
725        return;
726    }
727    if (!$base->check_acl($class->type, $attribute, 'r')) {
728        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
729        return;
730    }
731    my %values;
732    foreach my $id ($base->list_objects($class->type)) {
733        my $obj = $base->get_object($class->type, $id);
734        my $value = $obj->_get_c_field($attribute);
735        if ($value) {
736            if (ref $value) {
737                foreach (@$value) {
738                    push(@{ $values{ $id } }, $_);
739                }
740            } else {
741                push(@{ $values{ $id } }, $value);
742            }
743        }
744    }
745    return %values;
746}
747
748=head2 find_next_numeric_id ($base, $field, $min, $max)
749
750Find next free uniq id for attribute C<$field>
751
752=cut
753
754sub find_next_numeric_id {
755    my ($class, $base, $field, $min, $max) = @_;
756    $base->attribute($class->type, $field) or return;
757    $min ||= 
758        $field eq 'uidNumber' ? 500 :
759        $field eq 'gidNumber' ? 500 :
760        1;
761    $max ||= 65635;
762    $base->log(LA_DEBUG, "Trying to find %s in range %d - %d",
763        $field, $min, $max);
764    my %existsid;
765    $base->temp_switch_unexported(sub {
766        foreach ($base->list_objects($class->type)) {
767            my $obj = $base->get_object($class->type, $_) or next;
768            my $id = $obj->_get_c_field($field) or next;
769            $existsid{$id + 0} = 1;
770        }
771    }, 1);
772    $min += 0;
773    $max += 0;
774    for(my $i = $min; $i <= $max; $i++) {
775        $existsid{$i + 0} or do {
776            $base->log(LA_DEBUG, "Next %s found: %d", $field, $i);
777            return $i;
778        };
779    }
780    return;
781}
782
783=head2 text_dump ($handle, $config, $base)
784
785Dump object into C<$handle>
786
787=cut
788
789sub text_dump {
790    my ($self, $handle, $config, $base) = @_;
791    print $handle $self->dump($config, $base);
792    return 1;
793}
794
795=head2 dump
796
797Return dump for tihs object
798
799=cut
800
801sub dump {
802    my ($self, $config, $base) = @_;
803
804    my $otype = $self->type;
805    $base ||= $self->base;
806    my $dump;
807    if (ref $self) {
808        $dump .= sprintf "# base %s: object %s/%s\n",
809            $base->label, $self->type, $self->id;
810    }
811    $dump .= sprintf "# %s\n", scalar(localtime);
812
813    foreach my $attr (sort { $a cmp $b } $base->list_canonical_fields($otype,
814        $config->{only_rw} ? 'rw' : 'r')) {
815        my $oattr = ref $self ? $self->attribute($attr) : $base->attribute($otype, $attr);
816        if ($oattr->hidden) { next; }
817        if (ref $self) {
818            my $val = $self->get_c_field($attr);
819            if ($val || $config->{empty_attr}) {
820                if (my @allowed = $base->obj_attr_allowed_values($otype, $attr)) {
821                    $dump .= sprintf("# %s must be%s: %s\n",
822                        $attr,
823                        ($oattr->mandatory ? '' : ' empty or either'),
824                        join(', ', @allowed)
825                    );
826                }
827                my @vals = ref $val ? @{ $val } : $val;
828                foreach (@vals) {
829                    $_ ||= '';
830                    s/\r?\n/\\n/g;
831                    $dump .= sprintf("%s%s:%s\n", 
832                        $oattr->ro ? '# (ro) ' : '',
833                        $attr, $_ ? " $_" : '');
834                }
835            }
836        } else {
837            if (my @allowed = $base->obj_attr_allowed_values($otype, $attr)) {
838                $dump .= sprintf("# %s must be empty or either: %s\n",
839                    $attr,
840                    join(', ', @allowed)
841                );
842            }
843            $dump .= sprintf("%s%s: %s\n", 
844                $oattr->ro ? '# (ro) ' : '',
845                $attr, '');
846        }
847    }
848    return $dump;
849}
850
851=head2 ReportChange($changetype, $message, @args)
852
853Possible per database way to log changes
854
855=cut
856
857sub ReportChange {
858    my ($self, $changetype, $message, @args) = @_;
859
860    $self->base->ReportChange(
861        $self->type,
862        $self->id,
863        $self->Iid,
864        $changetype, $message, @args
865    )
866}
867
8681;
869
870__END__
871
872
873=head1 SEE ALSO
874
875L<LATMOS::Accounts::Bases>
876
877=head1 AUTHOR
878
879Thauvin Olivier, E<lt>olivier.thauvin.ipsl.fr@localdomainE<gt>
880
881=head1 COPYRIGHT AND LICENSE
882
883Copyright (C) 2009 by Thauvin Olivier
884
885This library is free software; you can redistribute it and/or modify
886it under the same terms as Perl itself, either Perl version 5.10.0 or,
887at your option, any later version of Perl 5 you may have available.
888
889=cut
Note: See TracBrowser for help on using the repository browser.