Major clean-up of Net::LDAP::Filter.

This commit is contained in:
Austin Ziegler 2010-03-21 00:38:12 -04:00 committed by Kaspar Schiess
parent afe43a5e58
commit 1dbf5908ae
2 changed files with 493 additions and 356 deletions

View file

@ -1,5 +1,24 @@
=== Net::LDAP NEXT / 2010-__-__ === Net::LDAP NEXT / 2010-__-__
* Added documentation: * SSL capabilities will be enabled or disabled based on whether we can load
OpenSSL successfully or not.
* Moved the core class extensions extensions from being in the Net::LDAP
hierarchy to the Net::BER hierarchy as most of the methods therein are
related to BER-encoding values. This will make extracting Net::BER from
Net::LDAP easier in the future.
* Net::LDAP::Filter changes:
* Filters can only be constructed using our custom constructors (eq, ge,
etc.). Cleaned up the code to reflect the private new.
* Fixed #to_ber to output a BER representation for :ne filters. Simplified
* the BER construction for substring matching.
* Added Filter.join(left, right), Filter.intersect(left, right), and
Filter.negate(filter) to match Filter#&, Filter#|, and Filter#~@ to prevent
those operators from having problems with the private new.
* Added Filter.present and Filter.present? aliases for the method previously
only known as Filter.pres.
* Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed
some of the regular expressions to be more canonically defined.
* Cleaned up the string representation of Filter objects.
* Added or revised documentation:
* Core class extension methods under Net::BER. * Core class extension methods under Net::BER.
* Extended unit testing: * Extended unit testing:
* Added some unit tests for the BER core extensions. * Added some unit tests for the BER core extensions.
@ -7,12 +26,6 @@
* Replaced calls to #to_a with calls to Kernel#Array; since Ruby 1.8.3, the * Replaced calls to #to_a with calls to Kernel#Array; since Ruby 1.8.3, the
default #to_a implementation has been deprecated and should be replaced default #to_a implementation has been deprecated and should be replaced
either with calls to Kernel#Array or [value].flatten(1). either with calls to Kernel#Array or [value].flatten(1).
* SSL capabilities will be enabled or disabled based on whether we can load
OpenSSL successfully or not.
* Moved the core class extensions extensions from being in the Net::LDAP
hierarchy to the Net::BER hierarchy as most of the methods therein are
related to BER-encoding values. This will make extracting Net::BER from
Net::LDAP easier in the future.
=== Net::LDAP 0.1.1 / 2010-03-18 === Net::LDAP 0.1.1 / 2010-03-18
* Fixing a critical problem with sockets. * Fixing a critical problem with sockets.

View file

