631 lines
14 KiB
Perl
631 lines
14 KiB
Perl
# Copyright (c) 1998-2000 Graham Barr <gbarr@pobox.com>. 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;
|