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

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

review the way chanages are report, make it more general

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