From 1dbf5908aeabc328e84a94066a50e26074c4cf1d Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Sun, 21 Mar 2010 00:38:12 -0400 Subject: [PATCH] Major clean-up of Net::LDAP::Filter. --- History.txt | 27 +- lib/net/ldap/filter.rb | 822 ++++++++++++++++++++++++----------------- 2 files changed, 493 insertions(+), 356 deletions(-) diff --git a/History.txt b/History.txt index ae1c853..299bba3 100644 --- a/History.txt +++ b/History.txt @@ -1,5 +1,24 @@ === 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. * Extended unit testing: * 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 default #to_a implementation has been deprecated and should be replaced 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 * Fixing a critical problem with sockets. diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index fe03660..31dbaf5 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -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 # -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) 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 free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# 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: +# Free Software Foundation, Inc. +# 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301 +# USA -require 'strscan' - -module Net -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. +## +# 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 -# available in LDAP, including conjunction, disjunction and negation -# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254 -# standard notation for specifying LDAP search filters. +# Net::LDAP::Filter supports the complete set of search filters available in +# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT). +# This class supplants the (infamous) RFC 2254 standard notation for +# 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: -# f = Net::LDAP::Filter.pres( "objectclass" ) -# The object returned by this code can be passed directly to -# the :filter parameter of Net::LDAP#search. +# f = Net::LDAP::Filter.present("objectclass") +# +# The object returned by this code can be passed directly to the +# :filter parameter of Net::LDAP#search. # # See the individual class and instance methods below for more examples. -# -class Filter +class Net::LDAP::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 - @left = a - @right = b + @left = left + @right = right end - # #eq creates a filter object indicating that the value of - # a paticular attribute must be either present or must - # match a particular string. - # - # To specify that an attribute is "present" means that only - # directory entries which contain a value for the particular - # attribute will be selected by the filter. This is useful - # in case of optional attributes such as mail. - # Presence is indicated by giving the value "*" in the second - # parameter to #eq. This example selects only entries that have - # one or more values for sAMAccountName: - # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" ) - # - # To match a particular range of values, pass a string as the - # second parameter to #eq. The string may contain one or more - # "*" characters as wildcards: these match zero or more occurrences - # of any character. Full regular-expressions are not supported - # due to limitations in the underlying LDAP protocol. - # This example selects any entry with a mail value containing - # the substring "anderson": - # f = Net::LDAP::Filter.eq( "mail", "*anderson*" ) - #-- - # Removed gt and lt. They ain't in the standard! - # - def Filter::eq attribute, value; Filter.new :eq, attribute, value; end - def Filter::ne attribute, value; Filter.new :ne, attribute, value; end - #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end - #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end - def Filter::ge attribute, value; Filter.new :ge, attribute, value; end - def Filter::le attribute, value; Filter.new :le, attribute, value; end + class << self + # We don't want filters created except using our custom constructors. + private :new - # #pres( attribute ) is a synonym for #eq( attribute, "*" ) - # - def Filter::pres attribute; Filter.eq attribute, "*"; end + ## + # Creates a Filter object indicating that the value of a particular + # attribute must either be present or match a particular string. + # + # Specifying that an attribute is 'present' means only directory entries + # which contain a value for the particular attribute will be selected by + # the filter. This is useful in case of optional attributes such as + # mail. Presence is indicated by giving the value "*" in the + # second parameter to #eq. This example selects only entries that have + # one or more values for sAMAccountName: + # + # f = Net::LDAP::Filter.eq("sAMAccountName", "*") + # + # To match a particular range of values, pass a string as the second + # parameter to #eq. The string may contain one or more "*" characters as + # wildcards: these match zero or more occurrences of any character. Full + # regular-expressions are not supported due to limitations in the + # underlying LDAP protocol. This example selects any entry with a + # mail value containing the substring "anderson": + # + # f = Net::LDAP::Filter.eq("mail", "*anderson*") + def eq(attribute, value) + new(:eq, attribute, value) + end - # operator & ("AND") is used to conjoin two or more filters. - # This expression will select only entries that have an objectclass - # attribute AND have a mail attribute that begins with "George": - # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" ) - # - def & filter; Filter.new :and, self, filter; 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 - # operator | ("OR") is used to disjoin two or more filters. - # This expression will select entries that have either an objectclass - # attribute OR a mail 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 + ## + # 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 - # - # operator ~ ("NOT") is used to negate a filter. - # This expression will select only entries that do not have an objectclass - # attribute: - # f = ~ Net::LDAP::Filter.pres( "objectclass" ) - # - #-- - # This operator can't be !, evidently. Try it. - # Removed GT and LT. They're not in the RFC. - def ~@; Filter.new :not, self, nil; end + ## + # Joins two or more filters so that all conditions must be true. Calling + # Filter.join(left, right) is the same as left & + # right. + # + # # Selects only entries that have an objectclass attribute. + # x = Net::LDAP::Filter.present("objectclass") + # # Selects only entries that have a mail 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 - # Equality operator for filters, useful primarily for constructing unit tests. - def == filter + ## + # Creates a disjoint comparison between two or more filters. Selects + # entries where either the left or right side are true. Calling + # Filter.intersect(left, right) is the same as left | + # right. + # + # # Selects only entries that have an objectclass attribute. + # x = Net::LDAP::Filter.present("objectclass") + # # Selects only entries that have a mail 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 Fitler.negate(filter) i s the same + # as ~filter. + # + # # Selects only entries that do not have an objectclass + # # 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. + #-- + # We're hardcoding the BER constants from the RFC. These should be + # broken out insto constants. + def parse_ber(ber) + case ber.ber_identifier + when 0xa0 # context-specific constructed 0, "and" + ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj } + when 0xa1 # context-specific constructed 1, "or" + 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 + + ## + # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) + # 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 + + ## + # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter + # object. + #-- + # TODO, we're hardcoding the RFC-1777 BER-encodings of the various + # filter types. Could pull them out into a constant. + #++ + 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 objectclass attribute. + # x = Net::LDAP::Filter.present("objectclass") + # # Selects only entries that have a mail 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 objectclass attribute. + # x = Net::LDAP::Filter.present("objectclass") + # # Selects only entries that have a mail 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 objectclass + # # attribute. + # x = ~Net::LDAP::Filter.present("objectclass") + def ~@ + self.class.negate(self) + end + + ## + # Equality operator for filters, useful primarily for constructing unit tests. + def ==(filter) + # 20100320 AZ: We need to come up with a better way of doing this. This + # is just nasty. str = "[@op,@left,@right]" self.instance_eval(str) == filter.instance_eval(str) end - def to_s + def to_raw_rfc2254 case @op when :ne - "(!(#{@left}=#{@right}))" + "!(#{@left}=#{@right})" when :eq - "(#{@left}=#{@right})" - #when :gt - # "#{@left}>#{@right}" - #when :lt - # "#{@left}<#{@right}" + "#{@left}=#{@right}" when :ge "#{@left}>=#{@right}" when :le "#{@left}<=#{@right}" when :and - "(&(#{@left})(#{@right}))" + "&(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})" when :or - "(|(#{@left})(#{@right}))" + "|(#{@left.__send__(:to_raw_rfc2254)})(#{@right.__send__(:to_raw_rfc2254)})" when :not - "(!(#{@left}))" - else - raise "invalid or unsupported operator in LDAP Filter" + "!(#{@left.__send__(:to_raw_rfc2254)})" 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 # Filter ::= @@ -167,146 +340,95 @@ class Filter # 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 case @op when :eq - if @right == "*" # present - @left.to_s.to_ber_contextspecific 7 - elsif @right =~ /[\*]/ #substring - ary = @right.split( /[\*]+/ ) - final_star = @right =~ /[\*]$/ - initial_star = ary.first == "" and ary.shift + if @right == "*" # presence test + @left.to_s.to_ber_contextspecific(7) + elsif @right =~ /[*]/ # substring + # Parsing substrings is a little tricky. We use String#split 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, 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 = [] - unless initial_star - seq << ary.shift.to_ber_contextspecific(0) + ary = @right.split(/[*]+/, -1) + + if ary.first.empty? + first = nil + ary.shift + else + first = ary.shift.to_ber_contextspecific(0) end - n_any_strings = ary.length - (final_star ? 0 : 1) - #p n_any_strings - n_any_strings.times { - seq << ary.shift.to_ber_contextspecific(1) - } - unless final_star - seq << ary.shift.to_ber_contextspecific(2) + + if ary.last.empty? + last = nil + ary.pop + else + last = ary.pop.to_ber_contextspecific(2) end - [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4 - else #equality - [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3 + + 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 + [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3) end 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 - [@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 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) when :or 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 - [@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" + [@left.to_ber].to_ber_contextspecific(2) end end - def unescape(right) - right.gsub(/\\([a-fA-F\d]{2,2})/) do - [$1.hex].pack("U") - end - end - - - # 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. - # 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. + ## + # 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 # Net::LDAP::Filter objects. # - # These are the possible values that may be passed to the user-supplied block: - # :equalityMatch (the arguments will be an attribute name and a value to be matched); - # :substrings (two arguments: an attribute name and a value containing one or more * characters); - # :present (one argument: an attribute name); - # :greaterOrEqual (two arguments: an attribute name and a value to be compared against); - # :lessOrEqual (two arguments: an attribute name and a value to be compared against); - # :and (two or more arguments, each of which is an object returned from a recursive call - # to #execute, with the same block; - # :or (two or more arguments, each of which is an object returned from a recursive call - # to #execute, with the same block; - # :not (one argument, which is an object returned from a recursive call to #execute with the - # the same block. - # - def execute &block + # These are the possible values that may be passed to the user-supplied + # block: + # * :equalityMatch (the arguments will be an attribute name and a value + # to be matched); + # * :substrings (two arguments: an attribute name and a value containing + # one or more "*" characters); + # * :present (one argument: an attribute name); + # * :greaterOrEqual (two arguments: an attribute name and a value to be + # compared against); + # * :lessOrEqual (two arguments: an attribute name and a value to be + # compared against); + # * :and (two or more arguments, each of which is an object returned + # from a recursive call to #execute, with the same 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 when :eq if @right == "*" @@ -327,50 +449,27 @@ class Filter end || [] end - - #-- - # coalesce + ## # 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 # type of joining operator, then return both of them as an array (calling # coalesce recursively). If they're not, then return an array consisting # only of self. - # - def coalesce operator + def coalesce(operator) #:nodoc: if @op == operator - [@left.coalesce( operator ), @right.coalesce( operator )] + [@left.coalesce(operator), @right.coalesce(operator)] else [self] 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. # Do we match the attributes? # Return T/F, and call match recursively as necessary. - def match entry + #++ + def match(entry) case @op when :eq if @right == "*" @@ -379,114 +478,139 @@ class Filter l = entry[@left] and l = Array(l) and l.index(@right) end else - raise LdapError.new( "unknown filter type in match: #{@op}" ) + raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}" end end - # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254) - # to a Net::LDAP::Filter. - def self.construct ldap_filter_string - FilterParser.new(ldap_filter_string).filter + ## + # Converts escaped characters (e.g., "\\28") to unescaped characters + # ("("). + def unescape(right) + right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") } end + private :unescape - # Synonym for #construct. - # to a Net::LDAP::Filter. - def self.from_rfc2254 ldap_filter_string - construct ldap_filter_string - end + ## + # Parses RFC 2254-style string representations of LDAP filters into Filter + # object hierarchies. + class FilterParser #:nodoc: + ## + # The constructed filter. + attr_reader :filter -end # class Net::LDAP::Filter + class << self + private :new - - -class FilterParser #:nodoc: - - attr_reader :filter - - def initialize str - @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" ) - end - - def parse scanner - parse_filter_branch(scanner) or parse_paren_expression(scanner) - end - - def parse_paren_expression scanner - if scanner.scan(/\s*\(\s*/) - b = if scanner.scan(/\s*\&\s*/) - 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*/) - # TODO: DRY! - 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*/) - br = parse_paren_expression(scanner) - if br - ~ br - end - else - parse_filter_branch( scanner ) - end - - if b and scanner.scan( /\s*\)\s*/ ) - b + ## + # Construct a filter tree from the provided string and return it. + def parse(ldap_filter_string) + new(ldap_filter_string).filter end end - end - # Added a greatly-augmented filter contributed by Andre Nathan - # for detecting special characters in values. (15Aug06) - # Added blanks to the attribute filter (26Oct06) - def parse_filter_branch scanner - scanner.scan(/\s*/) - if token = scanner.scan( /[\w\-_]+/ ) + 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) + end + private :parse + + ## + # Join ("&") and intersect ("|") operations are presented in branches. + # That is, the expression (&(test1)(test2) 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 (&(test1)(test2) 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*/) + expr = if scanner.scan(/\s*\&\s*/) + merge_branches(:&, scanner) + elsif scanner.scan(/\s*\|\s*/) + merge_branches(:|, scanner) + elsif scanner.scan(/\s*\!\s*/) + br = parse_paren_expression(scanner) + ~br if br + else + parse_filter_branch(scanner) + end + + if expr and scanner.scan(/\s*\)\s*/) + expr + end + end + end + private :parse_paren_expression + + ## + # This parses a given expression inside of parentheses. + def parse_filter_branch(scanner) scanner.scan(/\s*/) - if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ ) + if token = scanner.scan(/[-\w_]+/) scanner.scan(/\s*/) - #if value = scanner.scan( /[\w\*\.]+/ ) (ORG) - #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou - if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ ) - case op - when "=" - Filter.eq( token, value ) - when "!=" - Filter.ne( token, value ) - when "<" - Filter.lt( token, value ) - when "<=" - Filter.le( token, value ) - when ">" - Filter.gt( token, value ) - when ">=" - Filter.ge( token, value ) + if op = scanner.scan(/<=|>=|!=|=/) + scanner.scan(/\s*/) + if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!\s]|\\[a-fA-F\d]{2})+/) + # 20100313 AZ: Assumes that "(uid=george*)" is the same as + # "(uid=george* )". The standard doesn't specify, but I can find + # no examples that suggest otherwise. + value.strip! + case op + when "=" + Net::LDAP::Filter.eq(token, value) + when "!=" + Net::LDAP::Filter.ne(token, value) + when "<=" + Net::LDAP::Filter.le(token, value) + when ">=" + Net::LDAP::Filter.ge(token, value) + end end end end end - end - -end # class Net::LDAP::FilterParser - -end # class Net::LDAP -end # module Net + private :parse_filter_branch + end # class Net::LDAP::FilterParser +end # class Net::LDAP::Filter