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.

283 lines
8.6 KiB

require 'ldap/server/syntax'
require 'ldap/server/result'
module LDAP
class Server
# A class which holds LDAP MatchingRules. For now there is a global pool
# of MatchingRule objects (rather than each Schema object having
# its own pool)
class MatchingRule
attr_reader :oid, :names, :syntax, :desc, :obsolete
# Create a new MatchingRule object
def initialize(oid, names, syntax, desc=nil, obsolete=false, &blk)
@oid = oid
@names = names
@names = [@names] unless @names.is_a?(Array)
@desc = desc
@obsolete = obsolete
@syntax = LDAP::Server::Syntax.find(syntax) # creates new obj if reqd
@def = nil
# initialization hook
self.instance_eval(&blk) if blk
end
def name
(@names && names[0]) || @oid
end
def to_s
(@names && names[0]) || @oid
end
def normalize(x)
x
end
# Create a new MatchingRule object, given its description string
def self.from_def(str, &blk)
m = LDAP::Server::Syntax::MatchingRuleDescription.match(str)
raise LDAP::ResultError::InvalidAttributeSyntax,
"Bad MatchingRuleDescription #{str.inspect}" unless m
new(m[1], m[2].scan(/'(.*?)'/).flatten, m[5], m[3], m[4], &blk)
end
def to_def
return @def if @def
ans = "( #{@oid} "
if names.nil? or @names.empty?
# nothing
elsif @names.size == 1
ans << "NAME '#{@names[0]}' "
else
ans << "NAME ( "
@names.each { |n| ans << "'#{n}' " }
ans << ") "
end
ans << "DESC '#@desc' " if @desc
ans << "OBSOLETE " if @obsolete
ans << "SYNTAX #@syntax " if @syntax
ans << ")"
@def = ans
end
@@rules = {} # oid / name / alias => object
# Add a new matching rule
def self.add(*args, &blk)
s = new(*args, &blk)
@@rules[s.oid] = s
return if s.names.nil?
s.names.each do |n|
@@rules[n.downcase] = s
end
end
# Find a MatchingRule object given a name or oid, or return nil
# (? should we create one automatically, like Syntax)
def self.find(x)
return x if x.nil? or x.is_a?(LDAP::Server::MatchingRule)
@@rules[x.downcase]
end
# Return all known matching rules
def self.all_matching_rules
@@rules.values.uniq
end
# Now some things we can mixin to a MatchingRule when needed.
# Replace 'normalize' with a function which gives the canonical
# version of a value for comparison.
module Equality
def eq(vals, m)
return false if vals.nil?
m = normalize(m)
vals.each { |v| return true if normalize(v) == m }
return false
end
end
module Ordering
def ge(vals, m)
return false if vals.nil?
m = normalize(m)
vals.each { |v| return true if normalize(v) >= m }
return false
end
def le(vals, m)
return false if vals.nil?
m = normalize(m)
vals.each { |v| return true if normalize(v) <= m }
return false
end
end
module Substrings
def substrings(vals, *ss)
return false if vals.nil?
# convert the condition list into a regexp
re = []
re << "^#{Regexp.escape(normalize(ss[0]).to_s)}" if ss[0]
ss[1..-2].each { |s| re << Regexp.escape(normalize(s).to_s) }
re << "#{Regexp.escape(normalize(ss[-1]).to_s)}$" if ss[-1]
re = Regexp.new(re.join(".*"))
vals.each do |v|
v = normalize(v).to_s
return true if re.match(v)
end
return false
end
end # module Substrings
class DefaultMatchingClass
include MatchingRule::Equality
include MatchingRule::Ordering
include MatchingRule::Substrings
def normalize(x)
x
end
end
DefaultMatch = DefaultMatchingClass.new
end # class MatchingRule
#
# And now, here are some matching rules you can use (RFC2252 section 8)
#
class MatchingRule
add('2.5.13.0', 'objectIdentifierMatch', '1.3.6.1.4.1.1466.115.121.1.38') do
extend Equality
end
# FIXME: Filters should return undef if the OID is not in the schema
# (which means passing in the schema to every equality test)
add('2.5.13.1', 'distinguishedNameMatch', '1.3.6.1.4.1.1466.115.121.1.12') do
extend Equality
end
# FIXME: Distinguished Name matching is supposed to parse the DN into
# its parts and then apply the schema equality rules to each part
# (i.e. some parts may be case-sensitive, others case-insensitive)
# This is just one of the many nonsense design decisions in LDAP :-(
# How is a DirectoryString different to an IA5String or a PrintableString?
module StringTrim
def normalize(x); x.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
end
module StringDowncase
def normalize(x); x.downcase.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
end
add('2.5.13.2', 'caseIgnoreMatch', '1.3.6.1.4.1.1466.115.1') do
extend Equality
extend StringDowncase
end
module Integer
def normalize(x); x.to_i; end
end
add('2.5.13.8', 'numericStringMatch', '1.3.6.1.4.1.1466.115.121.1.36') do
extend Equality
extend Integer
end
# TODO: Add semantics for these (difficult since RFC2252 doesn't give
# them, so we presumably have to go through X.500)
add('2.5.13.11', 'caseIgnoreListMatch', '1.3.6.1.4.1.1466.115.121.1.41')
add('2.5.13.14', 'integerMatch', '1.3.6.1.4.1.1466.115.121.1.27') do
extend Equality
extend Integer
end
add('2.5.13.16', 'bitStringMatch', '1.3.6.1.4.1.1466.115.121.1.6')
add('2.5.13.20', 'telephoneNumberMatch', '1.3.6.1.4.1.1466.115.121.1.50') do
extend Equality
extend StringTrim
end
add('2.5.13.22', 'presentationAddressMatch', '1.3.6.1.4.1.1466.115.121.1.43')
add('2.5.13.23', 'uniqueMemberMatch', '1.3.6.1.4.1.1466.115.121.1.34')
add('2.5.13.24', 'protocolInformationMatch', '1.3.6.1.4.1.1466.115.121.1.42')
add('2.5.13.27', 'generalizedTimeMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Equality }
# IA5 stuff. FIXME: What's the correct way to 'downcase' UTF8 strings?
module IA5Trim
def normalize(x); x.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
end
module IA5Downcase
def normalize(x); x.downcase.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
end
add('1.3.6.1.4.1.1466.109.114.1', 'caseExactIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
extend Equality
extend IA5Trim
end
add('1.3.6.1.4.1.1466.109.114.2', 'caseIgnoreIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
extend Equality
extend IA5Downcase
end
add('2.5.13.28', 'generalizedTimeOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Ordering }
add('2.5.13.3', 'caseIgnoreOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') do
extend Ordering
extend StringDowncase
end
add('2.5.13.4', 'caseIgnoreSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
extend Substrings
extend StringDowncase
end
add('2.5.13.21', 'telephoneNumberSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
extend Substrings
end
add('2.5.13.10', 'numericStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
extend Substrings
end
# from OpenLDAP
add('1.3.6.1.4.1.4203.1.2.1', 'caseExactIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
extend Substrings
extend IA5Trim
end
add('1.3.6.1.4.1.1466.109.114.3', 'caseIgnoreIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
extend Substrings
extend IA5Downcase
end
add('2.5.13.5', 'caseExactMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Equality }
add('2.5.13.6', 'caseExactOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Ordering }
add('2.5.13.7', 'caseExactSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') { extend Substrings }
add('2.5.13.9', 'numericStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.36') { extend Ordering; extend Integer }
add('2.5.13.13', 'booleanMatch', '1.3.6.1.4.1.1466.115.121.1.7') do
extend Equality
def self.normalize(x)
return true if x == 'TRUE'
return false if x == 'FALSE'
x
end
end
add('2.5.13.15', 'integerOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.27') { extend Ordering; extend Integer }
add('2.5.13.17', 'octetStringMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Equality }
add('2.5.13.18', 'octetStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Ordering }
add('2.5.13.19', 'octetStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Substrings }
end # class MatchingRule
end # class Server
end # module LDAP