prathe-net-ldap 0.2.20110317223538
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +11 -0
- data/.gemtest +0 -0
- data/.rspec +2 -0
- data/Contributors.rdoc +21 -0
- data/Hacking.rdoc +68 -0
- data/History.rdoc +172 -0
- data/License.rdoc +29 -0
- data/Manifest.txt +49 -0
- data/README.rdoc +52 -0
- data/Rakefile +75 -0
- data/autotest/discover.rb +1 -0
- data/lib/net-ldap.rb +2 -0
- data/lib/net/ber.rb +318 -0
- data/lib/net/ber/ber_parser.rb +168 -0
- 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 +60 -0
- data/lib/net/ber/core_ext/true_class.rb +12 -0
- data/lib/net/ldap.rb +1556 -0
- data/lib/net/ldap/dataset.rb +154 -0
- data/lib/net/ldap/dn.rb +225 -0
- data/lib/net/ldap/entry.rb +185 -0
- data/lib/net/ldap/filter.rb +759 -0
- data/lib/net/ldap/password.rb +31 -0
- data/lib/net/ldap/pdu.rb +256 -0
- data/lib/net/snmp.rb +268 -0
- data/net-ldap.gemspec +59 -0
- data/spec/integration/ssl_ber_spec.rb +36 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/unit/ber/ber_spec.rb +109 -0
- 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/common.rb +3 -0
- data/test/test_entry.rb +59 -0
- data/test/test_filter.rb +122 -0
- data/test/test_ldap_connection.rb +24 -0
- data/test/test_ldif.rb +79 -0
- data/test/test_password.rb +17 -0
- data/test/test_rename.rb +77 -0
- data/test/test_snmp.rb +114 -0
- data/test/testdata.ldif +101 -0
- data/testserver/ldapserver.rb +210 -0
- data/testserver/testdata.ldif +101 -0
- metadata +206 -0
@@ -0,0 +1,759 @@
|
|
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.
|
6
|
+
#
|
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
|
+
#++
|
15
|
+
#
|
16
|
+
# Here's how to code the familiar "objectclass is present" filter:
|
17
|
+
# f = Net::LDAP::Filter.present("objectclass")
|
18
|
+
#
|
19
|
+
# The object returned by this code can be passed directly to the
|
20
|
+
# <tt>:filter</tt> parameter of Net::LDAP#search.
|
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
|
36
|
+
|
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
|
67
|
+
|
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
|
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
|
126
|
+
|
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
|
134
|
+
|
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
|
142
|
+
|
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
|
150
|
+
|
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
|
158
|
+
|
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
|
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
|
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.
|
350
|
+
#
|
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.
|
365
|
+
#
|
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
|
386
|
+
|
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
|
395
|
+
|
396
|
+
def to_raw_rfc2254
|
397
|
+
case @op
|
398
|
+
when :ne
|
399
|
+
"!(#{@left}=#{@right})"
|
400
|
+
when :eq
|
401
|
+
"#{@left}=#{@right}"
|
402
|
+
when :ex
|
403
|
+
"#{@left}:=#{@right}"
|
404
|
+
when :ge
|
405
|
+
"#{@left}>=#{@right}"
|
406
|
+
when :le
|
407
|
+
"#{@left}<=#{@right}"
|
408
|
+
when :and
|
409
|
+
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
410
|
+
when :or
|
411
|
+
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
412
|
+
when :not
|
413
|
+
"!(#{@left.to_raw_rfc2254})"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
##
|
418
|
+
# Converts the Filter object to an RFC 2254-compatible text format.
|
419
|
+
def to_rfc2254
|
420
|
+
"(#{to_raw_rfc2254})"
|
421
|
+
end
|
422
|
+
|
423
|
+
def to_s
|
424
|
+
to_rfc2254
|
425
|
+
end
|
426
|
+
|
427
|
+
##
|
428
|
+
# Converts the filter to BER format.
|
429
|
+
#--
|
430
|
+
# Filter ::=
|
431
|
+
# CHOICE {
|
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
|
442
|
+
# }
|
443
|
+
#
|
444
|
+
# SubstringFilter ::=
|
445
|
+
# SEQUENCE {
|
446
|
+
# type AttributeType,
|
447
|
+
# SEQUENCE OF CHOICE {
|
448
|
+
# initial [0] LDAPString,
|
449
|
+
# any [1] LDAPString,
|
450
|
+
# final [2] LDAPString
|
451
|
+
# }
|
452
|
+
# }
|
453
|
+
#
|
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]
|
469
|
+
#
|
470
|
+
#++
|
471
|
+
def to_ber
|
472
|
+
case @op
|
473
|
+
when :eq
|
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)
|
494
|
+
end
|
495
|
+
|
496
|
+
if ary.last.empty?
|
497
|
+
last = nil
|
498
|
+
ary.pop
|
499
|
+
else
|
500
|
+
last = ary.pop.to_ber_contextspecific(2)
|
501
|
+
end
|
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}"
|
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)
|
525
|
+
when :ge
|
526
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
|
527
|
+
when :le
|
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)
|
531
|
+
when :and
|
532
|
+
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
533
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
|
534
|
+
when :or
|
535
|
+
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
536
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
|
537
|
+
when :not
|
538
|
+
[@left.to_ber].to_ber_contextspecific(2)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
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 || []
|
591
|
+
end
|
592
|
+
|
593
|
+
##
|
594
|
+
# This is a private helper method for dealing with chains of ANDs and ORs
|
595
|
+
# that are longer than two. If BOTH of our branches are of the specified
|
596
|
+
# type of joining operator, then return both of them as an array (calling
|
597
|
+
# coalesce recursively). If they're not, then return an array consisting
|
598
|
+
# only of self.
|
599
|
+
def coalesce(operator) #:nodoc:
|
600
|
+
if @op == operator
|
601
|
+
[@left.coalesce(operator), @right.coalesce(operator)]
|
602
|
+
else
|
603
|
+
[self]
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
##
|
608
|
+
#--
|
609
|
+
# We got a hash of attribute values.
|
610
|
+
# Do we match the attributes?
|
611
|
+
# Return T/F, and call match recursively as necessary.
|
612
|
+
#++
|
613
|
+
def match(entry)
|
614
|
+
case @op
|
615
|
+
when :eq
|
616
|
+
if @right == "*"
|
617
|
+
l = entry[@left] and l.length > 0
|
618
|
+
else
|
619
|
+
l = entry[@left] and l = Array(l) and l.index(@right)
|
620
|
+
end
|
621
|
+
else
|
622
|
+
raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
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") }
|
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
|
651
|
+
|
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
|
657
|
+
|
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)
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
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
|
724
|
+
end
|
725
|
+
end
|
726
|
+
private :parse_paren_expression
|
727
|
+
|
728
|
+
##
|
729
|
+
# This parses a given expression inside of parentheses.
|
730
|
+
def parse_filter_branch(scanner)
|
731
|
+
scanner.scan(/\s*/)
|
732
|
+
if token = scanner.scan(/[-\w:.]*[\w]/)
|
733
|
+
scanner.scan(/\s*/)
|
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
|
753
|
+
end
|
754
|
+
end
|
755
|
+
end
|
756
|
+
end
|
757
|
+
private :parse_filter_branch
|
758
|
+
end # class Net::LDAP::FilterParser
|
759
|
+
end # class Net::LDAP::Filter
|