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-__-__
* 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.

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
#
# 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 <tt>:filter</tt> parameter of Net::LDAP#search.
# 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.
#
# 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 <i>present</i> or must
# match a particular string.
class << self
# We don't want filters created except using our custom constructors.
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
# 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 <tt>mail.</tt>
# Presence is indicated by giving the value "*" in the second
# parameter to #eq. This example selects only entries that have
# 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
# <tt>mail.</tt> 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 <tt>sAMAccountName:</tt>
# 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 <i>not</i> supported
# due to limitations in the underlying LDAP protocol.
# This example selects any entry with a <tt>mail</tt> value containing
# the substring "anderson":
# f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
# 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 <i>not</i> supported due to limitations in the
# underlying LDAP protocol. This example selects any entry with a
# <tt>mail</tt> value containing the substring "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!
#
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
# 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
# #pres( attribute ) is a synonym for #eq( attribute, "*" )
#
def Filter::pres attribute; Filter.eq attribute, "*"; 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
# operator & ("AND") is used to conjoin two or more filters.
# This expression will select only entries that have an <tt>objectclass</tt>
# attribute AND have 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 :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" )
#
##
# Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
# object.
#--
# 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
# 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 <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.
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]"
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
end # class Net::LDAP::Filter
class FilterParser #:nodoc:
##
# Parses RFC 2254-style string representations of LDAP filters into Filter
# object hierarchies.
class FilterParser #:nodoc:
##
# The constructed filter.
attr_reader :filter
def initialize str
@filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
class << self
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
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)
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*/)
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
expr = if scanner.scan(/\s*\&\s*/)
merge_branches(:&, scanner)
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
merge_branches(:|, scanner)
elsif scanner.scan(/\s*\!\s*/)
br = parse_paren_expression(scanner)
if br
~ br
end
~br if br
else
parse_filter_branch( scanner )
parse_filter_branch(scanner)
end
if b and scanner.scan( /\s*\)\s*/ )
b
if expr and scanner.scan(/\s*\)\s*/)
expr
end
end
end
private :parse_paren_expression
# 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
##
# This parses a given expression inside of parentheses.
def parse_filter_branch(scanner)
scanner.scan(/\s*/)
if token = scanner.scan( /[\w\-_]+/ )
if token = scanner.scan(/[-\w_]+/)
scanner.scan(/\s*/)
if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
if op = scanner.scan(/<=|>=|!=|=/)
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})+/ )
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 "="
Filter.eq( token, value )
Net::LDAP::Filter.eq(token, value)
when "!="
Filter.ne( token, value )
when "<"
Filter.lt( token, value )
Net::LDAP::Filter.ne(token, value)
when "<="
Filter.le( token, value )
when ">"
Filter.gt( token, value )
Net::LDAP::Filter.le(token, value)
when ">="
Filter.ge( token, value )
Net::LDAP::Filter.ge(token, value)
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