Adding experimental RFC4515 extensible filtering.

This commit is contained in:
Austin Ziegler 2010-03-21 00:50:35 -04:00 committed by Kaspar Schiess
parent 1dbf5908ae
commit 3e07125214
3 changed files with 208 additions and 95 deletions

View file

@ -18,6 +18,8 @@
* Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed * Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed
some of the regular expressions to be more canonically defined. some of the regular expressions to be more canonically defined.
* Cleaned up the string representation of Filter objects. * Cleaned up the string representation of Filter objects.
* Added experimental support for RFC4515 extensible matching (e.g.,
"(cn:caseExactMatch:=Fred Flintstone)"); provided by "nowhereman".
* Added or revised documentation: * Added or revised documentation:
* Core class extension methods under Net::BER. * Core class extension methods under Net::BER.
* Extended unit testing: * Extended unit testing:

View file

@ -43,7 +43,7 @@
class Net::LDAP::Filter class Net::LDAP::Filter
## ##
# Known filter types. # Known filter types.
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not ] FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
def initialize(op, left, right) #:nodoc: def initialize(op, left, right) #:nodoc:
unless FilterTypes.include?(op) unless FilterTypes.include?(op)
@ -83,6 +83,55 @@ class Net::LDAP::Filter
new(:eq, attribute, value) new(:eq, attribute, value)
end end
##
# Creates a Filter object indicating extensible comparison. This Filter
# object is currently considered EXPERIMENTAL.
#
# sample_attributes = ['cn:fr', 'cn:fr.eq',
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
# attr = sample_attributes.first # Pick an extensible attribute
# value = 'roberts'
#
# filter = "#{attr}:=#{value}" # Basic String Filter
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
#
# # Perform a search with the Extensible Match Filter
# Net::LDAP.search(:filter => filter)
#--
# The LDIF required to support the above examples on the OpenDS LDAP
# server:
#
# version: 1
#
# dn: dc=example,dc=com
# objectClass: domain
# objectClass: top
# dc: example
#
# dn: ou=People,dc=example,dc=com
# objectClass: organizationalUnit
# objectClass: top
# ou: People
#
# dn: uid=1,ou=People,dc=example,dc=com
# objectClass: person
# objectClass: organizationalPerson
# objectClass: inetOrgPerson
# objectClass: top
# cn:: csO0YsOpcnRz
# sn:: YsO0YiByw7Riw6lydHM=
# givenName:: YsO0Yg==
# uid: 1
#
# =Refs:
# * http://www.ietf.org/rfc/rfc2251.txt
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
#++
def ex(attribute, value)
new(:ex, attribute, value)
end
## ##
# Creates a Filter object indicating that a particular attribute value # Creates a Filter object indicating that a particular attribute value
# is either not present or does not match a particular string; see # is either not present or does not match a particular string; see
@ -280,9 +329,9 @@ class Net::LDAP::Filter
def ==(filter) def ==(filter)
# 20100320 AZ: We need to come up with a better way of doing this. This # 20100320 AZ: We need to come up with a better way of doing this. This
# is just nasty. # is just nasty.
str = "[@op,@left,@right]" str = "[@op,@left,@right]"
self.instance_eval(str) == filter.instance_eval(str) self.instance_eval(str) == filter.instance_eval(str)
end end
def to_raw_rfc2254 def to_raw_rfc2254
case @op case @op
@ -290,19 +339,20 @@ class Net::LDAP::Filter
"!(#{@left}=#{@right})" "!(#{@left}=#{@right})"
when :eq when :eq
"#{@left}=#{@right}" "#{@left}=#{@right}"
when :ex
"#{@left}:=#{@right}"
when :ge when :ge
"#{@left}>=#{@right}" "#{@left}>=#{@right}"
when :le when :le
"#{@left}<=#{@right}" "#{@left}<=#{@right}"
when :and when :and
"&(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})" "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
when :or when :or
"|(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})" "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
when :not when :not
"!(#{@left.__send__(:to_raw_rfc2254)})" "!(#{@left.to_raw_rfc2254})"
end end
end end
private :to_raw_rfc2254
## ##
# Converts the Filter object to an RFC 2254-compatible text format. # Converts the Filter object to an RFC 2254-compatible text format.
@ -317,21 +367,21 @@ class Net::LDAP::Filter
## ##
# Converts the filter to BER format. # Converts the filter to BER format.
#-- #--
# to_ber
# Filter ::= # Filter ::=
# CHOICE { # CHOICE {
# and [0] SET OF Filter, # and [0] SET OF Filter,
# or [1] SET OF Filter, # or [1] SET OF Filter,
# not [2] Filter, # not [2] Filter,
# equalityMatch [3] AttributeValueAssertion, # equalityMatch [3] AttributeValueAssertion,
# substrings [4] SubstringFilter, # substrings [4] SubstringFilter,
# greaterOrEqual [5] AttributeValueAssertion, # greaterOrEqual [5] AttributeValueAssertion,
# lessOrEqual [6] AttributeValueAssertion, # lessOrEqual [6] AttributeValueAssertion,
# present [7] AttributeType, # present [7] AttributeType,
# approxMatch [8] AttributeValueAssertion # approxMatch [8] AttributeValueAssertion,
# extensibleMatch [9] MatchingRuleAssertion
# } # }
# #
# SubstringFilter # SubstringFilter ::=
# SEQUENCE { # SEQUENCE {
# type AttributeType, # type AttributeType,
# SEQUENCE OF CHOICE { # SEQUENCE OF CHOICE {
@ -340,6 +390,23 @@ class Net::LDAP::Filter
# final [2] LDAPString # final [2] LDAPString
# } # }
# } # }
#
# MatchingRuleAssertion ::=
# SEQUENCE {
# matchingRule [1] MatchingRuleId OPTIONAL,
# type [2] AttributeDescription OPTIONAL,
# matchValue [3] AssertionValue,
# dnAttributes [4] BOOLEAN DEFAULT FALSE
# }
#
# Matching Rule Suffixes
# Less than [.1] or .[lt]
# Less than or equal to [.2] or [.lte]
# Equality [.3] or [.eq] (default)
# Greater than or equal to [.4] or [.gte]
# Greater than [.5] or [.gt]
# Substring [.6] or [.sub]
#
#++ #++
def to_ber def to_ber
case @op case @op
@ -381,6 +448,20 @@ class Net::LDAP::Filter
else # equality else # equality
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3) [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
end end
when :ex
seq = []
unless @left =~ /^([-;\d\w]*)(:dn)?(:(\w+|[.\d\w]+))?$/
raise Net::LDAP::LdapError, "Bad attribute #{@left}"
end
type, dn, rule = $1, $2, $4
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
seq.to_ber_contextspecific(9)
when :ge when :ge
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5) [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
when :le when :le
@ -407,10 +488,10 @@ class Net::LDAP::Filter
# some desired application-defined processing, and may return a # some desired application-defined processing, and may return a
# locally-meaningful object that will appear as a parameter in the :and, # locally-meaningful object that will appear as a parameter in the :and,
# :or and :not operations detailed below. # :or and :not operations detailed below.
# #
# A typical object to return from the user-supplied block is an array of # A typical object to return from the user-supplied block is an array of
# Net::LDAP::Filter objects. # Net::LDAP::Filter objects.
# #
# These are the possible values that may be passed to the user-supplied # These are the possible values that may be passed to the user-supplied
# block: # block:
# * :equalityMatch (the arguments will be an attribute name and a value # * :equalityMatch (the arguments will be an attribute name and a value
@ -428,26 +509,26 @@ class Net::LDAP::Filter
# a recursive call to #execute, with the same block; and # a recursive call to #execute, with the same block; and
# * :not (one argument, which is an object returned from a recursive # * :not (one argument, which is an object returned from a recursive
# call to #execute with the the same block. # call to #execute with the the same block.
def execute(&block) def execute(&block)
case @op case @op
when :eq when :eq
if @right == "*" if @right == "*"
yield :present, @left yield :present, @left
elsif @right.index '*' elsif @right.index '*'
yield :substrings, @left, @right yield :substrings, @left, @right
else else
yield :equalityMatch, @left, @right yield :equalityMatch, @left, @right
end end
when :ge when :ge
yield :greaterOrEqual, @left, @right yield :greaterOrEqual, @left, @right
when :le when :le
yield :lessOrEqual, @left, @right yield :lessOrEqual, @left, @right
when :or, :and when :or, :and
yield @op, (@left.execute(&block)), (@right.execute(&block)) yield @op, (@left.execute(&block)), (@right.execute(&block))
when :not when :not
yield @op, (@left.execute(&block)) yield @op, (@left.execute(&block))
end || [] end || []
end end
## ##
# This is a private helper method for dealing with chains of ANDs and ORs # This is a private helper method for dealing with chains of ANDs and ORs
@ -588,9 +669,9 @@ class Net::LDAP::Filter
# This parses a given expression inside of parentheses. # This parses a given expression inside of parentheses.
def parse_filter_branch(scanner) def parse_filter_branch(scanner)
scanner.scan(/\s*/) scanner.scan(/\s*/)
if token = scanner.scan(/[-\w_]+/) if token = scanner.scan(/[-\w\d_:.]*[\d\w]/)
scanner.scan(/\s*/) scanner.scan(/\s*/)
if op = scanner.scan(/<=|>=|!=|=/) if op = scanner.scan(/<=|>=|!=|:=|=/)
scanner.scan(/\s*/) scanner.scan(/\s*/)
if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/) if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/)
# 20100313 AZ: Assumes that "(uid=george*)" is the same as # 20100313 AZ: Assumes that "(uid=george*)" is the same as
@ -606,6 +687,8 @@ class Net::LDAP::Filter
Net::LDAP::Filter.le(token, value) Net::LDAP::Filter.le(token, value)
when ">=" when ">="
Net::LDAP::Filter.ge(token, value) Net::LDAP::Filter.ge(token, value)
when ":="
Net::LDAP::Filter.ex(token, value)
end end
end end
end end

