Chris@909: # $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
Chris@909: #
Chris@909: #
Chris@909: #----------------------------------------------------------------------------
Chris@909: #
Chris@909: # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
Chris@909: #
Chris@909: # Gmail: garbagecat10
Chris@909: #
Chris@909: # This program is free software; you can redistribute it and/or modify
Chris@909: # it under the terms of the GNU General Public License as published by
Chris@909: # the Free Software Foundation; either version 2 of the License, or
Chris@909: # (at your option) any later version.
Chris@909: #
Chris@909: # This program is distributed in the hope that it will be useful,
Chris@909: # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@909: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@909: # GNU General Public License for more details.
Chris@909: #
Chris@909: # You should have received a copy of the GNU General Public License
Chris@909: # along with this program; if not, write to the Free Software
Chris@909: # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Chris@909: #
Chris@909: #---------------------------------------------------------------------------
Chris@909: #
Chris@909: #
Chris@909:
Chris@909:
Chris@909: module Net
Chris@909: class LDAP
Chris@909:
Chris@909:
Chris@909: # Class Net::LDAP::Filter is used to constrain
Chris@909: # LDAP searches. An object of this class is
Chris@909: # passed to Net::LDAP#search in the parameter :filter.
Chris@909: #
Chris@909: # Net::LDAP::Filter supports the complete set of search filters
Chris@909: # available in LDAP, including conjunction, disjunction and negation
Chris@909: # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
Chris@909: # standard notation for specifying LDAP search filters.
Chris@909: #
Chris@909: # Here's how to code the familiar "objectclass is present" filter:
Chris@909: # f = Net::LDAP::Filter.pres( "objectclass" )
Chris@909: # The object returned by this code can be passed directly to
Chris@909: # the :filter parameter of Net::LDAP#search.
Chris@909: #
Chris@909: # See the individual class and instance methods below for more examples.
Chris@909: #
Chris@909: class Filter
Chris@909:
Chris@909: def initialize op, a, b
Chris@909: @op = op
Chris@909: @left = a
Chris@909: @right = b
Chris@909: end
Chris@909:
Chris@909: # #eq creates a filter object indicating that the value of
Chris@909: # a paticular attribute must be either present or must
Chris@909: # match a particular string.
Chris@909: #
Chris@909: # To specify that an attribute is "present" means that only
Chris@909: # directory entries which contain a value for the particular
Chris@909: # attribute will be selected by the filter. This is useful
Chris@909: # in case of optional attributes such as mail.
Chris@909: # Presence is indicated by giving the value "*" in the second
Chris@909: # parameter to #eq. This example selects only entries that have
Chris@909: # one or more values for sAMAccountName:
Chris@909: # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
Chris@909: #
Chris@909: # To match a particular range of values, pass a string as the
Chris@909: # second parameter to #eq. The string may contain one or more
Chris@909: # "*" characters as wildcards: these match zero or more occurrences
Chris@909: # of any character. Full regular-expressions are not supported
Chris@909: # due to limitations in the underlying LDAP protocol.
Chris@909: # This example selects any entry with a mail value containing
Chris@909: # the substring "anderson":
Chris@909: # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
Chris@909: #--
Chris@909: # Removed gt and lt. They ain't in the standard!
Chris@909: #
Chris@909: def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
Chris@909: def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
Chris@909: #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
Chris@909: #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
Chris@909: def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
Chris@909: def Filter::le attribute, value; Filter.new :le, attribute, value; end
Chris@909:
Chris@909: # #pres( attribute ) is a synonym for #eq( attribute, "*" )
Chris@909: #
Chris@909: def Filter::pres attribute; Filter.eq attribute, "*"; end
Chris@909:
Chris@909: # operator & ("AND") is used to conjoin two or more filters.
Chris@909: # This expression will select only entries that have an objectclass
Chris@909: # attribute AND have a mail attribute that begins with "George":
Chris@909: # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
Chris@909: #
Chris@909: def & filter; Filter.new :and, self, filter; end
Chris@909:
Chris@909: # operator | ("OR") is used to disjoin two or more filters.
Chris@909: # This expression will select entries that have either an objectclass
Chris@909: # attribute OR a mail attribute that begins with "George":
Chris@909: # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
Chris@909: #
Chris@909: def | filter; Filter.new :or, self, filter; end
Chris@909:
Chris@909:
Chris@909: #
Chris@909: # operator ~ ("NOT") is used to negate a filter.
Chris@909: # This expression will select only entries that do not have an objectclass
Chris@909: # attribute:
Chris@909: # f = ~ Net::LDAP::Filter.pres( "objectclass" )
Chris@909: #
Chris@909: #--
Chris@909: # This operator can't be !, evidently. Try it.
Chris@909: # Removed GT and LT. They're not in the RFC.
Chris@909: def ~@; Filter.new :not, self, nil; end
Chris@909:
Chris@909:
Chris@909: def to_s
Chris@909: case @op
Chris@909: when :ne
Chris@909: "(!(#{@left}=#{@right}))"
Chris@909: when :eq
Chris@909: "(#{@left}=#{@right})"
Chris@909: #when :gt
Chris@909: # "#{@left}>#{@right}"
Chris@909: #when :lt
Chris@909: # "#{@left}<#{@right}"
Chris@909: when :ge
Chris@909: "#{@left}>=#{@right}"
Chris@909: when :le
Chris@909: "#{@left}<=#{@right}"
Chris@909: when :and
Chris@909: "(&(#{@left})(#{@right}))"
Chris@909: when :or
Chris@909: "(|(#{@left})(#{@right}))"
Chris@909: when :not
Chris@909: "(!(#{@left}))"
Chris@909: else
Chris@909: raise "invalid or unsupported operator in LDAP Filter"
Chris@909: end
Chris@909: end
Chris@909:
Chris@909:
Chris@909: #--
Chris@909: # to_ber
Chris@909: # Filter ::=
Chris@909: # CHOICE {
Chris@909: # and [0] SET OF Filter,
Chris@909: # or [1] SET OF Filter,
Chris@909: # not [2] Filter,
Chris@909: # equalityMatch [3] AttributeValueAssertion,
Chris@909: # substrings [4] SubstringFilter,
Chris@909: # greaterOrEqual [5] AttributeValueAssertion,
Chris@909: # lessOrEqual [6] AttributeValueAssertion,
Chris@909: # present [7] AttributeType,
Chris@909: # approxMatch [8] AttributeValueAssertion
Chris@909: # }
Chris@909: #
Chris@909: # SubstringFilter
Chris@909: # SEQUENCE {
Chris@909: # type AttributeType,
Chris@909: # SEQUENCE OF CHOICE {
Chris@909: # initial [0] LDAPString,
Chris@909: # any [1] LDAPString,
Chris@909: # final [2] LDAPString
Chris@909: # }
Chris@909: # }
Chris@909: #
Chris@909: # Parsing substrings is a little tricky.
Chris@909: # We use the split method to break a string into substrings
Chris@909: # delimited by the * (star) character. But we also need
Chris@909: # to know whether there is a star at the head and tail
Chris@909: # of the string. A Ruby particularity comes into play here:
Chris@909: # if you split on * and the first character of the string is
Chris@909: # a star, then split will return an array whose first element
Chris@909: # is an _empty_ string. But if the _last_ character of the
Chris@909: # string is star, then split will return an array that does
Chris@909: # _not_ add an empty string at the end. So we have to deal
Chris@909: # with all that specifically.
Chris@909: #
Chris@909: def to_ber
Chris@909: case @op
Chris@909: when :eq
Chris@909: if @right == "*" # present
Chris@909: @left.to_s.to_ber_contextspecific 7
Chris@909: elsif @right =~ /[\*]/ #substring
Chris@909: ary = @right.split( /[\*]+/ )
Chris@909: final_star = @right =~ /[\*]$/
Chris@909: initial_star = ary.first == "" and ary.shift
Chris@909:
Chris@909: seq = []
Chris@909: unless initial_star
Chris@909: seq << ary.shift.to_ber_contextspecific(0)
Chris@909: end
Chris@909: n_any_strings = ary.length - (final_star ? 0 : 1)
Chris@909: #p n_any_strings
Chris@909: n_any_strings.times {
Chris@909: seq << ary.shift.to_ber_contextspecific(1)
Chris@909: }
Chris@909: unless final_star
Chris@909: seq << ary.shift.to_ber_contextspecific(2)
Chris@909: end
Chris@909: [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
Chris@909: else #equality
Chris@909: [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
Chris@909: end
Chris@909: when :ge
Chris@909: [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
Chris@909: when :le
Chris@909: [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
Chris@909: when :and
Chris@909: ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
Chris@909: ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
Chris@909: when :or
Chris@909: ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
Chris@909: ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
Chris@909: when :not
Chris@909: [@left.to_ber].to_ber_contextspecific 2
Chris@909: else
Chris@909: # ERROR, we'll return objectclass=* to keep things from blowing up,
Chris@909: # but that ain't a good answer and we need to kick out an error of some kind.
Chris@909: raise "unimplemented search filter"
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: #--
Chris@909: # coalesce
Chris@909: # This is a private helper method for dealing with chains of ANDs and ORs
Chris@909: # that are longer than two. If BOTH of our branches are of the specified
Chris@909: # type of joining operator, then return both of them as an array (calling
Chris@909: # coalesce recursively). If they're not, then return an array consisting
Chris@909: # only of self.
Chris@909: #
Chris@909: def coalesce operator
Chris@909: if @op == operator
Chris@909: [@left.coalesce( operator ), @right.coalesce( operator )]
Chris@909: else
Chris@909: [self]
Chris@909: end
Chris@909: end
Chris@909:
Chris@909:
Chris@909:
Chris@909: #--
Chris@909: # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
Chris@909: # object. Convert it to a Net::LDAP::Filter.
Chris@909: # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
Chris@909: # filter types. Could pull them out into a constant.
Chris@909: #
Chris@909: def Filter::parse_ldap_filter obj
Chris@909: case obj.ber_identifier
Chris@909: when 0x87 # present. context-specific primitive 7.
Chris@909: Filter.eq( obj.to_s, "*" )
Chris@909: when 0xa3 # equalityMatch. context-specific constructed 3.
Chris@909: Filter.eq( obj[0], obj[1] )
Chris@909: else
Chris@909: raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
Chris@909: end
Chris@909: end
Chris@909:
Chris@909:
Chris@909: #--
Chris@909: # We got a hash of attribute values.
Chris@909: # Do we match the attributes?
Chris@909: # Return T/F, and call match recursively as necessary.
Chris@909: def match entry
Chris@909: case @op
Chris@909: when :eq
Chris@909: if @right == "*"
Chris@909: l = entry[@left] and l.length > 0
Chris@909: else
Chris@909: l = entry[@left] and l = l.to_a and l.index(@right)
Chris@909: end
Chris@909: else
Chris@909: raise LdapError.new( "unknown filter type in match: #{@op}" )
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
Chris@909: # to a Net::LDAP::Filter.
Chris@909: def self.construct ldap_filter_string
Chris@909: FilterParser.new(ldap_filter_string).filter
Chris@909: end
Chris@909:
Chris@909: # Synonym for #construct.
Chris@909: # to a Net::LDAP::Filter.
Chris@909: def self.from_rfc2254 ldap_filter_string
Chris@909: construct ldap_filter_string
Chris@909: end
Chris@909:
Chris@909: end # class Net::LDAP::Filter
Chris@909:
Chris@909:
Chris@909:
Chris@909: class FilterParser #:nodoc:
Chris@909:
Chris@909: attr_reader :filter
Chris@909:
Chris@909: def initialize str
Chris@909: require 'strscan'
Chris@909: @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
Chris@909: end
Chris@909:
Chris@909: def parse scanner
Chris@909: parse_filter_branch(scanner) or parse_paren_expression(scanner)
Chris@909: end
Chris@909:
Chris@909: def parse_paren_expression scanner
Chris@909: if scanner.scan(/\s*\(\s*/)
Chris@909: b = if scanner.scan(/\s*\&\s*/)
Chris@909: a = nil
Chris@909: branches = []
Chris@909: while br = parse_paren_expression(scanner)
Chris@909: branches << br
Chris@909: end
Chris@909: if branches.length >= 2
Chris@909: a = branches.shift
Chris@909: while branches.length > 0
Chris@909: a = a & branches.shift
Chris@909: end
Chris@909: a
Chris@909: end
Chris@909: elsif scanner.scan(/\s*\|\s*/)
Chris@909: # TODO: DRY!
Chris@909: a = nil
Chris@909: branches = []
Chris@909: while br = parse_paren_expression(scanner)
Chris@909: branches << br
Chris@909: end
Chris@909: if branches.length >= 2
Chris@909: a = branches.shift
Chris@909: while branches.length > 0
Chris@909: a = a | branches.shift
Chris@909: end
Chris@909: a
Chris@909: end
Chris@909: elsif scanner.scan(/\s*\!\s*/)
Chris@909: br = parse_paren_expression(scanner)
Chris@909: if br
Chris@909: ~ br
Chris@909: end
Chris@909: else
Chris@909: parse_filter_branch( scanner )
Chris@909: end
Chris@909:
Chris@909: if b and scanner.scan( /\s*\)\s*/ )
Chris@909: b
Chris@909: end
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: # Added a greatly-augmented filter contributed by Andre Nathan
Chris@909: # for detecting special characters in values. (15Aug06)
Chris@909: def parse_filter_branch scanner
Chris@909: scanner.scan(/\s*/)
Chris@909: if token = scanner.scan( /[\w\-_]+/ )
Chris@909: scanner.scan(/\s*/)
Chris@909: if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
Chris@909: scanner.scan(/\s*/)
Chris@909: #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
Chris@909: if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
Chris@909: case op
Chris@909: when "="
Chris@909: Filter.eq( token, value )
Chris@909: when "!="
Chris@909: Filter.ne( token, value )
Chris@909: when "<"
Chris@909: Filter.lt( token, value )
Chris@909: when "<="
Chris@909: Filter.le( token, value )
Chris@909: when ">"
Chris@909: Filter.gt( token, value )
Chris@909: when ">="
Chris@909: Filter.ge( token, value )
Chris@909: end
Chris@909: end
Chris@909: end
Chris@909: end
Chris@909: end
Chris@909:
Chris@909: end # class Net::LDAP::FilterParser
Chris@909:
Chris@909: end # class Net::LDAP
Chris@909: end # module Net
Chris@909:
Chris@909: