Major clean-up of Net::LDAP::Filter.
This commit is contained in:
parent
afe43a5e58
commit
1dbf5908ae
27
History.txt
27
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.
|
||||
|
|
|
@ -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":
|
||||
# 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,49 +340,53 @@ 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
|
||||
|
||||
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
|
||||
[@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)
|
||||
|
@ -217,96 +394,41 @@ class Filter
|
|||
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
||||
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,16 +449,13 @@ 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)]
|
||||
else
|
||||
|
@ -344,33 +463,13 @@ class Filter
|
|||
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
|
||||
|
||||
|
||||
|
||||
##
|
||||
# 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)
|
||||
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
|
||||
|
||||
private :parse_filter_branch
|
||||
end # class Net::LDAP::FilterParser
|
||||
|
||||
end # class Net::LDAP
|
||||
end # module Net
|
||||
end # class Net::LDAP::Filter
|
||||
|
|
Loading…
Reference in a new issue