View file

@ -1,68 +1,97 @@
# $Id: testfilter.rb 245 2007-05-05 02:44:32Z blackhedd $
require 'common' require 'common'
class TestFilter < Test::Unit::TestCase class TestFilter < Test::Unit::TestCase
Filter = Net::LDAP::Filter
# Note that the RFC doesn't define either less-than or greater-than. def test_bug_7534_rfc2254
def test_rfc_2254 assert_equal("(cn=Tim Wizard)",
Net::LDAP::Filter.from_rfc2254( " ( uid=george* ) " ) Filter.from_rfc2254("(cn=Tim Wizard)").to_rfc2254)
Net::LDAP::Filter.from_rfc2254( "uid!=george*" ) end
Net::LDAP::Filter.from_rfc2254( "uid <= george*" )
Net::LDAP::Filter.from_rfc2254( "uid>=george*" )
Net::LDAP::Filter.from_rfc2254( "uid!=george*" )
Net::LDAP::Filter.from_rfc2254( "(& (uid!=george* ) (mail=*))" ) def test_invalid_filter_string
Net::LDAP::Filter.from_rfc2254( "(| (uid!=george* ) (mail=*))" ) assert_raises(Net::LDAP::LdapError) { Filter.from_rfc2254("") }
Net::LDAP::Filter.from_rfc2254( "(! (mail=*))" ) end
def test_invalid_filter
assert_raises(Net::LDAP::LdapError) {
# This test exists to prove that our constructor blocks unknown filter
# types. All filters must be constructed using helpers.
Filter.__send__(:new, :xx, nil, nil)
}
end
def test_to_s
assert_equal("(uid=george *)", Filter.eq("uid", "george *").to_s)
end
def test_c2
assert_equal("(uid=george *)",
Filter.from_rfc2254("uid=george *").to_rfc2254)
assert_equal("(uid:=george *)",
Filter.from_rfc2254("uid:=george *").to_rfc2254)
assert_equal("(uid=george*)",
Filter.from_rfc2254(" ( uid = george* ) ").to_rfc2254)
assert_equal("(!(uid=george*))",
Filter.from_rfc2254("uid!=george*").to_rfc2254)
assert_equal("(uid<=george*)",
Filter.from_rfc2254("uid <= george*").to_rfc2254)
assert_equal("(uid>=george*)",
Filter.from_rfc2254("uid>=george*").to_rfc2254)
assert_equal("(&(uid=george*)(mail=*))",
Filter.from_rfc2254("(& (uid=george* ) (mail=*))").to_rfc2254)
assert_equal("(|(uid=george*)(mail=*))",
Filter.from_rfc2254("(| (uid=george* ) (mail=*))").to_rfc2254)
assert_equal("(!(mail=*))",
Filter.from_rfc2254("(! (mail=*))").to_rfc2254)
end end
def test_filters_from_ber def test_filters_from_ber
[ [
Net::LDAP::Filter.eq( "objectclass", "*" ), Net::LDAP::Filter.eq("objectclass", "*"),
Net::LDAP::Filter.pres( "objectclass" ), Net::LDAP::Filter.pres("objectclass"),
Net::LDAP::Filter.eq( "objectclass", "ou" ), Net::LDAP::Filter.eq("objectclass", "ou"),
Net::LDAP::Filter.ge( "uid", "500" ), Net::LDAP::Filter.ge("uid", "500"),
Net::LDAP::Filter.le( "uid", "500" ), Net::LDAP::Filter.le("uid", "500"),
(~ Net::LDAP::Filter.pres( "objectclass" )), (~ Net::LDAP::Filter.pres("objectclass")),
(Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" )), (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou")),
(Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.pres( "ou" ) & Net::LDAP::Filter.pres("sn")), (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou") & Net::LDAP::Filter.pres("sn")),
(Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.pres( "ou" ) | Net::LDAP::Filter.pres("sn")), (Net::LDAP::Filter.pres("objectclass") | Net::LDAP::Filter.pres("ou") | Net::LDAP::Filter.pres("sn")),
Net::LDAP::Filter.eq( "objectclass", "*aaa" ), Net::LDAP::Filter.eq("objectclass", "*aaa"),
Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb" ), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb"),
Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc" ), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc"),
Net::LDAP::Filter.eq( "objectclass", "aaa*bbb" ), Net::LDAP::Filter.eq("objectclass", "aaa*bbb"),
Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc" ), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc"),
Net::LDAP::Filter.eq( "objectclass", "abc*def*1111*22*g" ), Net::LDAP::Filter.eq("objectclass", "abc*def*1111*22*g"),
Net::LDAP::Filter.eq( "objectclass", "*aaa*" ), Net::LDAP::Filter.eq("objectclass", "*aaa*"),
Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*" ), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*"),
Net::LDAP::Filter.eq( "objectclass", "*aaa*bbb*ccc*" ), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc*"),
Net::LDAP::Filter.eq( "objectclass", "aaa*" ), Net::LDAP::Filter.eq("objectclass", "aaa*"),
Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*" ), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*"),
Net::LDAP::Filter.eq( "objectclass", "aaa*bbb*ccc*" ), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc*"),
].each {|ber| ].each {|ber|
f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax))
assert( f == ber ) assert(f == ber)
assert_equal( f.to_ber, ber.to_ber ) assert_equal(f.to_ber, ber.to_ber)
} }
end end
def test_ber_from_rfc2254_filter def test_ber_from_rfc2254_filter
[ [
Net::LDAP::Filter.construct( "objectclass=*" ), Net::LDAP::Filter.construct("objectclass=*"),
Net::LDAP::Filter.construct("objectclass=ou" ), Net::LDAP::Filter.construct("objectclass=ou"),
Net::LDAP::Filter.construct("uid >= 500" ), Net::LDAP::Filter.construct("uid >= 500"),
Net::LDAP::Filter.construct("uid <= 500" ), Net::LDAP::Filter.construct("uid <= 500"),
Net::LDAP::Filter.construct("(!(uid=*))" ), Net::LDAP::Filter.construct("(!(uid=*))"),
Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))" ), Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))"),
Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))" ), Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))"),
Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))" ), Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))"),
Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))" ), Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))"),
Net::LDAP::Filter.construct("objectclass=*aaa"), Net::LDAP::Filter.construct("objectclass=*aaa"),
Net::LDAP::Filter.construct("objectclass=*aaa*bbb"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb"),
Net::LDAP::Filter.construct("objectclass=*aaa bbb"),
Net::LDAP::Filter.construct("objectclass=*aaa bbb"),
Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"),
Net::LDAP::Filter.construct("objectclass=aaa*bbb"), Net::LDAP::Filter.construct("objectclass=aaa*bbb"),
Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"),
@ -74,10 +103,9 @@ class TestFilter < Test::Unit::TestCase
Net::LDAP::Filter.construct("objectclass=aaa*bbb*"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*"),
Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"),
].each {|ber| ].each {|ber|
f = Net::LDAP::Filter.parse_ber( ber.to_ber.read_ber( Net::LDAP::AsnSyntax) ) f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax))
assert( f == ber ) assert(f == ber)
assert_equal( f.to_ber, ber.to_ber ) assert_equal(f.to_ber, ber.to_ber)
} }
end end
end end