Skip to content

Commit 3e07125

Browse files
Austin Zieglerkschiess
authored andcommitted
Adding experimental RFC4515 extensible filtering.
1 parent 1dbf590 commit 3e07125

File tree

3 files changed

+208
-95
lines changed

3 files changed

+208
-95
lines changed

History.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed
1919
some of the regular expressions to be more canonically defined.
2020
* Cleaned up the string representation of Filter objects.
21+
* Added experimental support for RFC4515 extensible matching (e.g.,
22+
"(cn:caseExactMatch:=Fred Flintstone)"); provided by "nowhereman".
2123
* Added or revised documentation:
2224
* Core class extension methods under Net::BER.
2325
* Extended unit testing:

lib/net/ldap/filter.rb

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
class Net::LDAP::Filter
4444
##
4545
# Known filter types.
46-
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not ]
46+
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
4747

4848
def initialize(op, left, right) #:nodoc:
4949
unless FilterTypes.include?(op)
@@ -83,6 +83,55 @@ def eq(attribute, value)
8383
new(:eq, attribute, value)
8484
end
8585

86+
##
87+
# Creates a Filter object indicating extensible comparison. This Filter
88+
# object is currently considered EXPERIMENTAL.
89+
#
90+
# sample_attributes = ['cn:fr', 'cn:fr.eq',
91+
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
92+
# attr = sample_attributes.first # Pick an extensible attribute
93+
# value = 'roberts'
94+
#
95+
# filter = "#{attr}:=#{value}" # Basic String Filter
96+
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
97+
#
98+
# # Perform a search with the Extensible Match Filter
99+
# Net::LDAP.search(:filter => filter)
100+
#--
101+
# The LDIF required to support the above examples on the OpenDS LDAP
102+
# server:
103+
#
104+
# version: 1
105+
#
106+
# dn: dc=example,dc=com
107+
# objectClass: domain
108+
# objectClass: top
109+
# dc: example
110+
#
111+
# dn: ou=People,dc=example,dc=com
112+
# objectClass: organizationalUnit
113+
# objectClass: top
114+
# ou: People
115+
#
116+
# dn: uid=1,ou=People,dc=example,dc=com
117+
# objectClass: person
118+
# objectClass: organizationalPerson
119+
# objectClass: inetOrgPerson
120+
# objectClass: top
121+
# cn:: csO0YsOpcnRz
122+
# sn:: YsO0YiByw7Riw6lydHM=
123+
# givenName:: YsO0Yg==
124+
# uid: 1
125+
#
126+
# =Refs:
127+
# * http://www.ietf.org/rfc/rfc2251.txt
128+
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
129+
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
130+
#++
131+
def ex(attribute, value)
132+
new(:ex, attribute, value)
133+
end
134+
86135
##
87136
# Creates a Filter object indicating that a particular attribute value
88137
# is either not present or does not match a particular string; see
@@ -280,29 +329,30 @@ def ~@
280329
def ==(filter)
281330
# 20100320 AZ: We need to come up with a better way of doing this. This
282331
# is just nasty.
283-
str = "[@op,@left,@right]"
284-
self.instance_eval(str) == filter.instance_eval(str)
285-
end
332+
str = "[@op,@left,@right]"
333+
self.instance_eval(str) == filter.instance_eval(str)
334+
end
286335

287336
def to_raw_rfc2254
288337
case @op
289338
when :ne
290339
"!(#{@left}=#{@right})"
291340
when :eq
292341
"#{@left}=#{@right}"
342+
when :ex
343+
"#{@left}:=#{@right}"
293344
when :ge
294345
"#{@left}>=#{@right}"
295346
when :le
296347
"#{@left}<=#{@right}"
297348
when :and
298-
"&(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
349+
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
299350
when :or
300-
"|(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
351+
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
301352
when :not
302-
"!(#{@left.__send__(:to_raw_rfc2254)})"
353+
"!(#{@left.to_raw_rfc2254})"
303354
end
304355
end
305-
private :to_raw_rfc2254
306356

