adams-net-ldap 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +15 -0
  2. data/.autotest +11 -0
  3. data/.rspec +2 -0
  4. data/Contributors.rdoc +22 -0
  5. data/Hacking.rdoc +68 -0
  6. data/History.rdoc +186 -0
  7. data/License.rdoc +29 -0
  8. data/Manifest.txt +49 -0
  9. data/README.rdoc +52 -0
  10. data/Rakefile +74 -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 +96 -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 +76 -0
  21. data/lib/net/ber/core_ext/true_class.rb +12 -0
  22. data/lib/net/ldap.rb +1633 -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 +781 -0
  27. data/lib/net/ldap/password.rb +37 -0
  28. data/lib/net/ldap/pdu.rb +273 -0
  29. data/lib/net/snmp.rb +268 -0
  30. data/net-ldap.gemspec +58 -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 +114 -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 +78 -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 +221 -0
@@ -0,0 +1,781 @@
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, :bineq ]
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 a binary comparison.
70
+ # this prevents the search data from being forced into a UTF-8 string.
71
+ #
72
+ # This is primarily used for Microsoft Active Directory to compare
73
+ # GUID values.
74
+ #
75
+ # # for guid represented as hex charecters
76
+ # guid = "6a31b4a12aa27a41aca9603f27dd5116"
77
+ # guid_bin = [guid].pack("H*")
78
+ # f = Net::LDAP::Filter.bineq("objectGUID", guid_bin)
79
+ #
80
+ # This filter does not perform any escaping.
81
+ def bineq(attribute, value)
82
+ new(:bineq, attribute, value)
83
+ end
84
+
85
+ ##
86
+ # Creates a Filter object indicating extensible comparison. This Filter
87
+ # object is currently considered EXPERIMENTAL.
88
+ #
89
+ # sample_attributes = ['cn:fr', 'cn:fr.eq',
90
+ # 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
91
+ # attr = sample_attributes.first # Pick an extensible attribute
92
+ # value = 'roberts'
93
+ #
94
+ # filter = "#{attr}:=#{value}" # Basic String Filter
95
+ # filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
96
+ #
97
+ # # Perform a search with the Extensible Match Filter
98
+ # Net::LDAP.search(:filter => filter)
99
+ #--
100
+ # The LDIF required to support the above examples on the OpenDS LDAP
101
+ # server:
102
+ #
103
+ # version: 1
104
+ #
105
+ # dn: dc=example,dc=com
106
+ # objectClass: domain
107
+ # objectClass: top
108
+ # dc: example
109
+ #
110
+ # dn: ou=People,dc=example,dc=com
111
+ # objectClass: organizationalUnit
112
+ # objectClass: top
113
+ # ou: People
114
+ #
115
+ # dn: uid=1,ou=People,dc=example,dc=com
116
+ # objectClass: person
117
+ # objectClass: organizationalPerson
118
+ # objectClass: inetOrgPerson
119
+ # objectClass: top
120
+ # cn:: csO0YsOpcnRz
121
+ # sn:: YsO0YiByw7Riw6lydHM=
122
+ # givenName:: YsO0Yg==
123
+ # uid: 1
124
+ #
125
+ # =Refs:
126
+ # * http://www.ietf.org/rfc/rfc2251.txt
127
+ # * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
128
+ # * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
129
+ #++
130
+ def ex(attribute, value)
131
+ new(:ex, attribute, value)
132
+ end
133
+
134
+ ##
135
+ # Creates a Filter object indicating that a particular attribute value
136
+ # is either not present or does not match a particular string; see
137
+ # Filter::eq for more information.
138
+ #
139
+ # This filter does not perform any escaping
140
+ def ne(attribute, value)
141
+ new(:ne, attribute, value)
142
+ end
143
+
144
+ ##
145
+ # Creates a Filter object indicating that the value of a particular
146
+ # attribute must match a particular string. The attribute value is
147
+ # escaped, so the "*" character is interpreted literally.
148
+ def equals(attribute, value)
149
+ new(:eq, attribute, escape(value))
150
+ end
151
+
152
+ ##
153
+ # Creates a Filter object indicating that the value of a particular
154
+ # attribute must begin with a particular string. The attribute value is
155
+ # escaped, so the "*" character is interpreted literally.
156
+ def begins(attribute, value)
157
+ new(:eq, attribute, escape(value) + "*")
158
+ end
159
+
160
+ ##
161
+ # Creates a Filter object indicating that the value of a particular
162
+ # attribute must end with a particular string. The attribute value is
163
+ # escaped, so the "*" character is interpreted literally.
164
+ def ends(attribute, value)
165
+ new(:eq, attribute, "*" + escape(value))
166
+ end
167
+
168
+ ##
169
+ # Creates a Filter object indicating that the value of a particular
170
+ # attribute must contain a particular string. The attribute value is
171
+ # escaped, so the "*" character is interpreted literally.
172
+ def contains(attribute, value)
173
+ new(:eq, attribute, "*" + escape(value) + "*")
174
+ end
175
+
176
+ ##
177
+ # Creates a Filter object indicating that a particular attribute value
178
+ # is greater than or equal to the specified value.
179
+ def ge(attribute, value)
180
+ new(:ge, attribute, value)
181
+ end
182
+
183
+ ##
184
+ # Creates a Filter object indicating that a particular attribute value
185
+ # is less than or equal to the specified value.
186
+ def le(attribute, value)
187
+ new(:le, attribute, value)
188
+ end
189
+
190
+ ##
191
+ # Joins two or more filters so that all conditions must be true. Calling
192
+ # <tt>Filter.join(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 both conditions above.
201
+ # z = Net::LDAP::Filter.join(x, y)
202
+ def join(left, right)
203
+ new(:and, left, right)
204
+ end
205
+
206
+ ##
207
+ # Creates a disjoint comparison between two or more filters. Selects
208
+ # entries where either the left or right side are true. Calling
209
+ # <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
210
+ # right</tt>.
211
+ #
212
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
213
+ # x = Net::LDAP::Filter.present("objectclass")
214
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
215
+ # # with "George".
216
+ # y = Net::LDAP::Filter.eq("mail", "George*")
217
+ # # Selects only entries that meet either condition above.
218
+ # z = x | y
219
+ def intersect(left, right)
220
+ new(:or, left, right)
221
+ end
222
+
223
+ ##
224
+ # Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
225
+ # as <tt>~filter</tt>.
226
+ #
227
+ # # Selects only entries that do not have an <tt>objectclass</tt>
228
+ # # attribute.
229
+ # x = ~Net::LDAP::Filter.present("objectclass")
230
+ def negate(filter)
231
+ new(:not, filter, nil)
232
+ end
233
+
234
+ ##
235
+ # This is a synonym for #eq(attribute, "*"). Also known as #present and
236
+ # #pres.
237
+ def present?(attribute)
238
+ eq(attribute, "*")
239
+ end
240
+ alias_method :present, :present?
241
+ alias_method :pres, :present?
242
+
243
+ # http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
244
+ # charset for filters. All of the following must be escaped in any normal
245
+ # string using a single backslash ('\') as escape.
246
+ #
247
+ ESCAPES = {
248
+ "\0" => '00', # NUL = %x00 ; null character
249
+ '*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
250
+ '(' => '28', # LPARENS = %x28 ; left parenthesis ("(")
251
+ ')' => '29', # RPARENS = %x29 ; right parenthesis (")")
252
+ '\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\")
253
+ }
254
+ # Compiled character class regexp using the keys from the above hash.
255
+ ESCAPE_RE = Regexp.new(
256
+ "[" +
257
+ ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
258
+ "]")
259
+
260
+ ##
261
+ # Escape a string for use in an LDAP filter
262
+ def escape(string)
263
+ string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
264
+ end
265
+
266
+ ##
267
+ # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
268
+ # object. The incoming BER object most likely came to us by parsing an
269
+ # LDAP searchRequest PDU. See also the comments under #to_ber, including
270
+ # the grammar snippet from the RFC.
271
+ #--
272
+ # We're hardcoding the BER constants from the RFC. These should be
273
+ # broken out insto constants.
274
+ def parse_ber(ber)
275
+ case ber.ber_identifier
276
+ when 0xa0 # context-specific constructed 0, "and"
277
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
278
+ when 0xa1 # context-specific constructed 1, "or"
279
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
280
+ when 0xa2 # context-specific constructed 2, "not"
281
+ ~parse_ber(ber.first)
282
+ when 0xa3 # context-specific constructed 3, "equalityMatch"
283
+ if ber.last == "*"
284
+ else
285
+ eq(ber.first, ber.last)
286
+ end
287
+ when 0xa4 # context-specific constructed 4, "substring"
288
+ str = ""
289
+ final = false
290
+ ber.last.each { |b|
291
+ case b.ber_identifier
292
+ when 0x80 # context-specific primitive 0, SubstringFilter "initial"
293
+ raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
294
+ str += b
295
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
296
+ str += "*#{b}"
297
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
298
+ str += "*#{b}"
299
+ final = true
300
+ end
301
+ }
302
+ str += "*" unless final
303
+ eq(ber.first.to_s, str)
304
+ when 0xa5 # context-specific constructed 5, "greaterOrEqual"
305
+ ge(ber.first.to_s, ber.last.to_s)
306
+ when 0xa6 # context-specific constructed 6, "lessOrEqual"
307
+ le(ber.first.to_s, ber.last.to_s)
308
+ when 0x87 # context-specific primitive 7, "present"
309
+ # call to_s to get rid of the BER-identifiedness of the incoming string.
310
+ present?(ber.to_s)
311
+ when 0xa9 # context-specific constructed 9, "extensible comparison"
312
+ raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
313
+
314
+ # Reassembles the extensible filter parts
315
+ # (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
316
+ type = value = dn = rule = nil
317
+ ber.each do |element|
318
+ case element.ber_identifier
319
+ when 0x81 then rule=element
320
+ when 0x82 then type=element
321
+ when 0x83 then value=element
322
+ when 0x84 then dn='dn'
323
+ end
324
+ end
325
+
326
+ attribute = ''
327
+ attribute << type if type
328
+ attribute << ":#{dn}" if dn
329
+ attribute << ":#{rule}" if rule
330
+
331
+ ex(attribute, value)
332
+ else
333
+ raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
334
+ end
335
+ end
336
+
337
+ ##
338
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
339
+ # to a Net::LDAP::Filter.
340
+ def construct(ldap_filter_string)
341
+ FilterParser.parse(ldap_filter_string)
342
+ end
343
+ alias_method :from_rfc2254, :construct
344
+ alias_method :from_rfc4515, :construct
345
+
346
+ ##
347
+ # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
348
+ # object.
349
+ #--
350
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
351
+ # filter types. Could pull them out into a constant.
352
+ #++
353
+ def parse_ldap_filter(obj)
354
+ case obj.ber_identifier
355
+ when 0x87 # present. context-specific primitive 7.
356
+ eq(obj.to_s, "*")
357
+ when 0xa3 # equalityMatch. context-specific constructed 3.
358
+ eq(obj[0], obj[1])
359
+ else
360
+ raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
361
+ end
362
+ end
363
+ end
364
+
365
+ ##
366
+ # Joins two or more filters so that all conditions must be true.
367
+ #
368
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
369
+ # x = Net::LDAP::Filter.present("objectclass")
370
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
371
+ # # with "George".
372
+ # y = Net::LDAP::Filter.eq("mail", "George*")
373
+ # # Selects only entries that meet both conditions above.
374
+ # z = x & y
375
+ def &(filter)
376
+ self.class.join(self, filter)
377
+ end
378
+
379
+ ##
380
+ # Creates a disjoint comparison between two or more filters. Selects
381
+ # entries where either the left or right side are true.
382
+ #
383
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
384
+ # x = Net::LDAP::Filter.present("objectclass")
385
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
386
+ # # with "George".
387
+ # y = Net::LDAP::Filter.eq("mail", "George*")
388
+ # # Selects only entries that meet either condition above.
389
+ # z = x | y
390
+ def |(filter)
391
+ self.class.intersect(self, filter)
392
+ end
393
+
394
+ ##
395
+ # Negates a filter.
396
+ #
397
+ # # Selects only entries that do not have an <tt>objectclass</tt>
398
+ # # attribute.
399
+ # x = ~Net::LDAP::Filter.present("objectclass")
400
+ def ~@
401
+ self.class.negate(self)
402
+ end
403
+
404
+ ##
405
+ # Equality operator for filters, useful primarily for constructing unit tests.
406
+ def ==(filter)
407
+ # 20100320 AZ: We need to come up with a better way of doing this. This
408
+ # is just nasty.
409
+ str = "[@op,@left,@right]"
410
+ self.instance_eval(str) == filter.instance_eval(str)
411
+ end
412
+
413
+ def to_raw_rfc2254
414
+ case @op
415
+ when :ne
416
+ "!(#{@left}=#{@right})"
417
+ when :eq
418
+ "#{@left}=#{@right}"
419
+ when :bineq
420
+ "#{@left}=#{@right}"
421
+ when :ex
422
+ "#{@left}:=#{@right}"
423
+ when :ge
424
+ "#{@left}>=#{@right}"
425
+ when :le
426
+ "#{@left}<=#{@right}"
427
+ when :and
428
+ "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
429
+ when :or
430
+ "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
431
+ when :not
432
+ "!(#{@left.to_raw_rfc2254})"
433
+ end
434
+ end
435
+
436
+ ##
437
+ # Converts the Filter object to an RFC 2254-compatible text format.
438
+ def to_rfc2254
439
+ "(#{to_raw_rfc2254})"
440
+ end
441
+
442
+ def to_s
443
+ to_rfc2254
444
+ end
445
+
446
+ ##
447
+ # Converts the filter to BER format.
448
+ #--
449
+ # Filter ::=
450
+ # CHOICE {
451
+ # and [0] SET OF Filter,
452
+ # or [1] SET OF Filter,
453
+ # not [2] Filter,
454
+ # equalityMatch [3] AttributeValueAssertion,
455
+ # substrings [4] SubstringFilter,
456
+ # greaterOrEqual [5] AttributeValueAssertion,
457
+ # lessOrEqual [6] AttributeValueAssertion,
458
+ # present [7] AttributeType,
459
+ # approxMatch [8] AttributeValueAssertion,
460
+ # extensibleMatch [9] MatchingRuleAssertion
461
+ # }
462
+ #
463
+ # SubstringFilter ::=
464
+ # SEQUENCE {
465
+ # type AttributeType,
466
+ # SEQUENCE OF CHOICE {
467
+ # initial [0] LDAPString,
468
+ # any [1] LDAPString,
469
+ # final [2] LDAPString
470
+ # }
471
+ # }
472
+ #
473
+ # MatchingRuleAssertion ::=
474
+ # SEQUENCE {
475
+ # matchingRule [1] MatchingRuleId OPTIONAL,
476
+ # type [2] AttributeDescription OPTIONAL,
477
+ # matchValue [3] AssertionValue,
478
+ # dnAttributes [4] BOOLEAN DEFAULT FALSE
479
+ # }
480
+ #
481
+ # Matching Rule Suffixes
482
+ # Less than [.1] or .[lt]
483
+ # Less than or equal to [.2] or [.lte]
484
+ # Equality [.3] or [.eq] (default)
485
+ # Greater than or equal to [.4] or [.gte]
486
+ # Greater than [.5] or [.gt]
487
+ # Substring [.6] or [.sub]
488
+ #
489
+ #++
490
+ def to_ber
491
+ case @op
492
+ when :eq
493
+ if @right == "*" # presence test
494
+ @left.to_s.to_ber_contextspecific(7)
495
+ elsif @right =~ /[*]/ # substring
496
+ # Parsing substrings is a little tricky. We use String#split to
497
+ # break a string into substrings delimited by the * (star)
498
+ # character. But we also need to know whether there is a star at the
499
+ # head and tail of the string, so we use a limit parameter value of
500
+ # -1: "If negative, there is no limit to the number of fields
501
+ # returned, and trailing null fields are not suppressed."
502
+ #
503
+ # 20100320 AZ: This is much simpler than the previous verison. Also,
504
+ # unnecessary regex escaping has been removed.
505
+
506
+ ary = @right.split(/[*]+/, -1)
507
+
508
+ if ary.first.empty?
509
+ first = nil
510
+ ary.shift
511
+ else
512
+ first = ary.shift.to_ber_contextspecific(0)
513
+ end
514
+
515
+ if ary.last.empty?
516
+ last = nil
517
+ ary.pop
518
+ else
519
+ last = ary.pop.to_ber_contextspecific(2)
520
+ end
521
+
522
+ seq = ary.map { |e| e.to_ber_contextspecific(1) }
523
+ seq.unshift first if first
524
+ seq.push last if last
525
+
526
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
527
+ else # equality
528
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
529
+ end
530
+ when :bineq
531
+ # make sure data is not forced to UTF-8
532
+ [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3)
533
+ when :ex
534
+ seq = []
535
+
536
+ unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
537
+ raise Net::LDAP::LdapError, "Bad attribute #{@left}"
538
+ end
539
+ type, dn, rule = $1, $2, $4
540
+
541
+ seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
542
+ seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
543
+ seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
544
+ seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
545
+
546
+ seq.to_ber_contextspecific(9)
547
+ when :ge
548
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
549
+ when :le
550
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
551
+ when :ne
552
+ [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
553
+ when :and
554
+ ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
555
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
556
+ when :or
557
+ ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
558
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
559
+ when :not
560
+ [@left.to_ber].to_ber_contextspecific(2)
561
+ end
562
+ end
563
+
564
+ ##
565
+ # Perform filter operations against a user-supplied block. This is useful
566
+ # when implementing an LDAP directory server. The caller's block will be
567
+ # called with two arguments: first, a symbol denoting the "operation" of
568
+ # the filter; and second, an array consisting of arguments to the
569
+ # operation. The user-supplied block (which is MANDATORY) should perform
570
+ # some desired application-defined processing, and may return a
571
+ # locally-meaningful object that will appear as a parameter in the :and,
572
+ # :or and :not operations detailed below.
573
+ #
574
+ # A typical object to return from the user-supplied block is an array of
575
+ # Net::LDAP::Filter objects.
576
+ #
577
+ # These are the possible values that may be passed to the user-supplied
578
+ # block:
579
+ # * :equalityMatch (the arguments will be an attribute name and a value
580
+ # to be matched);
581
+ # * :substrings (two arguments: an attribute name and a value containing
582
+ # one or more "*" characters);
583
+ # * :present (one argument: an attribute name);
584
+ # * :greaterOrEqual (two arguments: an attribute name and a value to be
585
+ # compared against);
586
+ # * :lessOrEqual (two arguments: an attribute name and a value to be
587
+ # compared against);
588
+ # * :and (two or more arguments, each of which is an object returned
589
+ # from a recursive call to #execute, with the same block;
590
+ # * :or (two or more arguments, each of which is an object returned from
591
+ # a recursive call to #execute, with the same block; and
592
+ # * :not (one argument, which is an object returned from a recursive
593
+ # call to #execute with the the same block.
594
+ def execute(&block)
595
+ case @op
596
+ when :eq
597
+ if @right == "*"
598
+ yield :present, @left
599
+ elsif @right.index '*'
600
+ yield :substrings, @left, @right
601
+ else
602
+ yield :equalityMatch, @left, @right
603
+ end
604
+ when :ge
605
+ yield :greaterOrEqual, @left, @right
606
+ when :le
607
+ yield :lessOrEqual, @left, @right
608
+ when :or, :and
609
+ yield @op, (@left.execute(&block)), (@right.execute(&block))
610
+ when :not
611
+ yield @op, (@left.execute(&block))
612
+ end || []
613
+ end
614
+
615
+ ##
616
+ # This is a private helper method for dealing with chains of ANDs and ORs
617
+ # that are longer than two. If BOTH of our branches are of the specified
618
+ # type of joining operator, then return both of them as an array (calling
619
+ # coalesce recursively). If they're not, then return an array consisting
620
+ # only of self.
621
+ def coalesce(operator) #:nodoc:
622
+ if @op == operator
623
+ [@left.coalesce(operator), @right.coalesce(operator)]
624
+ else
625
+ [self]
626
+ end
627
+ end
628
+
629
+ ##
630
+ #--
631
+ # We got a hash of attribute values.
632
+ # Do we match the attributes?
633
+ # Return T/F, and call match recursively as necessary.
634
+ #++
635
+ def match(entry)
636
+ case @op
637
+ when :eq
638
+ if @right == "*"
639
+ l = entry[@left] and l.length > 0
640
+ else
641
+ l = entry[@left] and l = Array(l) and l.index(@right)
642
+ end
643
+ else
644
+ raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
645
+ end
646
+ end
647
+
648
+ ##
649
+ # Converts escaped characters (e.g., "\\28") to unescaped characters
650
+ # ("(").
651
+ def unescape(right)
652
+ right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
653
+ end
654
+ private :unescape
655
+
656
+ ##
657
+ # Parses RFC 2254-style string representations of LDAP filters into Filter
658
+ # object hierarchies.
659
+ class FilterParser #:nodoc:
660
+ ##
661
+ # The constructed filter.
662
+ attr_reader :filter
663
+
664
+ class << self
665
+ private :new
666
+
667
+ ##
668
+ # Construct a filter tree from the provided string and return it.
669
+ def parse(ldap_filter_string)
670
+ new(ldap_filter_string).filter
671
+ end
672
+ end
673
+
674
+ def initialize(str)
675
+ require 'strscan' # Don't load strscan until we need it.
676
+ @filter = parse(StringScanner.new(str))
677
+ raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
678
+ end
679
+
680
+ ##
681
+ # Parse the string contained in the StringScanner provided. Parsing
682
+ # tries to parse a standalone expression first. If that fails, it tries
683
+ # to parse a parenthesized expression.
684
+ def parse(scanner)
685
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
686
+ end
687
+ private :parse
688
+
689
+ ##
690
+ # Join ("&") and intersect ("|") operations are presented in branches.
691
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
692
+ # test1 and test2. Each of these is parsed separately and then pushed
693
+ # into a branch array for filter merging using the parent operation.
694
+ #
695
+ # This method parses the branch text out into an array of filter
696
+ # objects.
697
+ def parse_branches(scanner)
698
+ branches = []
699
+ while branch = parse_paren_expression(scanner)
700
+ branches << branch
701
+ end
702
+ branches
703
+ end
704
+ private :parse_branches
705
+
706
+ ##
707
+ # Join ("&") and intersect ("|") operations are presented in branches.
708
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
709
+ # test1 and test2. Each of these is parsed separately and then pushed
710
+ # into a branch array for filter merging using the parent operation.
711
+ #
712
+ # This method calls #parse_branches to generate the branch list and then
713
+ # merges them into a single Filter tree by calling the provided
714
+ # operation.
715
+ def merge_branches(op, scanner)
716
+ filter = nil
717
+ branches = parse_branches(scanner)
718
+
719
+ if branches.size >= 1
720
+ filter = branches.shift
721
+ while not branches.empty?
722
+ filter = filter.__send__(op, branches.shift)
723
+ end
724
+ end
725
+
726
+ filter
727
+ end
728
+ private :merge_branches
729
+
730
+ def parse_paren_expression(scanner)
731
+ if scanner.scan(/\s*\(\s*/)
732
+ expr = if scanner.scan(/\s*\&\s*/)
733
+ merge_branches(:&, scanner)
734
+ elsif scanner.scan(/\s*\|\s*/)
735
+ merge_branches(:|, scanner)
736
+ elsif scanner.scan(/\s*\!\s*/)
737
+ br = parse_paren_expression(scanner)
738
+ ~br if br
739
+ else
740
+ parse_filter_branch(scanner)
741
+ end
742
+
743
+ if expr and scanner.scan(/\s*\)\s*/)
744
+ expr
745
+ end
746
+ end
747
+ end
748
+ private :parse_paren_expression
749
+
750
+ ##
751
+ # This parses a given expression inside of parentheses.
752
+ def parse_filter_branch(scanner)
753
+ scanner.scan(/\s*/)
754
+ if token = scanner.scan(/[-\w:.]*[\w]/)
755
+ scanner.scan(/\s*/)
756
+ if op = scanner.scan(/<=|>=|!=|:=|=/)
757
+ scanner.scan(/\s*/)
758
+ if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s\xC3\x80-\xCA\xAF]|\\[a-fA-F\d]{2})+/)
759
+ # 20100313 AZ: Assumes that "(uid=george*)" is the same as
760
+ # "(uid=george* )". The standard doesn't specify, but I can find
761
+ # no examples that suggest otherwise.
762
+ value.strip!
763
+ case op
764
+ when "="
765
+ Net::LDAP::Filter.eq(token, value)
766
+ when "!="
767
+ Net::LDAP::Filter.ne(token, value)
768
+ when "<="
769
+ Net::LDAP::Filter.le(token, value)
770
+ when ">="
771
+ Net::LDAP::Filter.ge(token, value)
772
+ when ":="
773
+ Net::LDAP::Filter.ex(token, value)
774
+ end
775
+ end
776
+ end
777
+ end
778
+ end
779
+ private :parse_filter_branch
780
+ end # class Net::LDAP::FilterParser
781
+ end # class Net::LDAP::Filter