package LATMOS::Accounts::Bases::Objects; use 5.010000; use strict; use warnings; use overload '""' => 'stringify'; use LATMOS::Accounts::Log; use LATMOS::Accounts::Bases::Attributes; use LATMOS::Accounts::Utils; use Crypt::Cracklib; our $VERSION = (q$Rev: 2072 $ =~ /^Rev: (\d+) /)[0]; =head1 NAME LATMOS::Accounts::Bases::Objects - Base class for account objects =head1 SYNOPSIS use LATMOS::Accounts::Bases::Objects; LATMOS::Accounts::Bases::Objects->new($base, $type, $id); =head1 DESCRIPTION =head1 FUNCTIONS =cut =head2 is_supported If exists, must return true or false if the object is supported or not =cut =head2 list($base) List object supported by this module existing in base $base Must be provide by object class sub list { my ($class, $base) = @_; } =cut =head2 listReal($base) List object supported by this module existing in base $base Can be override by base driver. The result must exclude specials object such alias. =cut sub listReal { my ($class, $base) = @_; $class->list($base); } =head2 list_from_rev($base, $rev) List objects create or modified after base revision C<$rev>. =cut =head2 new($base, $id) Create a new object having $id as uid. =cut sub new { my ($class, $base, $id, @args) = @_; # So can be call as $class->SUPER::new() bless { _base => $base, _type => lc(($class =~ m/::([^:]*)$/)[0]), _id => $id, }, $class; } =head2 _create($class, $base, $id, %data) Must create a new object in database. Is called if underling base does not override create_object sub _create( my ($class, $base, $id, %data) } =cut =head2 type Return the type of the object =cut sub type { my ($self) = @_; if (ref $self) { return $self->{_type} } else { return lc(($self =~ /::([^:]+)$/)[0]); } } =head2 base Return the base handle for this object. =cut sub base { return $_[0]->{_base} } =head2 id Must return the unique identifier for this object =cut sub id { my ($self) = @_; $self->{_id} } =head2 AclID Return object for acl check =cut sub AclID { $_[0]->id } =head2 Iid Return internal id if different from Id =cut sub Iid { my ($self) = @_; $self->id } =head2 stringify Display object as a string =cut sub stringify { my ($self) = @_; return $self->id } =head2 list_canonical_fields($for) Object shortcut to get the list of field supported by the object. =cut sub list_canonical_fields { my ($self, $for) = @_; $for ||= 'rw'; $self->_canonical_fields($for); } =head2 attribute ($attribute) Return L object for C<$attribute> =cut sub attribute { my ($self, $attribute) = @_; my $attrinfo; if (! ref $attribute) { $attrinfo = $self->_get_attr_schema( $self->base)->{$attribute} or return; $attrinfo->{name} = $attribute; } else { $attrinfo = $attribute; } return LATMOS::Accounts::Bases::Attributes->new( $attrinfo, $self, ); } sub _canonical_fields { my ($class, $base, $for) = @_; $for ||= 'rw'; my $info = $base->_get_attr_schema($class->type); my @attrs = map { $base->attribute($class->type, $_) } keys %{$info || {}}; @attrs = grep { ! $_->ro } @attrs if($for =~ /w/); @attrs = grep { $_->readable } @attrs if($for =~ /r/); @attrs = grep { !$_->hidden } @attrs unless($for =~ /a/); map { $_->name } @attrs; } =head2 GetOtypeDef This function is called to provide sub object definition. Must be overwritten per object class when need. Must return a hashref listing each sub object type and their related key atribute: return { addresses => 'user', } =cut sub GetOtypeDef { my ($class) = @_; return; } =head2 get_field($field) Return the value for $field, must be provide by data base. sub get_field { my ($self, $field) } =cut =head2 get_c_field($cfield) Return the value for canonical field $cfield. Call driver specific get_field() =cut sub get_c_field { my ($self, $cfield) = @_; $self->base->check_acl($self, $cfield, 'r') or do { $self->base->log(LA_DEBUG, "Permission denied to get %s/%s", $self->id, $cfield ); return; }; return $self->_get_c_field($cfield); } =head2 get_attributes($attr) Like get_c_field but always return an array =cut sub get_attributes { my ($self, $cfield) = @_; my $res = $self->get_c_field($cfield); if ($res) { return(ref $res ? @{$res} : $res); } else { return; } } sub _get_attributes { my ($self, $cfield) = @_; my $res = $self->_get_c_field($cfield); if ($res) { return(ref $res ? @{$res} : ($res)); } else { return; } } sub _get_c_field { my ($self, $cfield) = @_; my $attribute = $self->attribute($cfield) or do { $self->base->log(LA_WARN, "Unknow attribute $cfield"); return; }; $attribute->readable or do { $self->base->log(LA_WARN, "Attribute $cfield is not readable"); return; }; return $attribute->get; } =head2 GetAttributeValue($cfield) Return the value to exposed to other base =cut sub GetAttributeValue { my ($self, $cfield) = @_; return $self->get_c_field($cfield); } =head2 queryformat ($fmt) Return formated string according C<$fmt> =cut sub queryformat { my ($self, $fmt) = @_; $fmt ||= ''; # avoid undef $fmt =~ s/\\n/\n/g; $fmt =~ s/\\t/\t/g; my $old; do { $old = $fmt; $fmt =~ s&(?:%\{([\?!]+)?([^:}%]*)(?::([^}%]*))?\})& my $op = $1; my $attr = $2; my $val = ''; my $modifier = $3 || ''; if ($attr =~ /^(\w+)\((.*)\)$/) { $val = $self->base->QFunc($1, $2); } else { $val = $self->get_c_field($2); } my $res = ''; $val = '' unless( defined( $val ) ); if ($op) { if ($op eq '?') { $res = $val ? $3 : ''; } elsif ($op eq '?!') { $res = $val ? '' : $3; } if ($res =~ /^(\w+)\((.*)\)$/) { $res = $self->base->QFunc($1, $2); } } else { $res = $val; foreach (split('\|' , $modifier)) { /upper/ and do { $res = uc($res); next; }; /ucfirst/ and do { $res = ucfirst($res); next; }; /lower/ and do { $res = lc($res); next; }; /lcfirst/ and do { $res = lcfirst($res); next; }; /ascii/ and do { $res = LATMOS::Accounts::Utils::to_ascii($res); next; }; /substr\s+(\d+)\s+(\d+)?/ and do { $res = substr($res, $1, $2); next; }; $res = sprintf('%' . ($modifier || 's'), ref $val ? join(',', @$val) : (defined($val) ? $val : '')) } } $res &egx; } while($old ne $fmt); $fmt; } =head2 set_fields(%data) Set values for this object. %data is a list or peer field => values. sub set_fields { my ($self, %data) = @_; } =cut =head2 checkValues ($base, $obj, %attributes) Allow to pre-check values when object are modified or created C<$obj> is either the new id at object creation or the object itself on modification. =cut sub checkValues { my ($class, $base, $obj, %attributes) = @_; return 1; } =head2 check_allowed_values ($attr, $values) Check if value C<$values> is allowed for attributes C<$attr> =cut sub check_allowed_values { my ($self, $attr, $values) = @_; $self->base->check_allowed_values($self->type, $attr, $values); } =head2 attr_allow_values ($attr) Return allowed for attribute C<$attr> =cut sub attr_allow_values { my ($self, $attr) = @_; return $self->base->obj_attr_allowed_values( $self->type, $attr, ); } =head2 set_c_fields(%data) Set values for this object. %data is a list or peer canonical field => values. Fields names are translated. =cut sub set_c_fields { my ($self, %cdata) = @_; foreach my $cfield (keys %cdata) { $self->base->check_acl($self, $cfield, 'w') or do { $self->base->log(LA_ERR, "Cannot modified %s/%s: %s", $self->type, $self->id, "permission denied"); return; }; } foreach my $cfield (keys %cdata) { $self->check_allowed_values($cfield, $cdata{$cfield}) or do { $self->base->log(LA_ERR, "Cannot modified %s/%s: %s", $self->type, $self->id, "non authorized value"); return; }; } $self->_set_c_fields(%cdata); } sub _set_c_fields { my ($self, %cdata) = @_; my %data; my $res = 0; foreach my $cfield (keys %cdata) { my $attribute = $self->attribute($cfield) or do { $self->base->log(LA_ERR, "Cannot set unsupported attribute %s to %s (%s)", $cfield, $self->id, $self->type ); return; }; $attribute->ro and do { $self->base->log(LA_ERR, "Cannot set read-only attribute %s to %s (%s)", $cfield, $self->id, $self->type ); return; }; if (!$attribute->checkinput($cdata{$cfield})) { $self->base->log(LA_ERR, "Value for attribute %s to %s (%s) does not match requirements", $cfield, $self->id, $self->type ); return; }; } if (!$self->checkValues($self->base, $self, %cdata)) { my $last = LATMOS::Accounts::Log::lastmessage(LA_ERR); $self->base->log(LA_ERR, "Cannot update %s (%s): wrong value%s", $self->id, $self->type, ($last ? ": $last" : $last) ); return; } my %updated = (); foreach my $cfield (keys %cdata) { my $attribute = $self->attribute($cfield) or do { $self->base->log(LA_ERR, "Cannot set unsupported attribute %s to %s (%s)", $cfield, $self->id, $self->type ); return; }; if ($attribute->set($cdata{$cfield})) { $updated{$cfield} = $attribute->monitored; } } if (keys %updated) { $self->PostSetAttribute() or do { $self->base->log(LA_ERR, "PostSetAttribute failed when updating %s/%s", $self->type, $self->id); return; }; $self->ReportChange('Update', 'Attributes %s have been updated', join(', ', sort keys %updated)); foreach (sort keys %updated) { $self->ReportChange('Attributes', '%s set to %s', $_, (ref $cdata{$_} ? join(', ', sort @{ $cdata{$_} }) : $cdata{$_}) || '(none)') if ($updated{$_}); } } return scalar(keys %updated); } =head2 PostSetAttribute This function is call to compute data when object is modify. =cut sub PostSetAttribute { my ($self) = @_; return 1; } =head2 addAttributeValue($attribute, $value) Add a value to a multivalue attributes =cut sub _addAttributeValue { my ($self, $attribute, @values) = @_; my @oldvalues = grep { $_ } $self->_get_attributes($attribute); $self->_set_c_fields($attribute => [ @oldvalues, @values ]); } sub addAttributeValue { my ($self, $attribute, @values) = @_; my @oldvalues = grep { $_ } $self->_get_attributes($attribute); $self->set_c_fields($attribute => [ @oldvalues, @values ]); } =head2 delAttributeValue($attribute, $value) Remove a value to a multivalue attributes =cut sub _delAttributeValue { my ($self, $attribute, @values) = @_; my @oldvalues = grep { $_ } $self->_get_attributes($attribute); foreach my $value (@values) { @oldvalues = grep { $_ ne $value } @oldvalues; } $self->_set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef ); } sub delAttributeValue { my ($self, $attribute, @values) = @_; my @oldvalues = grep { $_ } $self->_get_attributes($attribute); foreach my $value (@values) { @oldvalues = grep { $_ ne $value } @oldvalues; } $self->set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef ); } =head2 set_password($password) Set the password into the database, $password is the clear version of the password. This function store it into userPassword canonical field if supported using crypt unix and md5 algorythm (crypt md5), the salt is 8 random caracters. The base driver should override it if another encryption is need. =cut sub set_password { my ($self, $clear_pass) = @_; if ($self->base->check_acl($self, 'userPassword', 'w')) { if ($self->_set_password($clear_pass)) { $self->ReportChange('Password', 'user password has changed'); return 1; } else { return; } } else { $self->base->log(LA_ERROR, "Permission denied for %s to change its password", $self->id); return; } } sub _set_password { my ($self, $clear_pass) = @_; if (my $attribute = $self->base->attribute($self->type, 'userPassword')) { my $res = $self->set_fields($attribute->iname, $self->base->passCrypt($clear_pass)); $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id) if($res); return $res; } else { $self->base->log(LA_WARN, "Cannot set password: userPassword attributes is unsupported"); } } =head2 check_password ($password) Check given password is secure using L =cut sub check_password { my ( $self, $password ) = @_; my $dictionary = $self->base->config('cracklib_dictionnary'); if ($password !~ /^[[:ascii:]]*$/) { return "the password must contains ascii characters only"; } return fascist_check($password, $dictionary); } =head2 InjectCryptPasswd($cryptpasswd) Inject a password encrypted using standard UNIX method. =cut sub InjectCryptPasswd { my ($self, $cryptpasswd) = @_; if ($self->can('_InjectCryptPasswd')) { return $self->_InjectCryptPasswd($cryptpasswd); } else { $self->base->log(LA_ERR, 'Injecting unix crypt password is not supported'); return; } } =head2 search ($base, @filter) Search object matching C<@filter> =cut sub search { my ($class, $base, @filter) = @_; my @results; my %parsed_filter; while (my $item = shift(@filter)) { # attr=foo => no extra white space ! # \W is false, it is possible to have two char my ($attr, $mode, $val) = $item =~ /^(\w+)(?:(\W)(.+))?$/ or next; if (!$mode) { $mode = '~'; $val = shift(@filter); } push( @{$parsed_filter{$attr}}, { attr => $attr, mode => $mode, val => $val, } ); } foreach my $id ($base->list_objects($class->type)) { my $obj = $base->get_object($class->type, $id); my $match = 1; foreach my $field (keys %parsed_filter) { $base->attribute($class->type, $field) or la_log(LA_WARN, "Unsupported attribute %s", $field); my $tmatch = 0; foreach (@{$parsed_filter{$field}}) { my $value = $_->{val}; my $fval = $obj->_get_c_field($field) || ''; if ($value eq '*') { if ($fval ne '') { $tmatch = 1; last; } } elsif ($value eq '!') { if ($fval eq '') { $match = 1; last; } } elsif ($_->{mode} eq '=') { if ($fval eq $value) { $tmatch = 1; last; } } elsif($_->{mode} eq '~') { if ($fval =~ m/\Q$value\E/i) { $tmatch = 1; last; } } } $match = 0 unless($tmatch); } push(@results, $id) if($match); } @results; } =head2 attributes_summary ($base, $attribute) Return list of values existing in base for C<$attribute> =cut sub attributes_summary { my ($class, $base, $attribute) = @_; my $attr = $base->attribute($class->type, $attribute) or do { $base->log(LA_WARN, "Cannot instantiate %s attribute for class %s", $attribute, $class->type); return; }; if (!$attr->readable) { $base->log(LA_WARN, l('Attribute %s is not readable', $attribute)); return; } if (!$base->check_acl($class->type, $attribute, 'r')) { $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute)); return; } my %values; foreach my $id ($base->list_objects($class->type)) { my $obj = $base->get_object($class->type, $id); my $value = $obj->_get_c_field($attribute); if ($value) { if (ref $value) { foreach (@$value) { $values{$_} = 1; } } else { $values{$value} = 1; } } } return sort(keys %values); } =head2 attributes_summary_by_object ($base, $attribute) Return list of peer object <=> values =cut sub attributes_summary_by_object { my ($class, $base, $attribute) = @_; my $attr = $base->attribute($class->type, $attribute) or do { $base->log(LA_WARN, "Cannot instantiate %s attribute for class %s", $attribute, $class->type); return; }; if (!$attr->readable) { $base->log(LA_WARN, l('Attribute %s is not readable', $attribute)); return; } if (!$base->check_acl($class->type, $attribute, 'r')) { $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute)); return; } my %values; foreach my $id ($base->list_objects($class->type)) { my $obj = $base->get_object($class->type, $id); my $value = $obj->_get_c_field($attribute); if ($value) { if (ref $value) { foreach (@$value) { push(@{ $values{ $id } }, $_); } } else { push(@{ $values{ $id } }, $value); } } } return %values; } =head2 find_next_numeric_id ($base, $field, $min, $max) Find next free uniq id for attribute C<$field> =cut sub find_next_numeric_id { my ($class, $base, $field, $min, $max) = @_; $base->attribute($class->type, $field) or return; $min ||= $field eq 'uidNumber' ? 500 : $field eq 'gidNumber' ? 500 : 1; $max ||= 65635; $base->log(LA_DEBUG, "Trying to find %s in range %d - %d", $field, $min, $max); my %existsid; $base->temp_switch_unexported(sub { foreach ($base->list_objects($class->type)) { my $obj = $base->get_object($class->type, $_) or next; my $id = $obj->_get_c_field($field) or next; $existsid{$id + 0} = 1; } }, 1); $min += 0; $max += 0; for(my $i = $min; $i <= $max; $i++) { $existsid{$i + 0} or do { $base->log(LA_DEBUG, "Next %s found: %d", $field, $i); return $i; }; } return; } =head2 ListChildObjects List dependant objects. =cut sub ListChildObjects { my ( $self ) = @_; return; } =head2 DataDump($config) Return a structure about the object =cut sub DataDump { my ($self, $config, $base) = @_; # { # base => # base latmoslocal: object address/thauvin-GY # otype => 'otype', # id => 'name', # roAttrs => [] # Attrs => { # 'Attr' => [], # } # subObjs => { # 'otype' => [], # } # } $config->{level} ||= 0; my $otype = $self->type; $base ||= $self->base; my $dump = { otype => $otype, id => ref $self ? $self->id : 'N/A', }; if (ref $self) { $dump->{base} = $base->label; } my %roAttrs = (); foreach my $attr (sort { $a cmp $b } $base->list_canonical_fields($otype, $config->{only_rw} ? 'rw' : 'r')) { my $oattr = ref $self ? $self->attribute($attr) : $base->attribute($otype, $attr); next if ($oattr->hidden); if (ref $self) { my $val = $self->get_c_field($attr); if ($val || $config->{empty_attr}) { my @vals = ref $val ? @{ $val } : $val; $dump->{Attrs}{$attr} = \@vals; $roAttrs{ $attr } = 1 if ($oattr->ro); } } else { $dump->{Attrs}{$attr} = undef; $roAttrs{ $attr } = 1 if ($oattr->ro); } $dump->{roAttrs} = [ sort keys %roAttrs ] unless($config->{noSchema}); } if ($config->{cb}) { $config->{cb}->($config, $dump); } my $SubOtype = undef; if (@{ $config->{SubOtype} || []}) { $SubOtype = { map { $_ => 1 } @{ $config->{SubOtype} } }; } if (ref $self && $config->{recur}) { my %subobj = $self->ListChildObjects; foreach my $otype (sort keys %subobj) { if ($SubOtype) { $SubOtype->{$otype} or next; } foreach my $oname (sort @{ $subobj{$otype} }) { my $obj = $self->base->get_object($otype, $oname) or next; push(@{ $dump->{subObjs}{$otype} }, $obj->DataDump({ %{$config || {}}, recur => $config->{recur}, level => $config->{level} + 2 })); } } } return $dump; } =head2 text_dump ($handle, $config, $base) Dump object into C<$handle> =cut sub text_dump { my ($self, $handle, $config, $base) = @_; print $handle $self->dump($config, $base); return 1; } =head2 dump Return dump for this object =cut sub dump { my ($self, $InitConfig, $base) = @_; $InitConfig->{level} ||= 0; $base ||= $self->base; my $dump = ''; $InitConfig->{cb} = sub { my ( $config, $Dump ) = @_; if ($config->{level}) { $dump .= "\n"; } if (ref $self) { $dump .= sprintf "%s# base %s: object %s/%s\n", ' ' x $config->{level}, $Dump->{base}, $Dump->{otype}, $Dump->{id}; } $dump .= sprintf( "%s# %s\n", ' ' x $config->{level}, scalar(localtime) ); my %roAttrs = map { $_ => 1 } @{ $Dump->{roAttrs} || [] }; foreach my $attr (sort { $a cmp $b } sort keys %{ $Dump->{Attrs} || {} }) { my $val = $Dump->{Attrs}{$attr}; my $oattr = $base->attribute($Dump->{otype}, $attr); if ($val) { if (my @allowed = $base->obj_attr_allowed_values($Dump->{otype}, $attr)) { $dump .= sprintf("%s# %s must be%s: %s\n", ' ' x $config->{level}, $attr, ($oattr->mandatory ? '' : ' empty or either'), join(', ', @allowed) ); } foreach (@$val) { $_ ||= ''; s/\r?\n/\\n/g; $dump .= sprintf("%s%s%s%s:%s\n", ' ' x $config->{level}, $roAttrs{$attr} ? '# (ro) ' : '', $config->{level} ? $Dump->{otype} . '[' . $Dump->{id} . '].' : '', $attr, $_ ? " $_" : ''); } } elsif ( $config->{empty_attr} || ! ref $self) { if (my @allowed = $base->obj_attr_allowed_values($Dump->{otype}, $attr)) { $dump .= sprintf("%s# %s must be%s: %s\n", ' ' x $config->{level}, $attr, ($oattr->mandatory ? '' : ' empty or either'), join(', ', @allowed) ); } $dump .= sprintf("%s%s%s%s:\n", ' ' x $config->{level}, $roAttrs{$attr} ? '# (ro) ' : '', $config->{level} ? $Dump->{otype} . '[' . $Dump->{id} . '].' : '', $attr); } } }; $self->DataDump($InitConfig, $base); return $dump; } =head2 ReportChange($changetype, $message, @args) Possible per database way to log changes =cut sub ReportChange { my ($self, $changetype, $message, @args) = @_; $self->base->ReportChange( $self->type, $self->id, $self->Iid, $changetype, $message, @args ) } 1; __END__ =head1 SEE ALSO L =head1 AUTHOR Thauvin Olivier, Eolivier.thauvin.ipsl.fr@localdomainE =head1 COPYRIGHT AND LICENSE Copyright (C) 2009 by Thauvin Olivier This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.0 or, at your option, any later version of Perl 5 you may have available. =cut