class Net::LDAP::Filter
See the individual class and instance methods below for more examples.:filter
parameter of Net::LDAP#search.
The object returned by this code can be passed directly to the
f = Net::LDAP::Filter.present(“objectclass”)
Here’s how to code the familiar “objectclass is present” filter:
++
filter strings (RFC 4515).
NOTE: This wording needs to change as we will be supporting LDAPv3 search
–
specifying LDAP search filters.
This class supplants the (infamous) RFC 2254 standard notation for
LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
Net::LDAP::Filter supports the complete set of search filters available in
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
#
def &(filter)
# Selects only entries that meet both conditions above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.
Joins two or more filters so that all conditions must be true.
#
def &(filter) self.class.join(self, filter) end
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 begins(attribute, value)
attribute must begin with a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def begins(attribute, value) new(:eq, attribute, escape(value) + "*") end
def bineq(attribute, value)
f = Net::LDAP::Filter.bineq("objectGUID", guid_bin)
guid_bin = [guid].pack("H*")
guid = "6a31b4a12aa27a41aca9603f27dd5116"
# for guid represented as hex charecters
GUID values.
This is primarily used for Microsoft Active Directory to compare
this prevents the search data from being forced into a UTF-8 string.
Creates a Filter object indicating a binary comparison.
#
def bineq(attribute, value) new(:bineq, attribute, value) end
def coalesce(operator) #:nodoc:
only of self.
coalesce recursively). If they're not, then return an array consisting
type of joining operator, then return both of them as an array (calling
that are longer than two. If BOTH of our branches are of the specified
This is a private helper method for dealing with chains of ANDs and ORs
#
def coalesce(operator) #:nodoc: if @op == operator [@left.coalesce(operator), @right.coalesce(operator)] else [self] end end
def construct(ldap_filter_string)
Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
#
def construct(ldap_filter_string) FilterParser.parse(ldap_filter_string) end
def contains(attribute, value)
attribute must contain a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def contains(attribute, value) new(:eq, attribute, "*" + escape(value) + "*") end
def ends(attribute, value)
attribute must end with a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def ends(attribute, value) new(:eq, attribute, "*" + escape(value)) end
def eq(attribute, value)
f = Net::LDAP::Filter.eq("mail", "*anderson*")
mail value containing the substring "anderson":
underlying LDAP protocol. This example selects any entry with a
regular-expressions are not supported due to limitations in the
wildcards: these match zero or more occurrences of any character. Full
parameter to #eq. The string may contain one or more "*" characters as
To match a particular range of values, pass a string as the second
f = Net::LDAP::Filter.eq("sAMAccountName", "*")
one or more values for sAMAccountName:
second parameter to #eq. This example selects only entries that have
mail. Presence is indicated by giving the value "*" in the
the filter. This is useful in case of optional attributes such as
which contain a value for the particular attribute will be selected by
Specifying that an attribute is 'present' means only directory entries
attribute must either be present or match a particular string.
Creates a Filter object indicating that the value of a particular
#
def eq(attribute, value) new(:eq, attribute, value) end
def equals(attribute, value)
attribute must match a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def equals(attribute, value) new(:eq, attribute, escape(value)) end
def escape(string)
#
def escape(string) string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } end
def ex(attribute, value)
* https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
* http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
* http://www.ietf.org/rfc/rfc2251.txt
=Refs:
uid: 1
givenName:: YsO0Yg==
sn:: YsO0YiByw7Riw6lydHM=
cn:: csO0YsOpcnRz
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
dn: uid=1,ou=People,dc=example,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit
dn: ou=People,dc=example,dc=com
dc: example
objectClass: top
objectClass: domain
dn: dc=example,dc=com
version: 1
server:
The LDIF required to support the above examples on the OpenDS LDAP
--
Net::LDAP.search(:filter => filter)
# Perform a search with the Extensible Match Filter
filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
filter = "#{attr}:=#{value}" # Basic String Filter
value = 'roberts'
attr = sample_attributes.first # Pick an extensible attribute
'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
sample_attributes = ['cn:fr', 'cn:fr.eq',
object is currently considered EXPERIMENTAL.
Creates a Filter object indicating extensible comparison. This Filter
#
def ex(attribute, value) new(:ex, attribute, value) end
def execute(&block)
* :not (one argument, which is an object returned from a recursive
a recursive call to #execute, with the same block; and
* :or (two or more arguments, each of which is an object returned from
from a recursive call to #execute, with the same block;
* :and (two or more arguments, each of which is an object returned
compared against);
* :lessOrEqual (two arguments: an attribute name and a value to be
compared against);
* :greaterOrEqual (two arguments: an attribute name and a value to be
* :present (one argument: an attribute name);
one or more "*" characters);
* :substrings (two arguments: an attribute name and a value containing
to be matched);
* :equalityMatch (the arguments will be an attribute name and a value
block:
These are the possible values that may be passed to the user-supplied
Net::LDAP::Filter objects.
A typical object to return from the user-supplied block is an array of
:or and :not operations detailed below.
locally-meaningful object that will appear as a parameter in the :and,
some desired application-defined processing, and may return a
operation. The user-supplied block (which is MANDATORY) should perform
the filter; and second, an array consisting of arguments to the
called with two arguments: first, a symbol denoting the "operation" of
when implementing an LDAP directory server. The caller's block will be
Perform filter operations against a user-supplied block. This is useful
#
def execute(&block) case @op when :eq if @right == "*" yield :present, @left elsif @right.index '*' yield :substrings, @left, @right else yield :equalityMatch, @left, @right end when :ge yield :greaterOrEqual, @left, @right when :le yield :lessOrEqual, @left, @right when :or, :and yield @op, (@left.execute(&block)), (@right.execute(&block)) when :not yield @op, (@left.execute(&block)) end || [] end
def ge(attribute, value)
Creates a Filter object indicating that a particular attribute value
#
def ge(attribute, value) new(:ge, attribute, value) end
def initialize(op, left, right) #:nodoc:
def initialize(op, left, right) #:nodoc: unless FilterTypes.include?(op) raise Net::LDAP::OperatorError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter." end @op = op @left = left @right = right end
def intersect(left, right)
# Selects only entries that meet either condition above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.
right.
Filter.intersect(left, right) is the same as left |
entries where either the left or right side are true. Calling
Creates a disjoint comparison between two or more filters. Selects
#
def intersect(left, right) new(:or, left, right) end
def join(left, right)
# Selects only entries that meet both conditions above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.
right.
Filter.join(left, right) is the same as left &
Joins two or more filters so that all conditions must be true. Calling
#
def join(left, right) new(:and, left, right) end
def le(attribute, value)
Creates a Filter object indicating that a particular attribute value
#
def le(attribute, value) new(:le, attribute, value) end
def match(entry)
Return T/F, and call match recursively as necessary.
Do we match the attributes?
We got a hash of attribute values.
--
#
def match(entry) case @op when :eq if @right == "*" l = entry[@left] and l.length > 0 else l = entry[@left] and l = Array(l) and l.index(@right) end else raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}" end end
def ne(attribute, value)
Filter::eq for more information.
is either not present or does not match a particular string; see
Creates a Filter object indicating that a particular attribute value
#
def ne(attribute, value) new(:ne, attribute, value) end
def negate(filter)
# attribute.
# Selects only entries that do not have an objectclass
as ~filter.
Negates a filter. Calling Fitler.negate(filter) i s the same
#
def negate(filter) new(:not, filter, nil) end
def parse_ber(ber)
We're hardcoding the BER constants from the RFC. These should be
--
the grammar snippet from the RFC.
LDAP searchRequest PDU. See also the comments under #to_ber, including
object. The incoming BER object most likely came to us by parsing an
Converts an LDAP search filter in BER format to an Net::LDAP::Filter
#
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 do |b| case b.ber_identifier when 0x80 # context-specific primitive 0, SubstringFilter "initial" raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0 str += escape(b) when 0x81 # context-specific primitive 0, SubstringFilter "any" str += "*#{escape(b)}" when 0x82 # context-specific primitive 0, SubstringFilter "final" str += "*#{escape(b)}" final = true end 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 6, "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) when 0xa9 # context-specific constructed 9, "extensible comparison" raise Net::LDAP::SearchFilterError, "Invalid extensible search filter, should be at least two elements" if ber.size < 2 # Reassembles the extensible filter parts # (["sn", "2.4.6.8.10", "Barbara Jones", '1']) type = value = dn = rule = nil ber.each do |element| case element.ber_identifier when 0x81 then rule=element when 0x82 then type=element when 0x83 then value=element when 0x84 then dn='dn' end end attribute = '' attribute << type if type attribute << ":#{dn}" if dn attribute << ":#{rule}" if rule ex(attribute, value) else raise Net::LDAP::BERInvalidError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter." end end
def parse_ldap_filter(obj)
filter types. Could pull them out into a constant.
TODO, we're hardcoding the RFC-1777 BER-encodings of the various
--
object.
Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
#
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::SearchFilterTypeUnknownError, "Unknown LDAP search-filter type: #{obj.ber_identifier}" end end
def present?(attribute)
This is a synonym for #eq(attribute, "*"). Also known as #present and
#
def present?(attribute) eq(attribute, "*") end
def to_ber
Substring [.6] or [.sub]
Greater than [.5] or [.gt]
Greater than or equal to [.4] or [.gte]
Equality [.3] or [.eq] (default)
Less than or equal to [.2] or [.lte]
Less than [.1] or .[lt]
Matching Rule Suffixes
}
dnAttributes [4] BOOLEAN DEFAULT FALSE
matchValue [3] AssertionValue,
type [2] AttributeDescription OPTIONAL,
matchingRule [1] MatchingRuleId OPTIONAL,
SEQUENCE {
MatchingRuleAssertion ::=
}
}
final [2] LDAPString
any [1] LDAPString,
initial [0] LDAPString,
SEQUENCE OF CHOICE {
type AttributeType,
SEQUENCE {
SubstringFilter ::=
}
extensibleMatch [9] MatchingRuleAssertion
approxMatch [8] AttributeValueAssertion,
present [7] AttributeType,
lessOrEqual [6] AttributeValueAssertion,
greaterOrEqual [5] AttributeValueAssertion,
substrings [4] SubstringFilter,
equalityMatch [3] AttributeValueAssertion,
not [2] Filter,
or [1] SET OF Filter,
and [0] SET OF Filter,
CHOICE {
Filter ::=
--
Converts the filter to BER format.
#
def to_ber case @op when :eq if @right == "*" # presence test @left.to_s.to_ber_contextspecific(7) elsif @right.to_s =~ /[*]/ # 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. ary = @right.split(/[*]+/, -1) if ary.first.empty? first = nil ary.shift else first = unescape(ary.shift).to_ber_contextspecific(0) end if ary.last.empty? last = nil ary.pop else last = unescape(ary.pop).to_ber_contextspecific(2) end seq = ary.map { |e| unescape(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 :bineq # make sure data is not forced to UTF-8 [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3) when :ex seq = [] unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/ raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}" end type, dn, rule = $1, $2, $4 seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type seq << unescape(@right).to_ber_contextspecific(3) # matchingValue seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes seq.to_ber_contextspecific(9) when :ge [@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) 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(&:to_ber).to_ber_contextspecific(0) when :or ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten ary.map(&:to_ber).to_ber_contextspecific(1) when :not [@left.to_ber].to_ber_contextspecific(2) end end
def to_raw_rfc2254
def to_raw_rfc2254 case @op when :ne "!(#{@left}=#{@right})" when :eq, :bineq "#{@left}=#{@right}" when :ex "#{@left}:=#{@right}" when :ge "#{@left}>=#{@right}" when :le "#{@left}<=#{@right}" when :and "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})" when :or "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})" when :not "!(#{@left.to_raw_rfc2254})" end end
def to_rfc2254
#
def to_rfc2254 "(#{to_raw_rfc2254})" end
def to_s
def to_s to_rfc2254 end
def unescape(right)
- Note: - slawson20170317: Don't attempt to unescape 16 byte binary data which we assume are objectGUIDs
def unescape(right) right = right.to_s if right.length == 16 && right.encoding == Encoding::BINARY right else right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") } end end
def |(filter)
# Selects only entries that meet either condition above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.
entries where either the left or right side are true.
Creates a disjoint comparison between two or more filters. Selects
#
def |(filter) self.class.intersect(self, filter) end
def ~@
# attribute.
# Selects only entries that do not have an objectclass
Negates a filter.
#
def ~@ self.class.negate(self) end