prathe-net-ldap 0.2.20110317223538

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.autotest +11 -0
  2. data/.gemtest +0 -0
  3. data/.rspec +2 -0
  4. data/Contributors.rdoc +21 -0
  5. data/Hacking.rdoc +68 -0
  6. data/History.rdoc +172 -0
  7. data/License.rdoc +29 -0
  8. data/Manifest.txt +49 -0
  9. data/README.rdoc +52 -0
  10. data/Rakefile +75 -0
  11. data/autotest/discover.rb +1 -0
  12. data/lib/net-ldap.rb +2 -0
  13. data/lib/net/ber.rb +318 -0
  14. data/lib/net/ber/ber_parser.rb +168 -0
  15. data/lib/net/ber/core_ext.rb +62 -0
  16. data/lib/net/ber/core_ext/array.rb +82 -0
  17. data/lib/net/ber/core_ext/bignum.rb +22 -0
  18. data/lib/net/ber/core_ext/false_class.rb +10 -0
  19. data/lib/net/ber/core_ext/fixnum.rb +66 -0
  20. data/lib/net/ber/core_ext/string.rb +60 -0
  21. data/lib/net/ber/core_ext/true_class.rb +12 -0
  22. data/lib/net/ldap.rb +1556 -0
  23. data/lib/net/ldap/dataset.rb +154 -0
  24. data/lib/net/ldap/dn.rb +225 -0
  25. data/lib/net/ldap/entry.rb +185 -0
  26. data/lib/net/ldap/filter.rb +759 -0
  27. data/lib/net/ldap/password.rb +31 -0
  28. data/lib/net/ldap/pdu.rb +256 -0
  29. data/lib/net/snmp.rb +268 -0
  30. data/net-ldap.gemspec +59 -0
  31. data/spec/integration/ssl_ber_spec.rb +36 -0
  32. data/spec/spec.opts +2 -0
  33. data/spec/spec_helper.rb +5 -0
  34. data/spec/unit/ber/ber_spec.rb +109 -0
  35. data/spec/unit/ber/core_ext/string_spec.rb +51 -0
  36. data/spec/unit/ldap/dn_spec.rb +80 -0
  37. data/spec/unit/ldap/entry_spec.rb +51 -0
  38. data/spec/unit/ldap/filter_spec.rb +84 -0
  39. data/spec/unit/ldap_spec.rb +48 -0
  40. data/test/common.rb +3 -0
  41. data/test/test_entry.rb +59 -0
  42. data/test/test_filter.rb +122 -0
  43. data/test/test_ldap_connection.rb +24 -0
  44. data/test/test_ldif.rb +79 -0
  45. data/test/test_password.rb +17 -0
  46. data/test/test_rename.rb +77 -0
  47. data/test/test_snmp.rb +114 -0
  48. data/test/testdata.ldif +101 -0
  49. data/testserver/ldapserver.rb +210 -0
  50. data/testserver/testdata.ldif +101 -0
  51. 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