net-ldap 0.1.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of net-ldap might be problematic. Click here for more details.
- data/.autotest +11 -0
- data/.gemtest +0 -0
- data/.rspec +2 -0
- data/Contributors.rdoc +21 -0
- data/Hacking.rdoc +68 -0
- data/{History.txt → History.rdoc} +65 -1
- data/License.rdoc +29 -0
- data/Manifest.txt +25 -14
- data/README.rdoc +52 -0
- data/Rakefile +52 -96
- data/autotest/discover.rb +1 -0
- data/lib/net-ldap.rb +1 -0
- data/lib/net/ber.rb +302 -81
- data/lib/net/ber/ber_parser.rb +153 -97
- data/lib/net/ber/core_ext.rb +62 -0
- data/lib/net/ber/core_ext/array.rb +82 -0
- data/lib/net/ber/core_ext/bignum.rb +22 -0
- data/lib/net/ber/core_ext/false_class.rb +10 -0
- data/lib/net/ber/core_ext/fixnum.rb +66 -0
- data/lib/net/ber/core_ext/string.rb +48 -0
- data/lib/net/ber/core_ext/true_class.rb +12 -0
- data/lib/net/ldap.rb +1455 -1475
- data/lib/net/ldap/dataset.rb +134 -79
- data/lib/net/ldap/dn.rb +225 -0
- data/lib/net/ldap/entry.rb +168 -249
- data/lib/net/ldap/filter.rb +654 -387
- data/lib/net/ldap/password.rb +31 -0
- data/lib/net/ldap/pdu.rb +232 -233
- data/lib/net/snmp.rb +4 -31
- data/net-ldap.gemspec +59 -0
- data/spec/integration/ssl_ber_spec.rb +3 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/unit/ber/ber_spec.rb +82 -6
- data/spec/unit/ber/core_ext/string_spec.rb +51 -0
- data/spec/unit/ldap/dn_spec.rb +80 -0
- data/spec/unit/ldap/entry_spec.rb +51 -0
- data/spec/unit/ldap/filter_spec.rb +84 -0
- data/spec/unit/ldap_spec.rb +48 -0
- data/test/test_entry.rb +54 -2
- data/test/test_filter.rb +93 -54
- data/test/test_ldap_connection.rb +24 -0
- data/test/test_ldif.rb +31 -23
- data/test/test_rename.rb +77 -0
- data/test/test_snmp.rb +34 -33
- metadata +88 -52
- data/COPYING +0 -272
- data/LICENSE +0 -56
- data/README.txt +0 -68
- data/lib/net/ldap/core_ext/all.rb +0 -43
- data/lib/net/ldap/core_ext/array.rb +0 -42
- data/lib/net/ldap/core_ext/bignum.rb +0 -25
- data/lib/net/ldap/core_ext/false_class.rb +0 -11
- data/lib/net/ldap/core_ext/fixnum.rb +0 -74
- data/lib/net/ldap/core_ext/string.rb +0 -40
- data/lib/net/ldap/core_ext/true_class.rb +0 -11
- data/lib/net/ldap/psw.rb +0 -57
- data/lib/net/ldif.rb +0 -34
- data/test/test_ber.rb +0 -78
data/lib/net/ldap/entry.rb
CHANGED
@@ -1,266 +1,185 @@
|
|
1
|
-
#
|
1
|
+
# -*- ruby encoding: utf-8 -*-
|
2
|
+
##
|
3
|
+
# Objects of this class represent individual entries in an LDAP directory.
|
4
|
+
# User code generally does not instantiate this class. Net::LDAP#search
|
5
|
+
# provides objects of this class to user code, either as block parameters or
|
6
|
+
# as return values.
|
2
7
|
#
|
3
|
-
|
8
|
+
# In LDAP-land, an "entry" is a collection of attributes that are uniquely
|
9
|
+
# and globally identified by a DN ("Distinguished Name"). Attributes are
|
10
|
+
# identified by short, descriptive words or phrases. Although a directory is
|
11
|
+
# free to implement any attribute name, most of them follow rigorous
|
12
|
+
# standards so that the range of commonly-encountered attribute names is not
|
13
|
+
# large.
|
4
14
|
#
|
5
|
-
#
|
15
|
+
# An attribute name is case-insensitive. Most directories also restrict the
|
16
|
+
# range of characters allowed in attribute names. To simplify handling
|
17
|
+
# attribute names, Net::LDAP::Entry internally converts them to a standard
|
18
|
+
# format. Therefore, the methods which take attribute names can take Strings
|
19
|
+
# or Symbols, and work correctly regardless of case or capitalization.
|
6
20
|
#
|
7
|
-
#
|
21
|
+
# An attribute consists of zero or more data items called <i>values.</i> An
|
22
|
+
# entry is the combination of a unique DN, a set of attribute names, and a
|
23
|
+
# (possibly-empty) array of values for each attribute.
|
8
24
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
25
|
+
# Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
|
26
|
+
# entries. In addition to the methods documented below, you may access
|
27
|
+
# individual attributes of an entry simply by giving the attribute name as
|
28
|
+
# the name of a method call. For example:
|
13
29
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
30
|
+
# ldap.search( ... ) do |entry|
|
31
|
+
# puts "Common name: #{entry.cn}"
|
32
|
+
# puts "Email addresses:"
|
33
|
+
# entry.mail.each {|ma| puts ma}
|
34
|
+
# end
|
18
35
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
36
|
+
# If you use this technique to access an attribute that is not present in a
|
37
|
+
# particular Entry object, a NoMethodError exception will be raised.
|
22
38
|
#
|
23
|
-
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
#--
|
40
|
+
# Ugly problem to fix someday: We key off the internal hash with a canonical
|
41
|
+
# form of the attribute name: convert to a string, downcase, then take the
|
42
|
+
# symbol. Unfortunately we do this in at least three places. Should do it in
|
43
|
+
# ONE place.
|
44
|
+
class Net::LDAP::Entry
|
45
|
+
##
|
46
|
+
# This constructor is not generally called by user code.
|
47
|
+
def initialize(dn = nil) #:nodoc:
|
48
|
+
@myhash = {}
|
49
|
+
@myhash[:dn] = [dn]
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Use the LDIF format for Marshal serialization.
|
54
|
+
def _dump(depth) #:nodoc:
|
55
|
+
to_ldif
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Use the LDIF format for Marshal serialization.
|
60
|
+
def self._load(entry) #:nodoc:
|
61
|
+
from_single_ldif_string(entry)
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
##
|
66
|
+
# Converts a single LDIF entry string into an Entry object. Useful for
|
67
|
+
# Marshal serialization. If a string with multiple LDIF entries is
|
68
|
+
# provided, an exception will be raised.
|
69
|
+
def from_single_ldif_string(ldif)
|
70
|
+
ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif))
|
71
|
+
|
72
|
+
return nil if ds.empty?
|
73
|
+
|
74
|
+
raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1
|
75
|
+
|
76
|
+
entry = ds.to_entries.first
|
77
|
+
|
78
|
+
return nil if entry.dn.nil?
|
79
|
+
entry
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Canonicalizes an LDAP attribute name as a \Symbol. The name is
|
84
|
+
# lowercased and, if present, a trailing equals sign is removed.
|
85
|
+
def attribute_name(name)
|
86
|
+
name = name.to_s.downcase
|
87
|
+
name = name[0..-2] if name[-1] == ?=
|
88
|
+
name.to_sym
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Sets or replaces the array of values for the provided attribute. The
|
94
|
+
# attribute name is canonicalized prior to assignment.
|
47
95
|
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
# (possibly-empty) array of values for each attribute.
|
96
|
+
# When an attribute is set using this, that attribute is now made
|
97
|
+
# accessible through methods as well.
|
51
98
|
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
99
|
+
# entry = Net::LDAP::Entry.new("dc=com")
|
100
|
+
# entry.foo # => NoMethodError
|
101
|
+
# entry["foo"] = 12345 # => [12345]
|
102
|
+
# entry.foo # => [12345]
|
103
|
+
def []=(name, value)
|
104
|
+
@myhash[self.class.attribute_name(name)] = Kernel::Array(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Reads the array of values for the provided attribute. The attribute name
|
109
|
+
# is canonicalized prior to reading. Returns an empty array if the
|
110
|
+
# attribute does not exist.
|
111
|
+
def [](name)
|
112
|
+
name = self.class.attribute_name(name)
|
113
|
+
@myhash[name] || []
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Returns the first distinguished name (dn) of the Entry as a \String.
|
118
|
+
def dn
|
119
|
+
self[:dn].first.to_s
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Returns an array of the attribute names present in the Entry.
|
124
|
+
def attribute_names
|
125
|
+
@myhash.keys
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Accesses each of the attributes present in the Entry.
|
56
130
|
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
#--
|
67
|
-
# Ugly problem to fix someday: We key off the internal hash with a canonical
|
68
|
-
# form of the attribute name: convert to a string, downcase, then take the
|
69
|
-
# symbol. Unfortunately we do this in at least three places. Should do it in
|
70
|
-
# ONE place.
|
71
|
-
#
|
72
|
-
class Entry
|
73
|
-
# This constructor is not generally called by user code.
|
74
|
-
#
|
75
|
-
def initialize dn = nil # :nodoc:
|
76
|
-
@myhash = {}
|
77
|
-
@myhash[:dn] = [dn]
|
78
|
-
end
|
79
|
-
|
80
|
-
def _dump depth
|
81
|
-
to_ldif
|
82
|
-
end
|
83
|
-
|
84
|
-
class << self
|
85
|
-
def _load entry
|
86
|
-
from_single_ldif_string entry
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
#--
|
91
|
-
# Discovered bug, 26Aug06: I noticed that we're not converting the
|
92
|
-
# incoming value to an array if it isn't already one.
|
93
|
-
def []=(name, value) # :nodoc:
|
94
|
-
sym = attribute_name(name)
|
95
|
-
value = [value] unless value.is_a?(Array)
|
96
|
-
@myhash[sym] = value
|
97
|
-
end
|
98
|
-
|
99
|
-
#--
|
100
|
-
# We have to deal with this one as we do with []= because this one and not
|
101
|
-
# the other one gets called in formulations like entry["CN"] << cn.
|
102
|
-
#
|
103
|
-
def [](name) # :nodoc:
|
104
|
-
name = attribute_name(name) unless name.is_a?(Symbol)
|
105
|
-
@myhash[name] || []
|
106
|
-
end
|
107
|
-
|
108
|
-
# Returns the dn of the Entry as a String.
|
109
|
-
def dn
|
110
|
-
self[:dn][0].to_s
|
111
|
-
end
|
112
|
-
|
113
|
-
# Returns an array of the attribute names present in the Entry.
|
114
|
-
def attribute_names
|
115
|
-
@myhash.keys
|
116
|
-
end
|
117
|
-
|
118
|
-
# Accesses each of the attributes present in the Entry.
|
119
|
-
# Calls a user-supplied block with each attribute in turn,
|
120
|
-
# passing two arguments to the block: a Symbol giving
|
121
|
-
# the name of the attribute, and a (possibly empty)
|
122
|
-
# Array of data values.
|
123
|
-
#
|
124
|
-
def each
|
125
|
-
if block_given?
|
126
|
-
attribute_names.each {|a|
|
127
|
-
attr_name,values = a,self[a]
|
128
|
-
yield attr_name, values
|
129
|
-
}
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
alias_method :each_attribute, :each
|
134
|
-
|
135
|
-
# Converts the Entry to a String, representing the
|
136
|
-
# Entry's attributes in LDIF format.
|
137
|
-
#--
|
138
|
-
def to_ldif
|
139
|
-
ary = []
|
140
|
-
ary << "dn: #{dn}\n"
|
141
|
-
v2 = "" # temp value, save on GC
|
142
|
-
each_attribute do |k,v|
|
143
|
-
unless k == :dn
|
144
|
-
v.each {|v1|
|
145
|
-
v2 = if (k == :userpassword) || is_attribute_value_binary?(v1)
|
146
|
-
": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}"
|
147
|
-
else
|
148
|
-
" #{v1}"
|
149
|
-
end
|
150
|
-
ary << "#{k}:#{v2}\n"
|
151
|
-
}
|
152
|
-
end
|
153
|
-
end
|
154
|
-
ary << "\n"
|
155
|
-
ary.join
|
156
|
-
end
|
157
|
-
|
158
|
-
#--
|
159
|
-
# TODO, doesn't support broken lines.
|
160
|
-
# It generates a SINGLE Entry object from an incoming LDIF stream which is
|
161
|
-
# of course useless for big LDIF streams that encode many objects.
|
162
|
-
#
|
163
|
-
# DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED.
|
164
|
-
#
|
165
|
-
# As it is, it's useful for unmarshalling objects that we create, but not
|
166
|
-
# for reading arbitrary LDIF files. Eventually, we should have a class
|
167
|
-
# method that parses large LDIF streams into individual LDIF blocks
|
168
|
-
# (delimited by blank lines) and passes them here.
|
169
|
-
#
|
170
|
-
class << self
|
171
|
-
def from_single_ldif_string ldif
|
172
|
-
entry = Entry.new
|
173
|
-
entry[:dn] = []
|
174
|
-
ldif.split(/\r?\n/m).each {|line|
|
175
|
-
break if line.length == 0
|
176
|
-
if line =~ /\A([\w]+):(:?)[\s]*/
|
177
|
-
entry[$1] <<= if $2 == ':'
|
178
|
-
Base64.decode64($')
|
179
|
-
else
|
180
|
-
$'
|
181
|
-
end
|
182
|
-
end
|
183
|
-
}
|
184
|
-
entry.dn ? entry : nil
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
#--
|
189
|
-
# Part of the support for getter and setter style access to attributes.
|
190
|
-
#
|
191
|
-
def respond_to?(sym)
|
192
|
-
name = attribute_name(sym)
|
193
|
-
return true if valid_attribute?(name)
|
194
|
-
return super
|
131
|
+
# Calls a user-supplied block with each attribute in turn, passing two
|
132
|
+
# arguments to the block: a Symbol giving the name of the attribute, and a
|
133
|
+
# (possibly empty) \Array of data values.
|
134
|
+
def each # :yields: attribute-name, data-values-array
|
135
|
+
if block_given?
|
136
|
+
attribute_names.each {|a|
|
137
|
+
attr_name,values = a,self[a]
|
138
|
+
yield attr_name, values
|
139
|
+
}
|
195
140
|
end
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
141
|
+
end
|
142
|
+
alias_method :each_attribute, :each
|
143
|
+
|
144
|
+
##
|
145
|
+
# Converts the Entry to an LDIF-formatted String
|
146
|
+
def to_ldif
|
147
|
+
Net::LDAP::Dataset.from_entry(self).to_ldif_string
|
148
|
+
end
|
149
|
+
|
150
|
+
def respond_to?(sym) #:nodoc:
|
151
|
+
return true if valid_attribute?(self.class.attribute_name(sym))
|
152
|
+
return super
|
153
|
+
end
|
154
|
+
|
155
|
+
def method_missing(sym, *args, &block) #:nodoc:
|
156
|
+
name = self.class.attribute_name(sym)
|
157
|
+
|
158
|
+
if valid_attribute?(name )
|
159
|
+
if setter?(sym) && args.size == 1
|
160
|
+
value = args.first
|
161
|
+
value = Array(value)
|
162
|
+
self[name]= value
|
163
|
+
return value
|
164
|
+
elsif args.empty?
|
165
|
+
return self[name]
|
214
166
|
end
|
215
|
-
|
216
|
-
super
|
217
167
|
end
|
218
168
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
private
|
169
|
+
super
|
170
|
+
end
|
223
171
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
# enough. There are plenty of A/D attributes that are binary
|
230
|
-
# in the middle. This is probably a nasty performance killer.
|
231
|
-
def is_attribute_value_binary? value
|
232
|
-
v = value.to_s
|
233
|
-
v.each_byte {|byt|
|
234
|
-
return true if (byt < 32) || (byt > 126)
|
235
|
-
}
|
236
|
-
if v[0..0] == ':' or v[0..0] == '<'
|
237
|
-
return true
|
238
|
-
end
|
239
|
-
false
|
240
|
-
end
|
241
|
-
|
242
|
-
# Returns the symbol that can be used to access the attribute that
|
243
|
-
# sym_or_str designates.
|
244
|
-
#
|
245
|
-
def attribute_name(sym_or_str)
|
246
|
-
str = sym_or_str.to_s.downcase
|
247
|
-
|
248
|
-
# Does str match 'something='? Still only returns :something
|
249
|
-
return str[0...-1].to_sym if str.size>1 && str[-1] == ?=
|
250
|
-
return str.to_sym
|
251
|
-
end
|
252
|
-
|
253
|
-
# Given a valid attribute symbol, returns true.
|
254
|
-
#
|
255
|
-
def valid_attribute?(attr_name)
|
256
|
-
attribute_names.include?(attr_name)
|
257
|
-
end
|
258
|
-
|
259
|
-
def setter?(sym)
|
260
|
-
sym.to_s[-1] == ?=
|
261
|
-
end
|
262
|
-
end # class Entry
|
172
|
+
# Given a valid attribute symbol, returns true.
|
173
|
+
def valid_attribute?(attr_name)
|
174
|
+
attribute_names.include?(attr_name)
|
175
|
+
end
|
176
|
+
private :valid_attribute?
|
263
177
|
|
178
|
+
# Returns true if the symbol ends with an equal sign.
|
179
|
+
def setter?(sym)
|
180
|
+
sym.to_s[-1] == ?=
|
181
|
+
end
|
182
|
+
private :setter?
|
183
|
+
end # class Entry
|
264
184
|
|
265
|
-
|
266
|
-
end # module Net
|
185
|
+
require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset
|
data/lib/net/ldap/filter.rb
CHANGED
@@ -1,164 +1,447 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# it under the terms of the GNU General Public License as published by
|
7
|
-
# the Free Software Foundation; either version 2 of the License, or
|
8
|
-
# (at your option) any later version.
|
9
|
-
#
|
10
|
-
# This program is distributed in the hope that it will be useful,
|
11
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
-
# GNU General Public License for more details.
|
1
|
+
# -*- ruby encoding: utf-8 -*-
|
2
|
+
|
3
|
+
##
|
4
|
+
# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
|
5
|
+
# this class is passed to Net::LDAP#search in the parameter :filter.
|
14
6
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
7
|
+
# Net::LDAP::Filter supports the complete set of search filters available in
|
8
|
+
# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
|
9
|
+
# This class supplants the (infamous) RFC 2254 standard notation for
|
10
|
+
# specifying LDAP search filters.
|
11
|
+
#--
|
12
|
+
# NOTE: This wording needs to change as we will be supporting LDAPv3 search
|
13
|
+
# filter strings (RFC 4515).
|
14
|
+
#++
|
18
15
|
#
|
19
|
-
|
16
|
+
# Here's how to code the familiar "objectclass is present" filter:
|
17
|
+
# f = Net::LDAP::Filter.present("objectclass")
|
20
18
|
#
|
19
|
+
# The object returned by this code can be passed directly to the
|
20
|
+
# <tt>:filter</tt> parameter of Net::LDAP#search.
|
21
21
|
#
|
22
|
+
# See the individual class and instance methods below for more examples.
|
23
|
+
class Net::LDAP::Filter
|
24
|
+
##
|
25
|
+
# Known filter types.
|
26
|
+
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
|
27
|
+
|
28
|
+
def initialize(op, left, right) #:nodoc:
|
29
|
+
unless FilterTypes.include?(op)
|
30
|
+
raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
|
31
|
+
end
|
32
|
+
@op = op
|
33
|
+
@left = left
|
34
|
+
@right = right
|
35
|
+
end
|
22
36
|
|
23
|
-
|
37
|
+
class << self
|
38
|
+
# We don't want filters created except using our custom constructors.
|
39
|
+
private :new
|
40
|
+
|
41
|
+
##
|
42
|
+
# Creates a Filter object indicating that the value of a particular
|
43
|
+
# attribute must either be present or match a particular string.
|
44
|
+
#
|
45
|
+
# Specifying that an attribute is 'present' means only directory entries
|
46
|
+
# which contain a value for the particular attribute will be selected by
|
47
|
+
# the filter. This is useful in case of optional attributes such as
|
48
|
+
# <tt>mail.</tt> Presence is indicated by giving the value "*" in the
|
49
|
+
# second parameter to #eq. This example selects only entries that have
|
50
|
+
# one or more values for <tt>sAMAccountName:</tt>
|
51
|
+
#
|
52
|
+
# f = Net::LDAP::Filter.eq("sAMAccountName", "*")
|
53
|
+
#
|
54
|
+
# To match a particular range of values, pass a string as the second
|
55
|
+
# parameter to #eq. The string may contain one or more "*" characters as
|
56
|
+
# wildcards: these match zero or more occurrences of any character. Full
|
57
|
+
# regular-expressions are <i>not</i> supported due to limitations in the
|
58
|
+
# underlying LDAP protocol. This example selects any entry with a
|
59
|
+
# <tt>mail</tt> value containing the substring "anderson":
|
60
|
+
#
|
61
|
+
# f = Net::LDAP::Filter.eq("mail", "*anderson*")
|
62
|
+
#
|
63
|
+
# This filter does not perform any escaping
|
64
|
+
def eq(attribute, value)
|
65
|
+
new(:eq, attribute, value)
|
66
|
+
end
|
24
67
|
|
25
|
-
|
26
|
-
|
68
|
+
##
|
69
|
+
# Creates a Filter object indicating extensible comparison. This Filter
|
70
|
+
# object is currently considered EXPERIMENTAL.
|
71
|
+
#
|
72
|
+
# sample_attributes = ['cn:fr', 'cn:fr.eq',
|
73
|
+
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
|
74
|
+
# attr = sample_attributes.first # Pick an extensible attribute
|
75
|
+
# value = 'roberts'
|
76
|
+
#
|
77
|
+
# filter = "#{attr}:=#{value}" # Basic String Filter
|
78
|
+
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
|
79
|
+
#
|
80
|
+
# # Perform a search with the Extensible Match Filter
|
81
|
+
# Net::LDAP.search(:filter => filter)
|
82
|
+
#--
|
83
|
+
# The LDIF required to support the above examples on the OpenDS LDAP
|
84
|
+
# server:
|
85
|
+
#
|
86
|
+
# version: 1
|
87
|
+
#
|
88
|
+
# dn: dc=example,dc=com
|
89
|
+
# objectClass: domain
|
90
|
+
# objectClass: top
|
91
|
+
# dc: example
|
92
|
+
#
|
93
|
+
# dn: ou=People,dc=example,dc=com
|
94
|
+
# objectClass: organizationalUnit
|
95
|
+
# objectClass: top
|
96
|
+
# ou: People
|
97
|
+
#
|
98
|
+
# dn: uid=1,ou=People,dc=example,dc=com
|
99
|
+
# objectClass: person
|
100
|
+
# objectClass: organizationalPerson
|
101
|
+
# objectClass: inetOrgPerson
|
102
|
+
# objectClass: top
|
103
|
+
# cn:: csO0YsOpcnRz
|
104
|
+
# sn:: YsO0YiByw7Riw6lydHM=
|
105
|
+
# givenName:: YsO0Yg==
|
106
|
+
# uid: 1
|
107
|
+
#
|
108
|
+
# =Refs:
|
109
|
+
# * http://www.ietf.org/rfc/rfc2251.txt
|
110
|
+
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
|
111
|
+
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
|
112
|
+
#++
|
113
|
+
def ex(attribute, value)
|
114
|
+
new(:ex, attribute, value)
|
115
|
+
end
|
27
116
|
|
117
|
+
##
|
118
|
+
# Creates a Filter object indicating that a particular attribute value
|
119
|
+
# is either not present or does not match a particular string; see
|
120
|
+
# Filter::eq for more information.
|
121
|
+
#
|
122
|
+
# This filter does not perform any escaping
|
123
|
+
def ne(attribute, value)
|
124
|
+
new(:ne, attribute, value)
|
125
|
+
end
|
28
126
|
|
29
|
-
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# standard notation for specifying LDAP search filters.
|
37
|
-
#
|
38
|
-
# Here's how to code the familiar "objectclass is present" filter:
|
39
|
-
# f = Net::LDAP::Filter.pres( "objectclass" )
|
40
|
-
# The object returned by this code can be passed directly to
|
41
|
-
# the <tt>:filter</tt> parameter of Net::LDAP#search.
|
42
|
-
#
|
43
|
-
# See the individual class and instance methods below for more examples.
|
44
|
-
#
|
45
|
-
class Filter
|
127
|
+
##
|
128
|
+
# Creates a Filter object indicating that the value of a particular
|
129
|
+
# attribute must match a particular string. The attribute value is
|
130
|
+
# escaped, so the "*" character is interpreted literally.
|
131
|
+
def equals(attribute, value)
|
132
|
+
new(:eq, attribute, escape(value))
|
133
|
+
end
|
46
134
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
135
|
+
##
|
136
|
+
# Creates a Filter object indicating that the value of a particular
|
137
|
+
# attribute must begin with a particular string. The attribute value is
|
138
|
+
# escaped, so the "*" character is interpreted literally.
|
139
|
+
def begins(attribute, value)
|
140
|
+
new(:eq, attribute, escape(value) + "*")
|
141
|
+
end
|
52
142
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
# in case of optional attributes such as <tt>mail.</tt>
|
61
|
-
# Presence is indicated by giving the value "*" in the second
|
62
|
-
# parameter to #eq. This example selects only entries that have
|
63
|
-
# one or more values for <tt>sAMAccountName:</tt>
|
64
|
-
# f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
|
65
|
-
#
|
66
|
-
# To match a particular range of values, pass a string as the
|
67
|
-
# second parameter to #eq. The string may contain one or more
|
68
|
-
# "*" characters as wildcards: these match zero or more occurrences
|
69
|
-
# of any character. Full regular-expressions are <i>not</i> supported
|
70
|
-
# due to limitations in the underlying LDAP protocol.
|
71
|
-
# This example selects any entry with a <tt>mail</tt> value containing
|
72
|
-
# the substring "anderson":
|
73
|
-
# f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
|
74
|
-
#--
|
75
|
-
# Removed gt and lt. They ain't in the standard!
|
76
|
-
#
|
77
|
-
def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
|
78
|
-
def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
|
79
|
-
#def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
|
80
|
-
#def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
|
81
|
-
def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
|
82
|
-
def Filter::le attribute, value; Filter.new :le, attribute, value; end
|
83
|
-
|
84
|
-
# #pres( attribute ) is a synonym for #eq( attribute, "*" )
|
85
|
-
#
|
86
|
-
def Filter::pres attribute; Filter.eq attribute, "*"; end
|
143
|
+
##
|
144
|
+
# Creates a Filter object indicating that the value of a particular
|
145
|
+
# attribute must end with a particular string. The attribute value is
|
146
|
+
# escaped, so the "*" character is interpreted literally.
|
147
|
+
def ends(attribute, value)
|
148
|
+
new(:eq, attribute, "*" + escape(value))
|
149
|
+
end
|
87
150
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
151
|
+
##
|
152
|
+
# Creates a Filter object indicating that the value of a particular
|
153
|
+
# attribute must contain a particular string. The attribute value is
|
154
|
+
# escaped, so the "*" character is interpreted literally.
|
155
|
+
def contains(attribute, value)
|
156
|
+
new(:eq, attribute, "*" + escape(value) + "*")
|
157
|
+
end
|
94
158
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
159
|
+
##
|
160
|
+
# Creates a Filter object indicating that a particular attribute value
|
161
|
+
# is greater than or equal to the specified value.
|
162
|
+
def ge(attribute, value)
|
163
|
+
new(:ge, attribute, value)
|
164
|
+
end
|
101
165
|
|
166
|
+
##
|
167
|
+
# Creates a Filter object indicating that a particular attribute value
|
168
|
+
# is less than or equal to the specified value.
|
169
|
+
def le(attribute, value)
|
170
|
+
new(:le, attribute, value)
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Joins two or more filters so that all conditions must be true. Calling
|
175
|
+
# <tt>Filter.join(left, right)</tt> is the same as <tt>left &
|
176
|
+
# right</tt>.
|
177
|
+
#
|
178
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
179
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
180
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
181
|
+
# # with "George".
|
182
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
183
|
+
# # Selects only entries that meet both conditions above.
|
184
|
+
# z = Net::LDAP::Filter.join(x, y)
|
185
|
+
def join(left, right)
|
186
|
+
new(:and, left, right)
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
191
|
+
# entries where either the left or right side are true. Calling
|
192
|
+
# <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
|
193
|
+
# right</tt>.
|
194
|
+
#
|
195
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
196
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
197
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
198
|
+
# # with "George".
|
199
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
200
|
+
# # Selects only entries that meet either condition above.
|
201
|
+
# z = x | y
|
202
|
+
def intersect(left, right)
|
203
|
+
new(:or, left, right)
|
204
|
+
end
|
102
205
|
|
206
|
+
##
|
207
|
+
# Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
|
208
|
+
# as <tt>~filter</tt>.
|
209
|
+
#
|
210
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
211
|
+
# # attribute.
|
212
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
213
|
+
def negate(filter)
|
214
|
+
new(:not, filter, nil)
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# This is a synonym for #eq(attribute, "*"). Also known as #present and
|
219
|
+
# #pres.
|
220
|
+
def present?(attribute)
|
221
|
+
eq(attribute, "*")
|
222
|
+
end
|
223
|
+
alias_method :present, :present?
|
224
|
+
alias_method :pres, :present?
|
225
|
+
|
226
|
+
# http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
|
227
|
+
# charset for filters. All of the following must be escaped in any normal
|
228
|
+
# string using a single backslash ('\') as escape.
|
229
|
+
#
|
230
|
+
ESCAPES = {
|
231
|
+
"\0" => '00', # NUL = %x00 ; null character
|
232
|
+
'*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
|
233
|
+
'(' => '28', # LPARENS = %x28 ; left parenthesis ("(")
|
234
|
+
')' => '29', # RPARENS = %x29 ; right parenthesis (")")
|
235
|
+
'\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\")
|
236
|
+
}
|
237
|
+
# Compiled character class regexp using the keys from the above hash.
|
238
|
+
ESCAPE_RE = Regexp.new(
|
239
|
+
"[" +
|
240
|
+
ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
|
241
|
+
"]")
|
242
|
+
|
243
|
+
##
|
244
|
+
# Escape a string for use in an LDAP filter
|
245
|
+
def escape(string)
|
246
|
+
string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
|
247
|
+
end
|
248
|
+
|
249
|
+
##
|
250
|
+
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
|
251
|
+
# object. The incoming BER object most likely came to us by parsing an
|
252
|
+
# LDAP searchRequest PDU. See also the comments under #to_ber, including
|
253
|
+
# the grammar snippet from the RFC.
|
254
|
+
#--
|
255
|
+
# We're hardcoding the BER constants from the RFC. These should be
|
256
|
+
# broken out insto constants.
|
257
|
+
def parse_ber(ber)
|
258
|
+
case ber.ber_identifier
|
259
|
+
when 0xa0 # context-specific constructed 0, "and"
|
260
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
|
261
|
+
when 0xa1 # context-specific constructed 1, "or"
|
262
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
|
263
|
+
when 0xa2 # context-specific constructed 2, "not"
|
264
|
+
~parse_ber(ber.first)
|
265
|
+
when 0xa3 # context-specific constructed 3, "equalityMatch"
|
266
|
+
if ber.last == "*"
|
267
|
+
else
|
268
|
+
eq(ber.first, ber.last)
|
269
|
+
end
|
270
|
+
when 0xa4 # context-specific constructed 4, "substring"
|
271
|
+
str = ""
|
272
|
+
final = false
|
273
|
+
ber.last.each { |b|
|
274
|
+
case b.ber_identifier
|
275
|
+
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
|
276
|
+
raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
|
277
|
+
str += b
|
278
|
+
when 0x81 # context-specific primitive 0, SubstringFilter "any"
|
279
|
+
str += "*#{b}"
|
280
|
+
when 0x82 # context-specific primitive 0, SubstringFilter "final"
|
281
|
+
str += "*#{b}"
|
282
|
+
final = true
|
283
|
+
end
|
284
|
+
}
|
285
|
+
str += "*" unless final
|
286
|
+
eq(ber.first.to_s, str)
|
287
|
+
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
|
288
|
+
ge(ber.first.to_s, ber.last.to_s)
|
289
|
+
when 0xa6 # context-specific constructed 6, "lessOrEqual"
|
290
|
+
le(ber.first.to_s, ber.last.to_s)
|
291
|
+
when 0x87 # context-specific primitive 7, "present"
|
292
|
+
# call to_s to get rid of the BER-identifiedness of the incoming string.
|
293
|
+
present?(ber.to_s)
|
294
|
+
when 0xa9 # context-specific constructed 9, "extensible comparison"
|
295
|
+
raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
|
296
|
+
|
297
|
+
# Reassembles the extensible filter parts
|
298
|
+
# (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
|
299
|
+
type = value = dn = rule = nil
|
300
|
+
ber.each do |element|
|
301
|
+
case element.ber_identifier
|
302
|
+
when 0x81 then rule=element
|
303
|
+
when 0x82 then type=element
|
304
|
+
when 0x83 then value=element
|
305
|
+
when 0x84 then dn='dn'
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
attribute = ''
|
310
|
+
attribute << type if type
|
311
|
+
attribute << ":#{dn}" if dn
|
312
|
+
attribute << ":#{rule}" if rule
|
313
|
+
|
314
|
+
ex(attribute, value)
|
315
|
+
else
|
316
|
+
raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
##
|
321
|
+
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
|
322
|
+
# to a Net::LDAP::Filter.
|
323
|
+
def construct(ldap_filter_string)
|
324
|
+
FilterParser.parse(ldap_filter_string)
|
325
|
+
end
|
326
|
+
alias_method :from_rfc2254, :construct
|
327
|
+
alias_method :from_rfc4515, :construct
|
328
|
+
|
329
|
+
##
|
330
|
+
# Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
|
331
|
+
# object.
|
332
|
+
#--
|
333
|
+
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
334
|
+
# filter types. Could pull them out into a constant.
|
335
|
+
#++
|
336
|
+
def parse_ldap_filter(obj)
|
337
|
+
case obj.ber_identifier
|
338
|
+
when 0x87 # present. context-specific primitive 7.
|
339
|
+
eq(obj.to_s, "*")
|
340
|
+
when 0xa3 # equalityMatch. context-specific constructed 3.
|
341
|
+
eq(obj[0], obj[1])
|
342
|
+
else
|
343
|
+
raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
##
|
349
|
+
# Joins two or more filters so that all conditions must be true.
|
103
350
|
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
# attribute
|
107
|
-
#
|
351
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
352
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
353
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
354
|
+
# # with "George".
|
355
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
356
|
+
# # Selects only entries that meet both conditions above.
|
357
|
+
# z = x & y
|
358
|
+
def &(filter)
|
359
|
+
self.class.join(self, filter)
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
364
|
+
# entries where either the left or right side are true.
|
108
365
|
#
|
109
|
-
|
110
|
-
#
|
111
|
-
#
|
112
|
-
|
366
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
367
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
368
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
369
|
+
# # with "George".
|
370
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
371
|
+
# # Selects only entries that meet either condition above.
|
372
|
+
# z = x | y
|
373
|
+
def |(filter)
|
374
|
+
self.class.intersect(self, filter)
|
375
|
+
end
|
376
|
+
|
377
|
+
##
|
378
|
+
# Negates a filter.
|
379
|
+
#
|
380
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
381
|
+
# # attribute.
|
382
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
383
|
+
def ~@
|
384
|
+
self.class.negate(self)
|
385
|
+
end
|
113
386
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
387
|
+
##
|
388
|
+
# Equality operator for filters, useful primarily for constructing unit tests.
|
389
|
+
def ==(filter)
|
390
|
+
# 20100320 AZ: We need to come up with a better way of doing this. This
|
391
|
+
# is just nasty.
|
392
|
+
str = "[@op,@left,@right]"
|
393
|
+
self.instance_eval(str) == filter.instance_eval(str)
|
394
|
+
end
|
119
395
|
|
120
|
-
def
|
396
|
+
def to_raw_rfc2254
|
121
397
|
case @op
|
122
398
|
when :ne
|
123
|
-
"
|
399
|
+
"!(#{@left}=#{@right})"
|
124
400
|
when :eq
|
125
|
-
"
|
126
|
-
|
127
|
-
|
128
|
-
#when :lt
|
129
|
-
# "#{@left}<#{@right}"
|
401
|
+
"#{@left}=#{@right}"
|
402
|
+
when :ex
|
403
|
+
"#{@left}:=#{@right}"
|
130
404
|
when :ge
|
131
405
|
"#{@left}>=#{@right}"
|
132
406
|
when :le
|
133
407
|
"#{@left}<=#{@right}"
|
134
408
|
when :and
|
135
|
-
"
|
409
|
+
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
136
410
|
when :or
|
137
|
-
"
|
411
|
+
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
138
412
|
when :not
|
139
|
-
"
|
140
|
-
else
|
141
|
-
raise "invalid or unsupported operator in LDAP Filter"
|
413
|
+
"!(#{@left.to_raw_rfc2254})"
|
142
414
|
end
|
143
415
|
end
|
144
416
|
|
417
|
+
##
|
418
|
+
# Converts the Filter object to an RFC 2254-compatible text format.
|
419
|
+
def to_rfc2254
|
420
|
+
"(#{to_raw_rfc2254})"
|
421
|
+
end
|
145
422
|
|
423
|
+
def to_s
|
424
|
+
to_rfc2254
|
425
|
+
end
|
426
|
+
|
427
|
+
##
|
428
|
+
# Converts the filter to BER format.
|
146
429
|
#--
|
147
|
-
# to_ber
|
148
430
|
# Filter ::=
|
149
431
|
# CHOICE {
|
150
|
-
# and
|
151
|
-
# or
|
152
|
-
# not
|
153
|
-
# equalityMatch
|
154
|
-
# substrings
|
155
|
-
# greaterOrEqual
|
156
|
-
# lessOrEqual
|
157
|
-
# present
|
158
|
-
# approxMatch
|
432
|
+
# and [0] SET OF Filter,
|
433
|
+
# or [1] SET OF Filter,
|
434
|
+
# not [2] Filter,
|
435
|
+
# equalityMatch [3] AttributeValueAssertion,
|
436
|
+
# substrings [4] SubstringFilter,
|
437
|
+
# greaterOrEqual [5] AttributeValueAssertion,
|
438
|
+
# lessOrEqual [6] AttributeValueAssertion,
|
439
|
+
# present [7] AttributeType,
|
440
|
+
# approxMatch [8] AttributeValueAssertion,
|
441
|
+
# extensibleMatch [9] MatchingRuleAssertion
|
159
442
|
# }
|
160
443
|
#
|
161
|
-
# SubstringFilter
|
444
|
+
# SubstringFilter ::=
|
162
445
|
# SEQUENCE {
|
163
446
|
# type AttributeType,
|
164
447
|
# SEQUENCE OF CHOICE {
|
@@ -168,325 +451,309 @@ class Filter
|
|
168
451
|
# }
|
169
452
|
# }
|
170
453
|
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
454
|
+
# MatchingRuleAssertion ::=
|
455
|
+
# SEQUENCE {
|
456
|
+
# matchingRule [1] MatchingRuleId OPTIONAL,
|
457
|
+
# type [2] AttributeDescription OPTIONAL,
|
458
|
+
# matchValue [3] AssertionValue,
|
459
|
+
# dnAttributes [4] BOOLEAN DEFAULT FALSE
|
460
|
+
# }
|
461
|
+
#
|
462
|
+
# Matching Rule Suffixes
|
463
|
+
# Less than [.1] or .[lt]
|
464
|
+
# Less than or equal to [.2] or [.lte]
|
465
|
+
# Equality [.3] or [.eq] (default)
|
466
|
+
# Greater than or equal to [.4] or [.gte]
|
467
|
+
# Greater than [.5] or [.gt]
|
468
|
+
# Substring [.6] or [.sub]
|
182
469
|
#
|
470
|
+
#++
|
183
471
|
def to_ber
|
184
472
|
case @op
|
185
473
|
when :eq
|
186
|
-
if @right == "*"
|
187
|
-
@left.to_s.to_ber_contextspecific
|
188
|
-
elsif @right =~ /[
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
474
|
+
if @right == "*" # presence test
|
475
|
+
@left.to_s.to_ber_contextspecific(7)
|
476
|
+
elsif @right =~ /[*]/ # substring
|
477
|
+
# Parsing substrings is a little tricky. We use String#split to
|
478
|
+
# break a string into substrings delimited by the * (star)
|
479
|
+
# character. But we also need to know whether there is a star at the
|
480
|
+
# head and tail of the string, so we use a limit parameter value of
|
481
|
+
# -1: "If negative, there is no limit to the number of fields
|
482
|
+
# returned, and trailing null fields are not suppressed."
|
483
|
+
#
|
484
|
+
# 20100320 AZ: This is much simpler than the previous verison. Also,
|
485
|
+
# unnecessary regex escaping has been removed.
|
486
|
+
|
487
|
+
ary = @right.split(/[*]+/, -1)
|
488
|
+
|
489
|
+
if ary.first.empty?
|
490
|
+
first = nil
|
491
|
+
ary.shift
|
492
|
+
else
|
493
|
+
first = ary.shift.to_ber_contextspecific(0)
|
196
494
|
end
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
seq << ary.shift.to_ber_contextspecific(2)
|
495
|
+
|
496
|
+
if ary.last.empty?
|
497
|
+
last = nil
|
498
|
+
ary.pop
|
499
|
+
else
|
500
|
+
last = ary.pop.to_ber_contextspecific(2)
|
204
501
|
end
|
205
|
-
|
206
|
-
|
207
|
-
|
502
|
+
|
503
|
+
seq = ary.map { |e| e.to_ber_contextspecific(1) }
|
504
|
+
seq.unshift first if first
|
505
|
+
seq.push last if last
|
506
|
+
|
507
|
+
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
|
508
|
+
else # equality
|
509
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
|
510
|
+
end
|
511
|
+
when :ex
|
512
|
+
seq = []
|
513
|
+
|
514
|
+
unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
|
515
|
+
raise Net::LDAP::LdapError, "Bad attribute #{@left}"
|
208
516
|
end
|
517
|
+
type, dn, rule = $1, $2, $4
|
518
|
+
|
519
|
+
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
|
520
|
+
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
|
521
|
+
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
|
522
|
+
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
|
523
|
+
|
524
|
+
seq.to_ber_contextspecific(9)
|
209
525
|
when :ge
|
210
|
-
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific
|
526
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
|
211
527
|
when :le
|
212
|
-
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific
|
528
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
|
529
|
+
when :ne
|
530
|
+
[self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
|
213
531
|
when :and
|
214
532
|
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
215
|
-
ary.map {|a| a.to_ber}.to_ber_contextspecific(
|
533
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
|
216
534
|
when :or
|
217
535
|
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
218
|
-
ary.map {|a| a.to_ber}.to_ber_contextspecific(
|
536
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
|
219
537
|
when :not
|
220
|
-
|
221
|
-
else
|
222
|
-
# ERROR, we'll return objectclass=* to keep things from blowing up,
|
223
|
-
# but that ain't a good answer and we need to kick out an error of some kind.
|
224
|
-
raise "unimplemented search filter"
|
538
|
+
[@left.to_ber].to_ber_contextspecific(2)
|
225
539
|
end
|
226
540
|
end
|
227
541
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
542
|
+
##
|
543
|
+
# Perform filter operations against a user-supplied block. This is useful
|
544
|
+
# when implementing an LDAP directory server. The caller's block will be
|
545
|
+
# called with two arguments: first, a symbol denoting the "operation" of
|
546
|
+
# the filter; and second, an array consisting of arguments to the
|
547
|
+
# operation. The user-supplied block (which is MANDATORY) should perform
|
548
|
+
# some desired application-defined processing, and may return a
|
549
|
+
# locally-meaningful object that will appear as a parameter in the :and,
|
550
|
+
# :or and :not operations detailed below.
|
551
|
+
#
|
552
|
+
# A typical object to return from the user-supplied block is an array of
|
553
|
+
# Net::LDAP::Filter objects.
|
554
|
+
#
|
555
|
+
# These are the possible values that may be passed to the user-supplied
|
556
|
+
# block:
|
557
|
+
# * :equalityMatch (the arguments will be an attribute name and a value
|
558
|
+
# to be matched);
|
559
|
+
# * :substrings (two arguments: an attribute name and a value containing
|
560
|
+
# one or more "*" characters);
|
561
|
+
# * :present (one argument: an attribute name);
|
562
|
+
# * :greaterOrEqual (two arguments: an attribute name and a value to be
|
563
|
+
# compared against);
|
564
|
+
# * :lessOrEqual (two arguments: an attribute name and a value to be
|
565
|
+
# compared against);
|
566
|
+
# * :and (two or more arguments, each of which is an object returned
|
567
|
+
# from a recursive call to #execute, with the same block;
|
568
|
+
# * :or (two or more arguments, each of which is an object returned from
|
569
|
+
# a recursive call to #execute, with the same block; and
|
570
|
+
# * :not (one argument, which is an object returned from a recursive
|
571
|
+
# call to #execute with the the same block.
|
572
|
+
def execute(&block)
|
573
|
+
case @op
|
574
|
+
when :eq
|
575
|
+
if @right == "*"
|
576
|
+
yield :present, @left
|
577
|
+
elsif @right.index '*'
|
578
|
+
yield :substrings, @left, @right
|
579
|
+
else
|
580
|
+
yield :equalityMatch, @left, @right
|
581
|
+
end
|
582
|
+
when :ge
|
583
|
+
yield :greaterOrEqual, @left, @right
|
584
|
+
when :le
|
585
|
+
yield :lessOrEqual, @left, @right
|
586
|
+
when :or, :and
|
587
|
+
yield @op, (@left.execute(&block)), (@right.execute(&block))
|
588
|
+
when :not
|
589
|
+
yield @op, (@left.execute(&block))
|
590
|
+
end || []
|
232
591
|
end
|
233
592
|
|
234
|
-
|
235
|
-
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
|
236
|
-
# object. The incoming BER object most likely came to us by parsing an
|
237
|
-
# LDAP searchRequest PDU.
|
238
|
-
# Cf the comments under #to_ber, including the grammar snippet from the RFC.
|
239
|
-
#--
|
240
|
-
# We're hardcoding the BER constants from the RFC. Ought to break them out
|
241
|
-
# into constants.
|
242
|
-
#
|
243
|
-
def Filter::parse_ber ber
|
244
|
-
case ber.ber_identifier
|
245
|
-
when 0xa0 # context-specific constructed 0, "and"
|
246
|
-
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
|
247
|
-
when 0xa1 # context-specific constructed 1, "or"
|
248
|
-
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
|
249
|
-
when 0xa2 # context-specific constructed 2, "not"
|
250
|
-
~ Filter::parse_ber( ber.first )
|
251
|
-
when 0xa3 # context-specific constructed 3, "equalityMatch"
|
252
|
-
if ber.last == "*"
|
253
|
-
else
|
254
|
-
Filter.eq( ber.first, ber.last )
|
255
|
-
end
|
256
|
-
when 0xa4 # context-specific constructed 4, "substring"
|
257
|
-
str = ""
|
258
|
-
final = false
|
259
|
-
ber.last.each {|b|
|
260
|
-
case b.ber_identifier
|
261
|
-
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
|
262
|
-
raise "unrecognized substring filter, bad initial" if str.length > 0
|
263
|
-
str += b
|
264
|
-
when 0x81 # context-specific primitive 0, SubstringFilter "any"
|
265
|
-
str += "*#{b}"
|
266
|
-
when 0x82 # context-specific primitive 0, SubstringFilter "final"
|
267
|
-
str += "*#{b}"
|
268
|
-
final = true
|
269
|
-
end
|
270
|
-
}
|
271
|
-
str += "*" unless final
|
272
|
-
Filter.eq( ber.first.to_s, str )
|
273
|
-
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
|
274
|
-
Filter.ge( ber.first.to_s, ber.last.to_s )
|
275
|
-
when 0xa6 # context-specific constructed 5, "lessOrEqual"
|
276
|
-
Filter.le( ber.first.to_s, ber.last.to_s )
|
277
|
-
when 0x87 # context-specific primitive 7, "present"
|
278
|
-
# call to_s to get rid of the BER-identifiedness of the incoming string.
|
279
|
-
Filter.pres( ber.to_s )
|
280
|
-
else
|
281
|
-
raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
|
286
|
-
# Perform filter operations against a user-supplied block. This is useful when implementing
|
287
|
-
# an LDAP directory server. The caller's block will be called with two arguments: first, a
|
288
|
-
# symbol denoting the "operation" of the filter; and second, an array consisting of arguments
|
289
|
-
# to the operation. The user-supplied block (which is MANDATORY) should perform some desired
|
290
|
-
# application-defined processing, and may return a locally-meaningful object that will appear
|
291
|
-
# as a parameter in the :and, :or and :not operations detailed below.
|
292
|
-
#
|
293
|
-
# A typical object to return from the user-supplied block is an array of
|
294
|
-
# Net::LDAP::Filter objects.
|
295
|
-
#
|
296
|
-
# These are the possible values that may be passed to the user-supplied block:
|
297
|
-
# :equalityMatch (the arguments will be an attribute name and a value to be matched);
|
298
|
-
# :substrings (two arguments: an attribute name and a value containing one or more * characters);
|
299
|
-
# :present (one argument: an attribute name);
|
300
|
-
# :greaterOrEqual (two arguments: an attribute name and a value to be compared against);
|
301
|
-
# :lessOrEqual (two arguments: an attribute name and a value to be compared against);
|
302
|
-
# :and (two or more arguments, each of which is an object returned from a recursive call
|
303
|
-
# to #execute, with the same block;
|
304
|
-
# :or (two or more arguments, each of which is an object returned from a recursive call
|
305
|
-
# to #execute, with the same block;
|
306
|
-
# :not (one argument, which is an object returned from a recursive call to #execute with the
|
307
|
-
# the same block.
|
308
|
-
#
|
309
|
-
def execute &block
|
310
|
-
case @op
|
311
|
-
when :eq
|
312
|
-
if @right == "*"
|
313
|
-
yield :present, @left
|
314
|
-
elsif @right.index '*'
|
315
|
-
yield :substrings, @left, @right
|
316
|
-
else
|
317
|
-
yield :equalityMatch, @left, @right
|
318
|
-
end
|
319
|
-
when :ge
|
320
|
-
yield :greaterOrEqual, @left, @right
|
321
|
-
when :le
|
322
|
-
yield :lessOrEqual, @left, @right
|
323
|
-
when :or, :and
|
324
|
-
yield @op, (@left.execute(&block)), (@right.execute(&block))
|
325
|
-
when :not
|
326
|
-
yield @op, (@left.execute(&block))
|
327
|
-
end || []
|
328
|
-
end
|
329
|
-
|
330
|
-
|
331
|
-
#--
|
332
|
-
# coalesce
|
593
|
+
##
|
333
594
|
# This is a private helper method for dealing with chains of ANDs and ORs
|
334
595
|
# that are longer than two. If BOTH of our branches are of the specified
|
335
596
|
# type of joining operator, then return both of them as an array (calling
|
336
597
|
# coalesce recursively). If they're not, then return an array consisting
|
337
598
|
# only of self.
|
338
|
-
|
339
|
-
def coalesce operator
|
599
|
+
def coalesce(operator) #:nodoc:
|
340
600
|
if @op == operator
|
341
|
-
[@left.coalesce(
|
601
|
+
[@left.coalesce(operator), @right.coalesce(operator)]
|
342
602
|
else
|
343
603
|
[self]
|
344
604
|
end
|
345
605
|
end
|
346
606
|
|
347
|
-
|
348
|
-
|
349
|
-
#--
|
350
|
-
# We get a Ruby object which comes from parsing an RFC-1777 "Filter"
|
351
|
-
# object. Convert it to a Net::LDAP::Filter.
|
352
|
-
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
353
|
-
# filter types. Could pull them out into a constant.
|
354
|
-
#
|
355
|
-
def Filter::parse_ldap_filter obj
|
356
|
-
case obj.ber_identifier
|
357
|
-
when 0x87 # present. context-specific primitive 7.
|
358
|
-
Filter.eq( obj.to_s, "*" )
|
359
|
-
when 0xa3 # equalityMatch. context-specific constructed 3.
|
360
|
-
Filter.eq( obj[0], obj[1] )
|
361
|
-
else
|
362
|
-
raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
|
363
|
-
end
|
364
|
-
end
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
607
|
+
##
|
369
608
|
#--
|
370
609
|
# We got a hash of attribute values.
|
371
610
|
# Do we match the attributes?
|
372
611
|
# Return T/F, and call match recursively as necessary.
|
373
|
-
|
612
|
+
#++
|
613
|
+
def match(entry)
|
374
614
|
case @op
|
375
615
|
when :eq
|
376
616
|
if @right == "*"
|
377
617
|
l = entry[@left] and l.length > 0
|
378
618
|
else
|
379
|
-
l = entry[@left] and l = l
|
619
|
+
l = entry[@left] and l = Array(l) and l.index(@right)
|
380
620
|
end
|
381
621
|
else
|
382
|
-
raise LdapError
|
622
|
+
raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
|
383
623
|
end
|
384
624
|
end
|
385
625
|
|
386
|
-
|
387
|
-
# to
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
# Synonym for #construct.
|
393
|
-
# to a Net::LDAP::Filter.
|
394
|
-
def self.from_rfc2254 ldap_filter_string
|
395
|
-
construct ldap_filter_string
|
396
|
-
end
|
397
|
-
|
398
|
-
end # class Net::LDAP::Filter
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
class FilterParser #:nodoc:
|
403
|
-
|
404
|
-
attr_reader :filter
|
405
|
-
|
406
|
-
def initialize str
|
407
|
-
@filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
|
626
|
+
##
|
627
|
+
# Converts escaped characters (e.g., "\\28") to unescaped characters
|
628
|
+
# ("(").
|
629
|
+
def unescape(right)
|
630
|
+
right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
|
408
631
|
end
|
632
|
+
private :unescape
|
633
|
+
|
634
|
+
##
|
635
|
+
# Parses RFC 2254-style string representations of LDAP filters into Filter
|
636
|
+
# object hierarchies.
|
637
|
+
class FilterParser #:nodoc:
|
638
|
+
##
|
639
|
+
# The constructed filter.
|
640
|
+
attr_reader :filter
|
641
|
+
|
642
|
+
class << self
|
643
|
+
private :new
|
644
|
+
|
645
|
+
##
|
646
|
+
# Construct a filter tree from the provided string and return it.
|
647
|
+
def parse(ldap_filter_string)
|
648
|
+
new(ldap_filter_string).filter
|
649
|
+
end
|
650
|
+
end
|
409
651
|
|
410
|
-
|
411
|
-
|
412
|
-
|
652
|
+
def initialize(str)
|
653
|
+
require 'strscan' # Don't load strscan until we need it.
|
654
|
+
@filter = parse(StringScanner.new(str))
|
655
|
+
raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
|
656
|
+
end
|
413
657
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
658
|
+
##
|
659
|
+
# Parse the string contained in the StringScanner provided. Parsing
|
660
|
+
# tries to parse a standalone expression first. If that fails, it tries
|
661
|
+
# to parse a parenthesized expression.
|
662
|
+
def parse(scanner)
|
663
|
+
parse_filter_branch(scanner) or parse_paren_expression(scanner)
|
664
|
+
end
|
665
|
+
private :parse
|
666
|
+
|
667
|
+
##
|
668
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
669
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
670
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
671
|
+
# into a branch array for filter merging using the parent operation.
|
672
|
+
#
|
673
|
+
# This method parses the branch text out into an array of filter
|
674
|
+
# objects.
|
675
|
+
def parse_branches(scanner)
|
676
|
+
branches = []
|
677
|
+
while branch = parse_paren_expression(scanner)
|
678
|
+
branches << branch
|
679
|
+
end
|
680
|
+
branches
|
681
|
+
end
|
682
|
+
private :parse_branches
|
683
|
+
|
684
|
+
##
|
685
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
686
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
687
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
688
|
+
# into a branch array for filter merging using the parent operation.
|
689
|
+
#
|
690
|
+
# This method calls #parse_branches to generate the branch list and then
|
691
|
+
# merges them into a single Filter tree by calling the provided
|
692
|
+
# operation.
|
693
|
+
def merge_branches(op, scanner)
|
694
|
+
filter = nil
|
695
|
+
branches = parse_branches(scanner)
|
696
|
+
|
697
|
+
if branches.size >= 1
|
698
|
+
filter = branches.shift
|
699
|
+
while not branches.empty?
|
700
|
+
filter = filter.__send__(op, branches.shift)
|
447
701
|
end
|
448
|
-
else
|
449
|
-
parse_filter_branch( scanner )
|
450
702
|
end
|
451
703
|
|
452
|
-
|
453
|
-
|
704
|
+
filter
|
705
|
+
end
|
706
|
+
private :merge_branches
|
707
|
+
|
708
|
+
def parse_paren_expression(scanner)
|
709
|
+
if scanner.scan(/\s*\(\s*/)
|
710
|
+
expr = if scanner.scan(/\s*\&\s*/)
|
711
|
+
merge_branches(:&, scanner)
|
712
|
+
elsif scanner.scan(/\s*\|\s*/)
|
713
|
+
merge_branches(:|, scanner)
|
714
|
+
elsif scanner.scan(/\s*\!\s*/)
|
715
|
+
br = parse_paren_expression(scanner)
|
716
|
+
~br if br
|
717
|
+
else
|
718
|
+
parse_filter_branch(scanner)
|
719
|
+
end
|
720
|
+
|
721
|
+
if expr and scanner.scan(/\s*\)\s*/)
|
722
|
+
expr
|
723
|
+
end
|
454
724
|
end
|
455
725
|
end
|
456
|
-
|
726
|
+
private :parse_paren_expression
|
457
727
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
def parse_filter_branch scanner
|
462
|
-
scanner.scan(/\s*/)
|
463
|
-
if token = scanner.scan( /[\w\-_]+/ )
|
728
|
+
##
|
729
|
+
# This parses a given expression inside of parentheses.
|
730
|
+
def parse_filter_branch(scanner)
|
464
731
|
scanner.scan(/\s*/)
|
465
|
-
if
|
732
|
+
if token = scanner.scan(/[-\w:.]*[\w]/)
|
466
733
|
scanner.scan(/\s*/)
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
734
|
+
if op = scanner.scan(/<=|>=|!=|:=|=/)
|
735
|
+
scanner.scan(/\s*/)
|
736
|
+
if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s]|\\[a-fA-F\d]{2})+/)
|
737
|
+
# 20100313 AZ: Assumes that "(uid=george*)" is the same as
|
738
|
+
# "(uid=george* )". The standard doesn't specify, but I can find
|
739
|
+
# no examples that suggest otherwise.
|
740
|
+
value.strip!
|
741
|
+
case op
|
742
|
+
when "="
|
743
|
+
Net::LDAP::Filter.eq(token, value)
|
744
|
+
when "!="
|
745
|
+
Net::LDAP::Filter.ne(token, value)
|
746
|
+
when "<="
|
747
|
+
Net::LDAP::Filter.le(token, value)
|
748
|
+
when ">="
|
749
|
+
Net::LDAP::Filter.ge(token, value)
|
750
|
+
when ":="
|
751
|
+
Net::LDAP::Filter.ex(token, value)
|
752
|
+
end
|
483
753
|
end
|
484
754
|
end
|
485
755
|
end
|
486
756
|
end
|
487
|
-
|
488
|
-
|
489
|
-
end # class Net::LDAP::
|
490
|
-
|
491
|
-
end # class Net::LDAP
|
492
|
-
end # module Net
|
757
|
+
private :parse_filter_branch
|
758
|
+
end # class Net::LDAP::FilterParser
|
759
|
+
end # class Net::LDAP::Filter
|