# Copyright (c) 1998-2000 Graham Barr . All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. package Net::LDAP::Schema; use strict; use vars qw($VERSION); $VERSION = "0.10"; # # Get schema from the server (or read from LDIF) and parse it into # data structure # sub new { my $self = shift; my $type = ref($self) || $self; my $schema = bless {}, $type; return $schema unless @_; return $schema->parse( shift ) ? $schema : undef; } sub _error { my $self = shift; $self->{error} = shift; return; } sub parse { my $schema = shift; my $arg = shift; unless ($arg) { $schema->{error} = "Bad argument"; return undef; } %$schema = (); my $entry; if( ref $arg ) { if (UNIVERSAL::isa($arg, 'Net::LDAP::Entry')) { $entry = $arg; } elsif (UNIVERSAL::isa($arg, 'Net::LDAP::Search')) { unless ($entry = $arg->entry) { $schema->{error} = 'Bad Argument'; return undef; } } else { $schema->{error} = 'Bad Argument'; return undef; } } elsif( -f $arg ) { require Net::LDAP::LDIF; my $ldif = Net::LDAP::LDIF->new( $arg, "r" ); $entry = $ldif->read(); unless( $entry ) { $schema->{error} = "Cannot parse LDIF from file [$arg]"; return undef; } } else { $schema->{error} = "Can't load schema from [$arg]: $!"; return undef; } eval { local $SIG{__DIE__} = sub {}; _parse_schema( $schema, $entry ); }; if ($@) { $schema->{error} = $@; return undef; } return $schema; } # # Dump as LDIF # # XXX - We should really dump from the internal structure. That way we can # have methods to modify the schema and write a new one -- GMB sub dump { my $self = shift; my $fh = @_ ? shift : \*STDOUT; my $entry = $self->{'entry'} or return; require Net::LDAP::LDIF; Net::LDAP::LDIF->new($fh,"w", wrap => 0)->write($entry); 1; } # # Given another Net::LDAP::Schema, merge the contents together. # XXX - todo # sub merge { my $self = shift; my $new = shift; # Go through structure of 'new', copying code to $self. Take some # parameters describing what to do in the event of a clash. } # # The names of all the attributes. # Or all atts in (one or more) objectclass(es). # sub attributes { my $self = shift; my @oc = @_; my %res; if( @oc ) { @res{ $self->must( @oc ) } = (); @res{ $self->may( @oc ) } = (); } else { @res{ @{ $self->{at} } } = () if $self->{at}; } return wantarray ? (keys %res) : [keys %res]; } # The names of all the object classes sub objectclasses { my $self = shift; my $res = $self->{oc}; return wantarray ? @$res : $res; } # Return all syntaxes sub syntaxes { my $self = shift; my $res = $self->{syn}; return wantarray ? @$res : $res; } # The names of all the matchingrules sub matchingrules { my $self = shift; my $res = $self->{mr}; return wantarray ? @$res : $res; } # The names of all the matchingruleuse sub matchingruleuse { my $self = shift; my $res = $self->{mru}; return wantarray ? @$res : $res; } # The names of all the ditstructurerules sub ditstructurerules { my $self = shift; my $res = $self->{dts}; return wantarray ? @$res : $res; } # The names of all the ditcontentrules sub ditcontentrules { my $self = shift; my $res = $self->{dtc}; return wantarray ? @$res : $res; } # The names of all the nameforms sub nameforms { my $self = shift; my $res = $self->{nfm}; return wantarray ? @$res : $res; } sub superclass { my $self = shift; my $oc = shift; my $oid = $self->is_objectclass( $oc ); return scalar _error($self, "Not an objectClass") unless $oid; my $res = $self->{oid}->{$oid}->{sup}; return scalar _error($self, "No superclass") unless $res; return wantarray ? @$res : $res; } sub must { my $self = shift; $self->_must_or_may( "must", @_ ); } sub may { my $self = shift; $self->_must_or_may( "may", @_ ); } # # Return must or may attributes for this OC. [As array or array ref] # return empty array/undef on error # sub _must_or_may { my $self = shift; my $must_or_may = shift; my @oc = @_ or return; # # If called with an entry, get the OC names and continue # if( UNIVERSAL::isa( $oc[0], "Net::LDAP::Entry" ) ) { my $entry = $oc[0]; @oc = $entry->get_value( "objectclass" ) or return; } my %res; # Use hash to get uniqueness foreach my $oc ( @oc ) { my $oid = $self->is_objectclass( $oc ); if( $oid ) { my $res = $self->{oid}->{$oid}->{$must_or_may} or next; @res{ @$res } = (); # Add in, getting uniqueness } } return wantarray ? (keys %res) : [ keys %res ]; } # # Return the value of an item, e.g. 'desc'. If item is array ref and we # are called from array context, return an array, else scalar # sub item { my $self = shift; my $arg = shift; my $item_name = shift; # May be undef. If so all are returned my @oid = $self->name2oid( $arg ); return _error($self, @oid ? "Non-unique name" : "Unknown name") unless @oid == 1; my $item_ref = $self->{oid}->{$oid[0]} or return _error($self, "Unknown OID"); my $value = $item_ref->{$item_name} or return _error($self, "No such property"); delete $self->{error}; if( ref $value eq "ARRAY" && wantarray ) { return @$value; } else { return $value; } } # # Return a list of items for a particular name or oid # # BUG:Dumps internal representation rather than real info. E.g. shows # the alias/name distinction we create and the 'type' field. # sub items { my $self = shift; my $arg = shift; my @oid = $self->name2oid( $arg ); return _error($self, @oid ? "Non-unique name" : "Unknown name") unless @oid == 1; my $item_ref = $self->{oid}->{$oid[0]} or return _error($self, "Unknown OID"); delete $self->{error}; return wantarray ? (keys %$item_ref) : [keys %$item_ref]; } # # Given a name, alias or oid, return oid or undef. Undef if not known. # sub name2oid { my $self = shift; my $name = lc shift; return _error($self, "Bad name") unless defined($name) && length($name); return $name if exists $self->{oid}->{$name}; # Already an oid my $oid = $self->{name}->{$name} || $self->{aliases}->{$name} or return _error($self, "Unknown name"); return (wantarray && ref $oid) ? @$oid : $oid; } # # Given an an OID (not a name) return the canonical name. Undef if not # an OID # sub oid2name { my $self = shift; my $oid = shift; return _error($self, "Bad OID") unless $oid; return _error($self, "Unknown OID") unless $self->{oid}->{$oid}; delete $self->{error}; return $self->{oid}->{$oid}->{name}; } # # Given name or oid, return oid or undef if not of appropriate type # sub is_attribute { my $self = shift; return $self->_is_type( "at", @_ ); } sub is_objectclass { my $self = shift; return $self->_is_type( "oc", @_ ); } sub is_syntax { my $self = shift; return $self->_is_type( "syn", @_ ); } sub is_matchingrule { my $self = shift; return $self->_is_type( "mr", @_ ); } sub is_matchingruleuse { my $self = shift; return $self->_is_type( "mru", @_ ); } sub is_ditstructurerule { my $self = shift; return $self->_is_type( "dts", @_ ); } sub is_ditcontentrule { my $self = shift; return $self->_is_type( "dtc", @_ ); } sub is_nameform { my $self = shift; return $self->_is_type( "nfm", @_ ); } # -------------------------------------------------- # Internal functions # -------------------------------------------------- # # Given a type and a name_or_oid, return true (the oid) if the name_or_oid # is of the appropriate type. Else return undef. # sub _is_type { my ($self, $type, $name) = @_; foreach my $oid ($self->name2oid( $name )) { my $hash = $self->{oid}->{$oid} or next; return $oid if $hash->{type} eq $type; } undef; } # # XXX - TODO - move long comments to POD and write up interface # # Data structure is: # # $schema (hash ref) # # The {oid} piece here is a little redundant since we control the other # top-level members. We promote the first listed name to be 'canonical' and # also make up a name for syntaxes (from the description). Thus we always # have a unique name. This avoids a lot of checking in the access routines. # # ->{oid}->{$oid}->{ # name => $canonical_name, (created for syn) # aliases => list of non. canon names # type => at/oc/syn # desc => description # must => list of can. names of mand. atts [if OC] # may => list of can. names of opt. atts [if OC] # syntax => can. name of syntax [if AT] # ... etc per oid details # # These next items are optimisations, to avoid always searching the OID # lists. Could be removed in theory. # # ->{at} = [ list of canonical names of attributes ] # ->{oc} = [ list of can. names of objectclasses ] # ->{syn} = [ list of can. names of syntaxes (we make names from descripts) ] # ->{mr} = [ list of can. names of matchingrules ] # ->{mru} = [ list of can. names of matchingruleuse ] # ->{dts} = [ list of can. names of ditstructurerules ] # ->{dtc} = [ list of can. names of ditcontentrules ] # ->{nfm} = [ list of can. names of nameForms ] # # This is used to optimise name => oid lookups (to avoid searching). # This could be removed or made into a cache to reduce memory usage. # The names include any aliases. # # ->{name}->{ $lower_case_name } = $oid # # # These items have no following arguments # my %flags = map { ($_,1) } qw( single-value obsolete collective no-user-modification abstract structural auxiliary ); # # These items can have lists arguments # (name can too, but we treat it special) # my %listops = map { ($_,1) } qw(must may sup); # # Map schema attribute names to internal names # my %type2attr = ( at => "attributetypes", oc => "objectclasses", syn => "ldapsyntaxes", mr => "matchingrules", mru => "matchingruleuse", dts => "ditstructurerules", dtc => "ditcontentrules", nfm => "nameforms", ); # # Return ref to hash containing schema data - undef on failure # sub _parse_schema { my $schema = shift; my $entry = shift; return undef unless defined($entry); keys %type2attr; # reset iterator while(my($type,$attr) = each %type2attr) { my $vals = $entry->get_value($attr, asref => 1); my @names; $schema->{$type} = \@names; # Save reference to list of names next unless $vals; # Just leave empty ref if nothing foreach my $val (@$vals) { # # The following statement takes care of defined attributes # that have no data associated with them. # next if $val eq ''; # # We assume that each value can be turned into an OID, a canonical # name and a 'schema_entry' which is a hash ref containing the items # present in the value. # my %schema_entry = ( type => $type, aliases => [] ); my @tokens; pos($val) = 0; push @tokens, $+ while $val =~ /\G\s*(?: ([()]) | ([^"'\s()]+) | "([^"]*)" | '([^']*)' )\s*/xcg; die "Cannot parse [$val] ",substr($val,pos($val)) unless @tokens and pos($val) == length($val); # remove () from start/end shift @tokens if $tokens[0] eq '('; pop @tokens if $tokens[-1] eq ')'; # The first token is the OID my $oid = $schema_entry{oid} = shift @tokens; while(@tokens) { my $tag = lc shift @tokens; if (exists $flags{$tag}) { $schema_entry{$tag} = 1; } elsif (@tokens) { if (($schema_entry{$tag} = shift @tokens) eq '(') { my @arr; $schema_entry{$tag} = \@arr; while(1) { my $tmp = shift @tokens; last if $tmp eq ')'; push @arr,$tmp unless $tmp eq '$'; # Drop of end of list ? die "Cannot parse [$val]" unless @tokens; } } # Ensure items that can be lists are stored as array refs $schema_entry{$tag} = [ $schema_entry{$tag} ] if exists $listops{$tag} and !ref $schema_entry{$tag}; } else { die "Cannot parse [$val]"; } } # # Extract the maximum length of a syntax # if ( exists $schema_entry{syntax}) { $schema_entry{syntax} =~ s/{(\d+)}// and $schema_entry{max_length} = $1; } # # Force a name if we don't have one # if (!exists $schema_entry{name}) { if (exists $schema_entry{desc}) { ($schema_entry{name} = $schema_entry{desc}) =~ s/\s+//g } else { $schema_entry{name} = "$type:$schema_entry{oid}" } } # # If we have multiple names, make the name be the first and demote the rest to aliases # $schema_entry{name} = shift @{$schema_entry{aliases} = $schema_entry{name}} if ref $schema_entry{name}; # # In the schema we store: # # 1 - The schema entry referenced by OID # 2 - a list of canonical names of each type # 3 - a (lower-cased) canonical name -> OID map # 4 - a (lower-cased) alias -> OID map # $schema->{oid}->{$oid} = \%schema_entry; my $uc_name = uc $schema_entry{name}; push @names, $uc_name; foreach my $name ( @{$schema_entry{aliases}}, $uc_name ) { if (exists $schema->{name}{lc $name}) { $schema->{name}{lc $name} = [ $schema->{name}{lc $name} ] unless ref $schema->{name}{lc $name}; push @{$schema->{name}{lc $name}}, $oid; } else { $schema->{name}{lc $name} = $oid; } } } } $schema->{entry} = $entry; return $schema; } # # Get the syntax of an attribute # sub syntax { my $self = shift; my $attr = shift; my $oid = $self->is_attribute( $attr ) or return undef; my $syntax = $self->{oid}->{$oid}->{syntax}; unless( $syntax ) { my @sup = @{$self->{oid}->{$oid}->{sup}}; $syntax = $self->syntax( $sup[0] ); } return $syntax; } # # Given an OID or name (or alias), return the canonical name # sub name { my $self = shift; my $arg = shift; my @oid = $self->name2oid( $arg ); return undef unless @oid == 1; return $self->oid2name( $oid[0] ); } sub error { $_[0]->{error}; } # # Return base entry # sub entry { $_[0]->{entry}; } 1;