You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

592 lines
24 KiB

require 'ldap/server/syntax'
require 'ldap/server/result'
module LDAP
class Server
# This object represents an LDAP schema: that is, a collection of
# objectclasses and attributetypes. Methods are provided for loading
# the schema (from a string or a disk file), and validating an av-hash
# against it.
class Schema
SUBSCHEMA_ENTRY_ATTR = 'cn'
SUBSCHEMA_ENTRY_VALUE = 'Subschema'
def initialize
@attrtypes = {} # name/alias/oid => AttributeType instance
@objectclasses = {} # name/alias/oid => ObjectClass instance
@subschema_cache = nil
end
# return the DN of the subschema subentry
def subschema_dn
"#{SUBSCHEMA_ENTRY_ATTR}=#{SUBSCHEMA_ENTRY_VALUE}"
end
# Return an av hash object giving the subschema subentry. This is cached, so
# call Schema#changed if it needs to be rebuilt
def subschema_subentry
@subschema_cache ||= {
'objectClass' => ['top','subschema','extensibleObject'],
SUBSCHEMA_ENTRY_ATTR => [SUBSCHEMA_ENTRY_VALUE],
'objectClasses' => all_objectclasses.collect { |s| s.to_def },
'attributeTypes' => all_attrtypes.collect { |s| s.to_def },
'ldapSyntaxes' => LDAP::Server::Syntax.all_syntaxes.collect { |s| s.to_def },
#'matchingRules' =>
#'matchingRuleUse' =>
}
end
# Clear the subschema subentry cache, so the next time someone requests
# it, it will be rebuilt
def changed
@subschema_cache = nil
end
# Add an AttributeType to the schema
def add_attrtype(str)
a = AttributeType.new(str)
@attrtypes[a.oid] = a if a.oid
a.names.each do |n|
@attrtypes[n.downcase] = a
end
end
# Locate an attributetype object by name/alias/oid (or raise exception)
def find_attrtype(n)
return n if n.nil? or n.is_a?(LDAP::Server::Schema::AttributeType)
r = @attrtypes[n.downcase]
raise LDAP::ResultError::UndefinedAttributeType, "Unknown AttributeType #{n.inspect}" unless r
r
end
# Return array of all AttributeType objects in this schema
def all_attrtypes
@attrtypes.values.uniq
end
# Add an ObjectClass to the schema
def add_objectclass(str)
o = ObjectClass.new(str)
@objectclasses[o.oid] = o if o.oid
o.names.each do |n|
@objectclasses[n.downcase] = o
end
end
# Locate an objectclass object by name/alias/oid (or raise exception)
def find_objectclass(n)
return n if n.nil? or n.is_a?(LDAP::Server::Schema::ObjectClass)
r = @objectclasses[n.downcase]
raise LDAP::ResultError::ObjectClassViolation, "Unknown ObjectClass #{n.inspect}" unless r
r
end
# Return array of all ObjectClass objects in this schema
def all_objectclasses
@objectclasses.values.uniq
end
# Load an OpenLDAP-format schema from a named file (see notes under 'load')
def load_file(filename)
File.open(filename) { |f| load(f) }
end
# Load an OpenLDAP-format schema from a string or IO object (anything
# which responds to 'each_line'). Lines starting 'attributetype'
# or 'objectclass' contain one of those objects. Does not implement
# named objectIdentifier prefixes (used in the dyngroup.schema file
# supplied with openldap, but not documented in RFC2252)
#
# Note: RFC2252 is strict about the order in which the elements appear,
# and so are we, but OpenLDAP is not. This means that a schema which
# works in OpenLDAP might not load here. For example, RFC2252 says
# that in an objectclass description, "SUP" must come before "MAY";
# if they are the other way round, our regexp-based parser will not
# accept it. The solution is simply to modify the definition so that
# the elements appear in the correct order.
def load(str_or_io)
meth = :junk_line
data = ""
str_or_io.each_line do |line|
case line
when /^\s*#/, /^\s*$/
next
when /^objectclass\s*(.*)$/i
m = $~
send(meth, data)
meth, data = :add_objectclass, m[1]
when /^attributetype\s*(.*)$/i
m = $~
send(meth, data)
meth, data = :add_attrtype, m[1]
else
data << line
end
end
send(meth,data)
self
end
def junk_line(data)
return if data.empty?
raise LDAP::ResultError::InvalidAttributeSyntax,
"Expected 'attributetype' or 'objectclass', got #{data}"
end
private :junk_line
# Load in the base set of objectclasses and attributetypes, being
# the same set as OpenLDAP preloads internally. Includes objectclasses
# 'top', 'objectclass'; attributetypes 'objectclass' , 'cn',
# 'userPassword' and 'distinguishedName'; common operational attributes
# such as 'modifyTimestamp'; plus extras needed for publishing a v3
# schema via LDAP
def load_system
load(<<EOS)
attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 2.5.4.35 NAME 'userPassword' DESC 'RFC2256/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )
attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC2256: common name(s) for which the entity is known by' SUP name )
attributetype ( 2.5.4.41 NAME 'name' DESC 'RFC2256: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
attributetype ( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC2256: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
attributetype ( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'namedref: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation )
attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC2256: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC2252: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )
attributetype ( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC2252: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )
attributetype ( 2.5.21.6 NAME 'objectClasses' DESC 'RFC2252: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )
attributetype ( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC2252: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )
attributetype ( 2.5.21.4 NAME 'matchingRules' DESC 'RFC2252: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )
attributetype ( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
attributetype ( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC2252: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC2252: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC2252: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC2252: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC2252: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )
attributetype ( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC2252: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )
attributetype ( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC2252: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.18.4 NAME 'modifiersName' DESC 'RFC2252: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.18.3 NAME 'creatorsName' DESC 'RFC2252: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC2252: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC2252: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.21.9 NAME 'structuralObjectClass' DESC 'X.500(93): structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
attributetype ( 2.5.4.0 NAME 'objectClass' DESC 'RFC2256: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
# These ones aren't published by OpenLDAP, but are referenced by the 'subschema' objectclass
attributetype ( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation )
attributetype ( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation )
attributetype ( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation )
objectclass ( 2.5.20.1 NAME 'subschema' DESC 'RFC2252: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )
#Don't have definition for subtreeSpecification:
#objectClass ( 2.5.17.0 NAME 'subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )
objectClass ( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )
objectClass ( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref )
objectClass ( 2.5.6.1 NAME 'alias' DESC 'RFC2256: an alias' SUP top STRUCTURAL MUST aliasedObjectName )
objectClass ( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC2252: extensible object' SUP top AUXILIARY )
objectClass ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
EOS
end
# After loading object classes and attr types: resolve oid strings to point
# to objects. This will expose schema inconsistencies (e.g. objectclass
# has unknown SUP class or points to unknown attributeType). However,
# unknown Syntaxes just create new Syntax objects.
def resolve_oids
all_attrtypes.each do |a|
if a.sup
s = find_attrtype(a.sup)
a.instance_eval {
@sup = s
# inherit properties (FIXME: This breaks to_def)
@equality ||= s.equality
@ordering ||= s.ordering
@substr ||= s.substr
@syntax ||= s.syntax
@maxlen ||= s.maxlen
@singlevalue ||= s.singlevalue
@collective ||= s.collective
@nousermod ||= s.nousermod
@usage ||= s.usage
}
end
a.instance_eval do
@syntax = LDAP::Server::Syntax.find(@syntax) if @syntax
@equality = LDAP::Server::MatchingRule.find(@equality) if @equality
@ordering = LDAP::Server::MatchingRule.find(@ordering) if @ordering
@substr = LDAP::Server::MatchingRule.find(@substr) if @substr
end
end
all_objectclasses.each do |o|
if o.sup
s = o.sup.collect { |ss| find_objectclass(ss) }
o.instance_eval { @sup = s }
end
if o.must
s = o.must.collect { |ss| find_attrtype(ss) }
o.instance_eval { @must = s }
end
if o.may
s = o.may.collect { |ss| find_attrtype(ss) }
o.instance_eval { @may = s }
end
end
end
# Validate a new entry or update. For a new entry, just pass a hash
# of attr=>[val, val, ...]; for an update, the first parameter is
# a hash of attr=>[:modtype, val, val...] and the second parameter
# is the existing entry, where it is assumed that the attribute names
# are already in their standard string forms (as returned by attr#name)
#
# Returns a hash containing the updated entry.
#
# If a block is given, it is called to decide whether the user is
# allowed to update an attribute; parameter is the attr *object*
# (not name; use #name if you need its name instead). Return false
# if the update is not permitted. Otherwise, the only restriction
# will be that updates to attributes declared 'nousermod' are forbidden.
#
# No DN checks are done here, since we don't know the DN.
# Checking that the entry contains an attribute for the RDN is the
# responsibility of the caller.
def validate(mods, entry={})
# Run through the mods, make the normalized names, and perform any
# updates
# FIXME: I don't know if these are the right results to return
# for the various types of validation errors
oc_changed = false
res = entry.dup
mods.each do |attrname, nv|
attr = find_attrtype(attrname)
attrname = attr.to_s
raise LDAP::ResultError::ConstraintViolation,
"Cannot modify #{attrname}" if attr.nousermod or
(block_given? and !yield(attr))
# Perform the update
vals = res[attrname] || []
checkvals = []
nv = [nv] unless nv.is_a?(Array)
case nv.first
when :add
checkvals = nv[1..-1]
vals += checkvals
vals.uniq! # FIXME: ?? error if duplicate values
# FIXME: normalize values? e.g. c: gb and c: GB are same value.
when :delete
nv = nv[1..-1]
if nv.empty?
vals = [] # ?? error if does not exist
else
nv.each { |v| vals.delete(v) } # ?? error if value missing
end
when :replace
vals = checkvals = nv[1..-1]
else
vals = checkvals = nv
end
if vals == []
res.delete(attrname)
else
res[attrname] = vals
end
# Attribute validation
raise LDAP::ResultError::ObjectClassViolation,
"Attribute #{attr} is SINGLE-VALUE" if attr.singlevalue and vals.size > 1
checkvals.each do |val|
raise LDAP::ResultError::InvalidAttributeSyntax,
"Nil or empty value for attribute #{attr}" if val.nil? or val.empty?
raise LDAP::ResultError::InvalidAttributeSyntax,
"Bad value for #{attr}: #{val.inspect}" if attr.syntax and ! attr.syntax.match(val)
raise LDAP::ResultError::InvalidAttributeSyntax,
"Value too long for #{attr} (max #{attr.maxlen})" if attr.maxlen and val.length > attr.maxlen
end
oc_changed = true if attrname == 'objectClass'
end
# Now do objectClass checks
oc = res['objectClass']
unless oc
raise LDAP::ResultError::ObjectClassViolation,
"objectClass attribute missing"
end
oc = oc.collect { |val| find_objectclass(val) }
if oc_changed
# Add superior objectClasses (note: growing an array while you
# iterate over it seems to work, in ruby-1.8.2 anyway!)
oc.each do |objectclass|
objectclass.sup.each do |s|
oc.push(s) unless oc.include?(s)
end
end
res['objectClass'] = oc.collect { |oo| oo.to_s }
# Check that exactly one structural objectClass is present
unless oc.find_all { |s| s.struct == :structural }.size >= 1
raise LDAP::ResultError::ObjectClassViolation,
"Entry must have at least one structural objectClass"
# Exactly one? But you have to sort out the inheritance problem
# (e.g. both person and organizationalPerson are declared
# structural)
end
end
# Ensure that all MUST attributes are present
allow_attr = {}
oc.each do |objectclass|
objectclass.must.each do |m|
unless res[m.name] and res[m.name] != []
raise LDAP::ResultError::ObjectClassViolation, "Missing attribute #{m} required by objectClass #{objectclass}"
end
allow_attr[m.name] = true
end
objectclass.may.each do |m|
allow_attr[m.name] = true
end
end
unless oc.find { |objectclass| objectclass.name == 'extensibleObject' }
# Now check all the attributes given are permitted by MUST or MAY
res.each_key do |attr|
unless allow_attr[attr] or find_attrtype(attr).usage == :directoryOperation
raise LDAP::ResultError::ObjectClassViolation, "Attribute #{attr} not permitted by objectClass"
end
end
end
return res
end
# Hopefully backwards-compatible API for ruby-ldap's LDAP::Schema.
# Since MUST/MAY/SUP may point to schema objects, convert them back
# to strings.
def names(key)
case key
when 'objectClasses'
return all_objectclasses.collect { |e| e.name }
when 'attributeTypes'
return all_attrtypes.collect { |e| e.name }
when 'ldapSyntaxes'
return LDAP::Server::Syntax.all_syntaxes.collect { |e| e.name }
when 'matchingRules'
return LDAP::Server::MatchingRule.all_matching_rules.collect { |e| e.name }
# TODO: matchingRuleUse
end
return nil
end
# Backwards-compatible for ruby-ldap LDAP::Schema
def attr(oc,at)
o = find_objectclass(oc)
case at.upcase
when 'MUST'
return o.must.collect { |e| e.to_s }
when 'MAY'
return o.may.collect { |e| e.to_s }
when 'SUP'
return o.sup.collect { |e| e.to_s }
when 'NAME'
return o.names.collect { |e| e.to_s }
when 'DESC'
return [o.desc]
end
return nil
rescue LDAP::ResultError
return nil
end
# Backwards-compatible for ruby-ldap LDAP::Schema
def must(oc)
attr(oc, "MUST")
end
# Backwards-compatible for ruby-ldap LDAP::Schema
def may(oc)
attr(oc, "MAY")
end
# Backwards-compatible for ruby-ldap LDAP::Schema
def sup(oc)
attr(oc, "SUP")
end
#####################################################################
# Class holding an instance of an AttributeTypeDescription (RFC2252 4.2)
class AttributeType
attr_reader :oid, :names, :desc, :obsolete, :sup, :equality, :ordering
attr_reader :substr, :syntax, :maxlen, :singlevalue, :collective
attr_reader :nousermod, :usage
def initialize(str)
m = LDAP::Server::Syntax::AttributeTypeDescription.match(str)
raise LDAP::ResultError::InvalidAttributeSyntax,
"Bad AttributeTypeDescription #{str.inspect}" unless m
@oid = m[1]
@names = (m[2]||"").scan(/'(.*?)'/).flatten
@desc = m[3]
@obsolete = ! m[4].nil?
@sup = m[5]
@equality = m[6]
@ordering = m[7]
@substr = m[8]
@syntax = m[9]
@maxlen = m[10] && m[10].to_i
@singlevalue = ! m[11].nil?
@collective = ! m[12].nil?
@nousermod = ! m[13].nil?
@usage = m[14] && m[14].intern
# This is the cache of the stringified version. Rather than
# initialize to str, we set nil to force it to be rebuilt
@def = nil
end
def name
@names.first
end
def to_s
(@names && @names.first) || @oid
end
def changed
@def = nil
end
def to_def
return @def if @def
ans = "( #{@oid} "
if @names.nil? or @names.empty?
# nothing
elsif @names.size == 1
ans << "NAME '#{@names.first}' "
else
ans << "NAME ( "
@names.each { |n| ans << "'#{n}' " }
ans << ") "
end
ans << "DESC '#{@desc}' " if @desc
ans << "OBSOLETE " if @obsolete
ans << "SUP #{@sup} " if @sup # oid
ans << "EQUALITY #{@equality} " if @equality # oid
ans << "ORDERING #{@ordering} " if @ordering # oid
ans << "SUBSTR #{@substr} " if @substr # oid
ans << "SYNTAX #{@syntax}#{@maxlen && "{#{@maxlen}}"} " if @syntax
ans << "SINGLE-VALUE " if @singlevalue
ans << "COLLECTIVE " if @collective
ans << "NO-USER-MODIFICATION " if @nousermod
ans << "USAGE #{@usage} " if @usage
ans << ")"
@def = ans
end
end # class AttributeType
#####################################################################
# Class holding an instance of an ObjectClassDescription (RFC2252 4.4)
class ObjectClass
attr_reader :oid, :names, :desc, :obsolete, :sup, :struct, :must, :may
SCAN_WOID = /#{LDAP::Server::Syntax::WOID}/x
def initialize(str)
m = LDAP::Server::Syntax::ObjectClassDescription.match(str)
raise LDAP::ResultError::InvalidAttributeSyntax,
"Bad ObjectClassDescription #{str.inspect}" unless m
@oid = m[1]
@names = (m[2]||"").scan(/'(.*?)'/).flatten
@desc = m[3]
@obsolete = ! m[4].nil?
@sup = (m[5]||"").scan(SCAN_WOID).flatten
@struct = m[6] ? m[6].downcase.intern : :structural
@must = (m[7]||"").scan(SCAN_WOID).flatten
@may = (m[8]||"").scan(SCAN_WOID).flatten
@def = nil
end
def name
@names.first
end
def to_s
(@names && @names.first) || @oid
end
def changed
@def = nil
end
def to_def
return @def if @def
ans = "( #{@oid} "
if @names.nil? or @names.empty?
# nothing
elsif @names.size == 1
ans << "NAME '#{@names.first}' "
else
ans << "NAME ( "
@names.each { |n| ans << "'#{n}' " }
ans << ") "
end
ans << "DESC '#{@desc}' " if @desc
ans << "OBSOLETE " if @obsolete
ans << joinoids("SUP ",@sup," ")
ans << "#{@struct.to_s.upcase} " if @struct
ans << joinoids("MUST ",@must," ")
ans << joinoids("MAY ",@may," ")
ans << ")"
@def = ans
end
def joinoids(pfx,arr,sfx)
return "" unless arr and !arr.empty?
return "#{pfx}#{arr}#{sfx}" unless arr.is_a?(Array)
a = arr.collect { |elem| elem.to_s }
if a.size == 1
return "#{pfx}#{a.first}#{sfx}"
else
return "#{pfx}( #{a.join(" $ ")} )#{sfx}"
end
end
end # class ObjectClass
end # class Schema
end # class Server
end # module LDAP