@ -1,148 +1,321 @@
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. # Copyright (C) 2006 by Francis Cianfrocca and other contributors. All
# Rights Reserved.
# #
# Gmail: garbagecat10 # Gmail: garbagecat10
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify it
# it under the terms of the GNU General Public License as published by # under the terms of the GNU General Public License as published by the Free
# the Free Software Foundation; either version 2 of the License, or # Software Foundation; either version 2 of the License, or (at your option)
# (at your option) any later version. # any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
#---------------------------------------------------------------------------
# #
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
# #
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to:
# Free Software Foundation, Inc.
# 51 Franklin St, Fifth Floor
# Boston, MA 02110-1301
# USA
require 'strscan' ##
# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
module Net # this class is passed to Net::LDAP#search in the parameter :filter.
class LDAP
# Class Net::LDAP::Filter is used to constrain
# LDAP searches. An object of this class is
# passed to Net::LDAP#search in the parameter :filter.
# #
# Net::LDAP::Filter supports the complete set of search filters # Net::LDAP::Filter supports the complete set of search filters available in
# available in LDAP, including conjunction, disjunction and negation # LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254 # This class supplants the (infamous) RFC 2254 standard notation for
# standard notation for specifying LDAP search filters. # specifying LDAP search filters.
#--
# NOTE: This wording needs to change as we will be supporting LDAPv3 search
# filter strings (RFC 4515).
#++
# #
# Here's how to code the familiar "objectclass is present" filter: # Here's how to code the familiar "objectclass is present" filter:
# f = Net::LDAP::Filter.pres( "objectclass" ) # f = Net::LDAP::Filter.present("objectclass")
# The object returned by this code can be passed directly to #
# the <tt>:filter</tt> parameter of Net::LDAP#search. # The object returned by this code can be passed directly to the
# <tt>:filter</tt> parameter of Net::LDAP#search.
# #
# See the individual class and instance methods below for more examples. # See the individual class and instance methods below for more examples.
# class Net::LDAP::Filter
class Filter ##
# Known filter types.
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not ]
def initialize op, a, b def initialize(op, left, right) #:nodoc:
unless FilterTypes.include?(op)
raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
end
@op = op @op = op
@left = a @left = left
@right = b @right = right
end end
# #eq creates a filter object indicating that the value of class << self
# a paticular attribute must be either <i>present</i> or must # We don't want filters created except using our custom constructors.
# match a particular string. private :new
##
# Creates a Filter object indicating that the value of a particular
# attribute must either be present or match a particular string.
# #
# To specify that an attribute is "present" means that only # Specifying that an attribute is 'present' means only directory entries
# directory entries which contain a value for the particular # which contain a value for the particular attribute will be selected by
# attribute will be selected by the filter. This is useful # the filter. This is useful in case of optional attributes such as
# in case of optional attributes such as <tt>mail.</tt> # <tt>mail.</tt> Presence is indicated by giving the value "*" in the
# Presence is indicated by giving the value "*" in the second # second parameter to #eq. This example selects only entries that have
# parameter to #eq. This example selects only entries that have
# one or more values for <tt>sAMAccountName:</tt> # one or more values for <tt>sAMAccountName:</tt>
#
# f = Net::LDAP::Filter.eq("sAMAccountName", "*") # f = Net::LDAP::Filter.eq("sAMAccountName", "*")
# #
# To match a particular range of values, pass a string as the # To match a particular range of values, pass a string as the second
# second parameter to #eq. The string may contain one or more # parameter to #eq. The string may contain one or more "*" characters as
# "*" characters as wildcards: these match zero or more occurrences # wildcards: these match zero or more occurrences of any character. Full
# of any character. Full regular-expressions are <i>not</i> supported # regular-expressions are <i>not</i> supported due to limitations in the
# due to limitations in the underlying LDAP protocol. # underlying LDAP protocol. This example selects any entry with a
# This example selects any entry with a <tt>mail</tt> value containing # <tt>mail</tt> value containing the substring "anderson":
# the substring "anderson": #
# f = Net::LDAP::Filter.eq("mail", "*anderson*") # f = Net::LDAP::Filter.eq("mail", "*anderson*")
def eq(attribute, value)
new(:eq, attribute, value)
end
##
# Creates a Filter object indicating that a particular attribute value
# is either not present or does not match a particular string; see
# Filter::eq for more information.
def ne(attribute, value)
new(:ne, attribute, value)
end
##
# Creates a Filter object indicating that a particular attribute value
# is greater than or equal to the specified value.
def ge(attribute, value)
new(:ge, attribute, value)
end
##
# Creates a Filter object indicating that a particular attribute value
# is less than or equal to the specified value.
def le(attribute, value)
new(:le, attribute, value)
end
##
# Joins two or more filters so that all conditions must be true. Calling
# <tt>Filter.join(left, right)</tt> is the same as <tt>left &
# right</tt>.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet both conditions above.
# z = Net::LDAP::Filter.join(x, y)
def join(left, right)
new(:and, left, right)
end
##
# Creates a disjoint comparison between two or more filters. Selects
# entries where either the left or right side are true. Calling
# <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
# right</tt>.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet either condition above.
# z = x | y
def intersect(left, right)
new(:or, left, right)
end
##
# Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
# as <tt>~filter</tt>.
#
# # Selects only entries that do not have an <tt>objectclass</tt>
# # attribute.
# x = ~Net::LDAP::Filter.present("objectclass")
def negate(filter)
new(:not, filter, nil)
end
##
# This is a synonym for #eq(attribute, "*"). Also known as #present and
# #pres.
def present?(attribute)
eq(attribute, "*")
end
alias_method :present, :present?
alias_method :pres, :present?
##
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
# object. The incoming BER object most likely came to us by parsing an
# LDAP searchRequest PDU. See also the comments under #to_ber, including
# the grammar snippet from the RFC.
#-- #--
# Removed gt and lt. They ain't in the standard! # We're hardcoding the BER constants from the RFC. These should be
# # broken out insto constants.
def Filter::eq attribute, value; Filter.new :eq, attribute, value; end def parse_ber(ber)
def Filter::ne attribute, value; Filter.new :ne, attribute, value; end case ber.ber_identifier
#def Filter::gt attribute, value; Filter.new :gt, attribute, value; end when 0xa0 # context-specific constructed 0, "and"
#def Filter::lt attribute, value; Filter.new :lt, attribute, value; end ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
def Filter::ge attribute, value; Filter.new :ge, attribute, value; end when 0xa1 # context-specific constructed 1, "or"
def Filter::le attribute, value; Filter.new :le, attribute, value; end ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
when 0xa2 # context-specific constructed 2, "not"
~parse_ber(ber.first)
when 0xa3 # context-specific constructed 3, "equalityMatch"
if ber.last == "*"
else
eq(ber.first, ber.last)
end
when 0xa4 # context-specific constructed 4, "substring"
str = ""
final = false
ber.last.each { |b|
case b.ber_identifier
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
str += b
when 0x81 # context-specific primitive 0, SubstringFilter "any"
str += "*#{b}"
when 0x82 # context-specific primitive 0, SubstringFilter "final"
str += "*#{b}"
final = true
end
}
str += "*" unless final
eq(ber.first.to_s, str)
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
ge(ber.first.to_s, ber.last.to_s)
when 0xa6 # context-specific constructed 5, "lessOrEqual"
le(ber.first.to_s, ber.last.to_s)
when 0x87 # context-specific primitive 7, "present"
# call to_s to get rid of the BER-identifiedness of the incoming string.
present?(ber.to_s)
else
raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
end
end
# #pres( attribute ) is a synonym for #eq( attribute, "*" ) ##
# # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
def Filter::pres attribute; Filter.eq attribute, "*"; end # to a Net::LDAP::Filter.
def construct(ldap_filter_string)
FilterParser.parse(ldap_filter_string)
end
alias_method :from_rfc2254, :construct
alias_method :from_rfc4515, :construct
# operator & ("AND") is used to conjoin two or more filters. ##
# This expression will select only entries that have an <tt>objectclass</tt> # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
# attribute AND have a <tt>mail</tt> attribute that begins with "George": # object.
# f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
#
def & filter; Filter.new :and, self, filter; end
# operator | ("OR") is used to disjoin two or more filters.
# This expression will select entries that have either an <tt>objectclass</tt>
# attribute OR a <tt>mail</tt> attribute that begins with "George":
# f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
#
def | filter; Filter.new :or, self, filter; end
#
# operator ~ ("NOT") is used to negate a filter.
# This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
# attribute:
# f = ~ Net::LDAP::Filter.pres( "objectclass" )
#
#-- #--
# This operator can't be !, evidently. Try it. # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
# Removed GT and LT. They're not in the RFC. # filter types. Could pull them out into a constant.
def ~@; Filter.new :not, self, nil; end #++
def parse_ldap_filter(obj)
case obj.ber_identifier
when 0x87 # present. context-specific primitive 7.
eq(obj.to_s, "*")
when 0xa3 # equalityMatch. context-specific constructed 3.
eq(obj[0], obj[1])
else
raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
end
end
end
##
# Joins two or more filters so that all conditions must be true.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet both conditions above.
# z = x & y
def &(filter)
self.class.join(self, filter)
end
##
# Creates a disjoint comparison between two or more filters. Selects
# entries where either the left or right side are true.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet either condition above.
# z = x | y
def |(filter)
self.class.intersect(self, filter)
end
##
# Negates a filter.
#
# # Selects only entries that do not have an <tt>objectclass</tt>
# # attribute.
# x = ~Net::LDAP::Filter.present("objectclass")
def ~@
self.class.negate(self)
end
##
# Equality operator for filters, useful primarily for constructing unit tests. # Equality operator for filters, useful primarily for constructing unit tests.
def == filter def ==(filter)
# 20100320 AZ: We need to come up with a better way of doing this. This
# 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_s def to_raw_rfc2254
case @op case @op
when :ne when :ne
"(!(#{@left}=#{@right}))" "!(#{@left}=#{@right})"
when :eq when :eq
"(#{@left}=#{@right})" "#{@left}=#{@right}"
#when :gt
# "#{@left}>#{@right}"
#when :lt
# "#{@left}<#{@right}"
when :ge when :ge
"#{@left}>=#{@right}" "#{@left}>=#{@right}"
when :le when :le
"#{@left}<=#{@right}" "#{@left}<=#{@right}"
when :and when :and
"(&(#{@left})(#{@right}))" "&(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
when :or when :or
"(|(#{@left})(#{@right}))" "|(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})"
when :not when :not
"(!(#{@left}))" "!(#{@left.__send__(:to_raw_rfc2254)})"
else
raise "invalid or unsupported operator in LDAP Filter"
end end
end end
private :to_raw_rfc2254
##
# Converts the Filter object to an RFC 2254-compatible text format.
def to_rfc2254
"(#{to_raw_rfc2254})"
end
def to_s
to_rfc2254
end
##
# Converts the filter to BER format.
#-- #--
# to_ber # to_ber
# Filter ::= # Filter ::=
@ -167,49 +340,53 @@ class Filter
# final [2] LDAPString # final [2] LDAPString
# } # }
# } # }
# #++
# Parsing substrings is a little tricky.
# We use the split method to break a string into substrings
# delimited by the * (star) character. But we also need
# to know whether there is a star at the head and tail
# of the string. A Ruby particularity comes into play here:
# if you split on * and the first character of the string is
# a star, then split will return an array whose first element
# is an _empty_ string. But if the _last_ character of the
# string is star, then split will return an array that does
# _not_ add an empty string at the end. So we have to deal
# with all that specifically.
#
def to_ber def to_ber
case @op case @op
when :eq when :eq
if @right == "*" # present if @right == "*" # presence test
@left.to_s.to_ber_contextspecific 7 @left.to_s.to_ber_contextspecific(7)
elsif @right =~ /[\*]/ #substring elsif @right =~ /[*]/ # substring
ary = @right.split( /[\*]+/ ) # Parsing substrings is a little tricky. We use String#split to
final_star = @right =~ /[\*]$/ # break a string into substrings delimited by the * (star)
initial_star = ary.first == "" and ary.shift # character. But we also need to know whether there is a star at the
# head and tail of the string, so we use a limit parameter value of
# -1: "If negative, there is no limit to the number of fields
# returned, and trailing null fields are not suppressed."
#
# 20100320 AZ: This is much simpler than the previous verison. Also,
# unnecessary regex escaping has been removed.
seq = [] ary = @right.split(/[*]+/, -1)
unless initial_star
seq << ary.shift.to_ber_contextspecific(0) if ary.first.empty?
first = nil
ary.shift
else
first = ary.shift.to_ber_contextspecific(0)
end end
n_any_strings = ary.length - (final_star ? 0 : 1)
#p n_any_strings if ary.last.empty?
n_any_strings.times { last = nil
seq << ary.shift.to_ber_contextspecific(1) ary.pop
} else
unless final_star last = ary.pop.to_ber_contextspecific(2)
seq << ary.shift.to_ber_contextspecific(2)
end end
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
seq = ary.map { |e| e.to_ber_contextspecific(1) }
seq.unshift first if first
seq.push last if last
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
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 :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
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6 [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
when :ne
[self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
when :and when :and
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
ary.map {|a| a.to_ber}.to_ber_contextspecific(0) ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
@ -217,96 +394,41 @@ class Filter
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
ary.map {|a| a.to_ber}.to_ber_contextspecific(1) ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
when :not when :not
[@left.to_ber].to_ber_contextspecific 2 [@left.to_ber].to_ber_contextspecific(2)
else
# ERROR, we'll return objectclass=* to keep things from blowing up,
# but that ain't a good answer and we need to kick out an error of some kind.
raise "unimplemented search filter"
end end
end end
def unescape(right) ##
right.gsub(/\\([a-fA-F\d]{2,2})/) do # Perform filter operations against a user-supplied block. This is useful
[$1.hex].pack("U") # when implementing an LDAP directory server. The caller's block will be
end # called with two arguments: first, a symbol denoting the "operation" of
end # the filter; and second, an array consisting of arguments to the
# operation. The user-supplied block (which is MANDATORY) should perform
# some desired application-defined processing, and may return a
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter # locally-meaningful object that will appear as a parameter in the :and,
# object. The incoming BER object most likely came to us by parsing an # :or and :not operations detailed below.
# LDAP searchRequest PDU.
# Cf the comments under #to_ber, including the grammar snippet from the RFC.
#--
# We're hardcoding the BER constants from the RFC. Ought to break them out
# into constants.
#
def Filter::parse_ber ber
case ber.ber_identifier
when 0xa0 # context-specific constructed 0, "and"
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
when 0xa1 # context-specific constructed 1, "or"
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
when 0xa2 # context-specific constructed 2, "not"
~ Filter::parse_ber( ber.first )
when 0xa3 # context-specific constructed 3, "equalityMatch"
if ber.last == "*"
else
Filter.eq( ber.first, ber.last )
end
when 0xa4 # context-specific constructed 4, "substring"
str = ""
final = false
ber.last.each {|b|
case b.ber_identifier
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
raise "unrecognized substring filter, bad initial" if str.length > 0
str += b
when 0x81 # context-specific primitive 0, SubstringFilter "any"
str += "*#{b}"
when 0x82 # context-specific primitive 0, SubstringFilter "final"
str += "*#{b}"
final = true
end
}
str += "*" unless final
Filter.eq( ber.first.to_s, str )
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
Filter.ge( ber.first.to_s, ber.last.to_s )
when 0xa6 # context-specific constructed 5, "lessOrEqual"
Filter.le( ber.first.to_s, ber.last.to_s )
when 0x87 # context-specific primitive 7, "present"
# call to_s to get rid of the BER-identifiedness of the incoming string.
Filter.pres( ber.to_s )
else
raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
end
end
# Perform filter operations against a user-supplied block. This is useful when implementing
# an LDAP directory server. The caller's block will be called with two arguments: first, a
# symbol denoting the "operation" of the filter; and second, an array consisting of arguments
# to the operation. The user-supplied block (which is MANDATORY) should perform some desired
# application-defined processing, and may return a locally-meaningful object that will appear
# as a parameter in the :and, :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 block: # These are the possible values that may be passed to the user-supplied
# :equalityMatch (the arguments will be an attribute name and a value to be matched); # block:
# :substrings (two arguments: an attribute name and a value containing one or more * characters); # * :equalityMatch (the arguments will be an attribute name and a value
# :present (one argument: an attribute name); # to be matched);
# :greaterOrEqual (two arguments: an attribute name and a value to be compared against); # * :substrings (two arguments: an attribute name and a value containing
# :lessOrEqual (two arguments: an attribute name and a value to be compared against); # one or more "*" characters);
# :and (two or more arguments, each of which is an object returned from a recursive call # * :present (one argument: an attribute name);
# to #execute, with the same block; # * :greaterOrEqual (two arguments: an attribute name and a value to be
# :or (two or more arguments, each of which is an object returned from a recursive call # compared against);
# to #execute, with the same block; # * :lessOrEqual (two arguments: an attribute name and a value to be
# :not (one argument, which is an object returned from a recursive call to #execute with the # compared against);
# the same block. # * :and (two or more arguments, each of which is an object returned
# # from a recursive call to #execute, with the same block;
def execute &block # * :or (two or more arguments, each of which is an object returned from
# a recursive call to #execute, with the same block; and
# * :not (one argument, which is an object returned from a recursive
# call to #execute with the the same block.
def execute(&block)
case @op case @op
when :eq when :eq
if @right == "*" if @right == "*"
@ -327,16 +449,13 @@ class Filter
end || [] end || []
end end
##
#--
# coalesce
# 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
# that are longer than two. If BOTH of our branches are of the specified # that are longer than two. If BOTH of our branches are of the specified
# type of joining operator, then return both of them as an array (calling # type of joining operator, then return both of them as an array (calling
# coalesce recursively). If they're not, then return an array consisting # coalesce recursively). If they're not, then return an array consisting
# only of self. # only of self.
# def coalesce(operator) #:nodoc:
def coalesce operator
if @op == operator if @op == operator
[@left.coalesce(operator), @right.coalesce(operator)] [@left.coalesce(operator), @right.coalesce(operator)]
else else
@ -344,33 +463,13 @@ class Filter
end end
end end
##
#--
# We get a Ruby object which comes from parsing an RFC-1777 "Filter"
# object. Convert it to a Net::LDAP::Filter.
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
# filter types. Could pull them out into a constant.
#
def Filter::parse_ldap_filter obj
case obj.ber_identifier
when 0x87 # present. context-specific primitive 7.
Filter.eq( obj.to_s, "*" )
when 0xa3 # equalityMatch. context-specific constructed 3.
Filter.eq( obj[0], obj[1] )
else
raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
end
end
#-- #--
# We got a hash of attribute values. # We got a hash of attribute values.
# Do we match the attributes? # Do we match the attributes?
# Return T/F, and call match recursively as necessary. # Return T/F, and call match recursively as necessary.
def match entry #++
def match(entry)
case @op case @op
when :eq when :eq
if @right == "*" if @right == "*"
@ -379,114 +478,139 @@ class Filter
l = entry[@left] and l = Array(l) and l.index(@right) l = entry[@left] and l = Array(l) and l.index(@right)
end end
else else
raise LdapError.new( "unknown filter type in match: #{@op}" ) raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
end end
end end
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) ##
# to a Net::LDAP::Filter. # Converts escaped characters (e.g., "\\28") to unescaped characters
def self.construct ldap_filter_string # ("(").
FilterParser.new(ldap_filter_string).filter def unescape(right)
right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
end end
private :unescape
# Synonym for #construct. ##
# to a Net::LDAP::Filter. # Parses RFC 2254-style string representations of LDAP filters into Filter
def self.from_rfc2254 ldap_filter_string # object hierarchies.
construct ldap_filter_string
end
end # class Net::LDAP::Filter
class FilterParser #:nodoc: class FilterParser #:nodoc:
##
# The constructed filter.
attr_reader :filter attr_reader :filter
def initialize str class << self
@filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" ) private :new
##
# Construct a filter tree from the provided string and return it.
def parse(ldap_filter_string)
new(ldap_filter_string).filter
end
end end
def parse scanner def initialize(str)
require 'strscan' # Don't load strscan until we need it.
@filter = parse(StringScanner.new(str))
raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
end
##
# Parse the string contained in the StringScanner provided. Parsing
# tries to parse a standalone expression first. If that fails, it tries
# to parse a parenthesized expression.
def parse(scanner)
parse_filter_branch(scanner) or parse_paren_expression(scanner) parse_filter_branch(scanner) or parse_paren_expression(scanner)
end end
private :parse
def parse_paren_expression scanner ##
# Join ("&") and intersect ("|") operations are presented in branches.
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
# test1 and test2. Each of these is parsed separately and then pushed
# into a branch array for filter merging using the parent operation.
#
# This method parses the branch text out into an array of filter
# objects.
def parse_branches(scanner)
branches = []
while branch = parse_paren_expression(scanner)
branches << branch
end
branches
end
private :parse_branches
##
# Join ("&") and intersect ("|") operations are presented in branches.
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
# test1 and test2. Each of these is parsed separately and then pushed
# into a branch array for filter merging using the parent operation.
#
# This method calls #parse_branches to generate the branch list and then
# merges them into a single Filter tree by calling the provided
# operation.
def merge_branches(op, scanner)
filter = nil
branches = parse_branches(scanner)
if branches.size >= 2
filter = branches.shift
while not branches.empty?
filter = filter.__send__(op, branches.shift)
end
end
filter
end
private :merge_branches
def parse_paren_expression(scanner)
if scanner.scan(/\s*\(\s*/) if scanner.scan(/\s*\(\s*/)
b = if scanner.scan(/\s*\&\s*/) expr = if scanner.scan(/\s*\&\s*/)
a = nil merge_branches(:&, scanner)
branches = []
while br = parse_paren_expression(scanner)
branches << br
end
if branches.length >= 2
a = branches.shift
while branches.length > 0
a = a & branches.shift
end
a
end
elsif scanner.scan(/\s*\|\s*/) elsif scanner.scan(/\s*\|\s*/)
# TODO: DRY! merge_branches(:|, scanner)
a = nil
branches = []
while br = parse_paren_expression(scanner)
branches << br
end
if branches.length >= 2
a = branches.shift
while branches.length > 0
a = a | branches.shift
end
a
end
elsif scanner.scan(/\s*\!\s*/) elsif scanner.scan(/\s*\!\s*/)
br = parse_paren_expression(scanner) br = parse_paren_expression(scanner)
if br ~br if br
~ br
end
else else
parse_filter_branch(scanner) parse_filter_branch(scanner)
end end
if b and scanner.scan( /\s*\)\s*/ ) if expr and scanner.scan(/\s*\)\s*/)
b expr
end end
end end
end end
private :parse_paren_expression
# Added a greatly-augmented filter contributed by Andre Nathan ##
# for detecting special characters in values. (15Aug06) # This parses a given expression inside of parentheses.
# Added blanks to the attribute filter (26Oct06) 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_]+/)
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\*\.]+/ ) (ORG) if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/)
#if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou # 20100313 AZ: Assumes that "(uid=george*)" is the same as
if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ ) # "(uid=george* )". The standard doesn't specify, but I can find
# no examples that suggest otherwise.
value.strip!
case op case op
when "=" when "="
Filter.eq( token, value ) Net::LDAP::Filter.eq(token, value)
when "!=" when "!="
Filter.ne( token, value ) Net::LDAP::Filter.ne(token, value)
when "<"
Filter.lt( token, value )
when "<=" when "<="
Filter.le( token, value ) Net::LDAP::Filter.le(token, value)
when ">"
Filter.gt( token, value )
when ">=" when ">="
Filter.ge( token, value ) Net::LDAP::Filter.ge(token, value)
end end
end end
end end
end end
end end
private :parse_filter_branch
end # class Net::LDAP::FilterParser end # class Net::LDAP::FilterParser
end # class Net::LDAP::Filter
end # class Net::LDAP
end # module Net