307357
##
308358
# Converts the Filter object to an RFC 2254-compatible text format.
@@ -317,21 +367,21 @@ def to_s
317367
##
318368
# Converts the filter to BER format.
319369
#--
320-
# to_ber
321370
# Filter ::=
322371
# CHOICE {
323-
# and [0] SET OF Filter,
324-
# or [1] SET OF Filter,
325-
# not [2] Filter,
326-
# equalityMatch [3] AttributeValueAssertion,
327-
# substrings [4] SubstringFilter,
328-
# greaterOrEqual [5] AttributeValueAssertion,
329-
# lessOrEqual [6] AttributeValueAssertion,
330-
# present [7] AttributeType,
331-
# approxMatch [8] AttributeValueAssertion
372+
# and [0] SET OF Filter,
373+
# or [1] SET OF Filter,
374+
# not [2] Filter,
375+
# equalityMatch [3] AttributeValueAssertion,
376+
# substrings [4] SubstringFilter,
377+
# greaterOrEqual [5] AttributeValueAssertion,
378+
# lessOrEqual [6] AttributeValueAssertion,
379+
# present [7] AttributeType,
380+
# approxMatch [8] AttributeValueAssertion,
381+
# extensibleMatch [9] MatchingRuleAssertion
332382
# }
333383
#
334-
# SubstringFilter
384+
# SubstringFilter ::=
335385
# SEQUENCE {
336386
# type AttributeType,
337387
# SEQUENCE OF CHOICE {
@@ -340,6 +390,23 @@ def to_s
340390
# final [2] LDAPString
341391
# }
342392
# }
393+
#
394+
# MatchingRuleAssertion ::=
395+
# SEQUENCE {
396+
# matchingRule [1] MatchingRuleId OPTIONAL,
397+
# type [2] AttributeDescription OPTIONAL,
398+
# matchValue [3] AssertionValue,
399+
# dnAttributes [4] BOOLEAN DEFAULT FALSE
400+
# }
401+
#
402+
# Matching Rule Suffixes
403+
# Less than [.1] or .[lt]
404+
# Less than or equal to [.2] or [.lte]
405+
# Equality [.3] or [.eq] (default)
406+
# Greater than or equal to [.4] or [.gte]
407+
# Greater than [.5] or [.gt]
408+
# Substring [.6] or [.sub]
409+
#
343410
#++
344411
def to_ber
345412
case @op
@@ -381,6 +448,20 @@ def to_ber
381448
else # equality
382449
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
383450
end
451+
when :ex
452+
seq = []
453+
454+
unless @left =~ /^([-;\d\w]*)(:dn)?(:(\w+|[.\d\w]+))?$/
455+
raise Net::LDAP::LdapError, "Bad attribute #{@left}"
456+
end
457+
type, dn, rule = $1, $2, $4
458+
459+
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
460+
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
461+
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
462+
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
463+
464+
seq.to_ber_contextspecific(9)
384465
when :ge
385466
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
386467
when :le
@@ -407,10 +488,10 @@ def to_ber
407488
# some desired application-defined processing, and may return a
408489
# locally-meaningful object that will appear as a parameter in the :and,
409490
# :or and :not operations detailed below.
410-
#
411-
# A typical object to return from the user-supplied block is an array of
412-
# Net::LDAP::Filter objects.
413-
#
491+
#
492+
# A typical object to return from the user-supplied block is an array of
493+
# Net::LDAP::Filter objects.
494+
#
414495
# These are the possible values that may be passed to the user-supplied
415496
# block:
416497
# * :equalityMatch (the arguments will be an attribute name and a value
@@ -428,26 +509,26 @@ def to_ber
428509
# a recursive call to #execute, with the same block; and
429510
# * :not (one argument, which is an object returned from a recursive
430511
# call to #execute with the the same block.
431-
def execute(&block)
432-
case @op
433-
when :eq
434-
if @right == "*"
435-
yield :present, @left
436-
elsif @right.index '*'
437-
yield :substrings, @left, @right
438-
else
439-
yield :equalityMatch, @left, @right
440-
end
441-
when :ge
442-
yield :greaterOrEqual, @left, @right
443-
when :le
444-
yield :lessOrEqual, @left, @right
445-
when :or, :and
446-
yield @op, (@left.execute(&block)), (@right.execute(&block))
447-
when :not
448-
yield @op, (@left.execute(&block))
449-
end || []
450-
end
512+
def execute(&block)
513+
case @op
514+
when :eq
515+
if @right == "*"
516+
yield :present, @left
517+
elsif @right.index '*'
518+
yield :substrings, @left, @right
519+
else
520+
yield :equalityMatch, @left, @right
521+
end
522+
when :ge
523+
yield :greaterOrEqual, @left, @right
524+
when :le
525+
yield :lessOrEqual, @left, @right
526+
when :or, :and
527+
yield @op, (@left.execute(&block)), (@right.execute(&block))
528+
when :not
529+
yield @op, (@left.execute(&block))
530+
end || []
531+
end
451532

452533
##
453534
# This is a private helper method for dealing with chains of ANDs and ORs
@@ -588,9 +669,9 @@ def parse_paren_expression(scanner)
588669
# This parses a given expression inside of parentheses.
589670
def parse_filter_branch(scanner)
590671
scanner.scan(/\s*/)
591-
if token = scanner.scan(/[-\w_]+/)
672+
if token = scanner.scan(/[-\w\d_:.]*[\d\w]/)
592673
scanner.scan(/\s*/)
593-
if op = scanner.scan(/<=|>=|!=|=/)
674+
if op = scanner.scan(/<=|>=|!=|:=|=/)
594675
scanner.scan(/\s*/)
595676
if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/)
596677
# 20100313 AZ: Assumes that "(uid=george*)" is the same as
@@ -606,6 +687,8 @@ def parse_filter_branch(scanner)
606687
Net::LDAP::Filter.le(token, value)
607688
when ">="
608689
Net::LDAP::Filter.ge(token, value)
690+
when ":="
691+
Net::LDAP::Filter.ex(token, value)
609692
end
610693
end
611694
end

0 commit comments

Comments
 (0)