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

Last change on this file since 1329 was 1329, checked in by nanardon, 9 years ago

Merge branch 'jquery'

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