rubinius-net-ldap 0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +5 -0
  4. data/.rubocop_todo.yml +462 -0
  5. data/.travis.yml +19 -0
  6. data/CONTRIBUTING.md +54 -0
  7. data/Contributors.rdoc +24 -0
  8. data/Gemfile +2 -0
  9. data/Hacking.rdoc +63 -0
  10. data/History.rdoc +260 -0
  11. data/License.rdoc +29 -0
  12. data/README.rdoc +65 -0
  13. data/Rakefile +17 -0
  14. data/lib/net-ldap.rb +2 -0
  15. data/lib/net/ber.rb +320 -0
  16. data/lib/net/ber/ber_parser.rb +182 -0
  17. data/lib/net/ber/core_ext.rb +55 -0
  18. data/lib/net/ber/core_ext/array.rb +96 -0
  19. data/lib/net/ber/core_ext/false_class.rb +10 -0
  20. data/lib/net/ber/core_ext/integer.rb +74 -0
  21. data/lib/net/ber/core_ext/string.rb +66 -0
  22. data/lib/net/ber/core_ext/true_class.rb +11 -0
  23. data/lib/net/ldap.rb +1229 -0
  24. data/lib/net/ldap/connection.rb +702 -0
  25. data/lib/net/ldap/dataset.rb +168 -0
  26. data/lib/net/ldap/dn.rb +225 -0
  27. data/lib/net/ldap/entry.rb +193 -0
  28. data/lib/net/ldap/error.rb +38 -0
  29. data/lib/net/ldap/filter.rb +778 -0
  30. data/lib/net/ldap/instrumentation.rb +23 -0
  31. data/lib/net/ldap/password.rb +38 -0
  32. data/lib/net/ldap/pdu.rb +297 -0
  33. data/lib/net/ldap/version.rb +5 -0
  34. data/lib/net/snmp.rb +264 -0
  35. data/rubinius-net-ldap.gemspec +37 -0
  36. data/script/install-openldap +112 -0
  37. data/script/package +7 -0
  38. data/script/release +16 -0
  39. data/test/ber/core_ext/test_array.rb +22 -0
  40. data/test/ber/core_ext/test_string.rb +25 -0
  41. data/test/ber/test_ber.rb +99 -0
  42. data/test/fixtures/cacert.pem +20 -0
  43. data/test/fixtures/openldap/memberof.ldif +33 -0
  44. data/test/fixtures/openldap/retcode.ldif +76 -0
  45. data/test/fixtures/openldap/slapd.conf.ldif +67 -0
  46. data/test/fixtures/seed.ldif +374 -0
  47. data/test/integration/test_add.rb +28 -0
  48. data/test/integration/test_ber.rb +30 -0
  49. data/test/integration/test_bind.rb +34 -0
  50. data/test/integration/test_delete.rb +31 -0
  51. data/test/integration/test_open.rb +88 -0
  52. data/test/integration/test_return_codes.rb +38 -0
  53. data/test/integration/test_search.rb +77 -0
  54. data/test/support/vm/openldap/.gitignore +1 -0
  55. data/test/support/vm/openldap/README.md +32 -0
  56. data/test/support/vm/openldap/Vagrantfile +33 -0
  57. data/test/test_dn.rb +44 -0
  58. data/test/test_entry.rb +65 -0
  59. data/test/test_filter.rb +223 -0
  60. data/test/test_filter_parser.rb +20 -0
  61. data/test/test_helper.rb +66 -0
  62. data/test/test_ldap.rb +60 -0
  63. data/test/test_ldap_connection.rb +404 -0
  64. data/test/test_ldif.rb +104 -0
  65. data/test/test_password.rb +10 -0
  66. data/test/test_rename.rb +77 -0
  67. data/test/test_search.rb +39 -0
  68. data/test/test_snmp.rb +119 -0
  69. data/test/test_ssl_ber.rb +40 -0
  70. data/test/testdata.ldif +101 -0
  71. data/testserver/ldapserver.rb +210 -0
  72. data/testserver/testdata.ldif +101 -0
  73. metadata +204 -0
@@ -0,0 +1,38 @@
1
+ class Net::LDAP
2
+ class LdapError < StandardError
3
+ def message
4
+ "Deprecation warning: Net::LDAP::LdapError is no longer used. Use Net::LDAP::Error or rescue one of it's subclasses. \n" + super
5
+ end
6
+ end
7
+
8
+ class Error < StandardError; end
9
+
10
+ class AlreadyOpenedError < Error; end
11
+ class SocketError < Error; end
12
+ class ConnectionRefusedError < Error; end
13
+ class NoOpenSSLError < Error; end
14
+ class NoStartTLSResultError < Error; end
15
+ class NoSearchBaseError < Error; end
16
+ class StartTLSError < Error; end
17
+ class EncryptionUnsupportedError < Error; end
18
+ class EncMethodUnsupportedError < Error; end
19
+ class AuthMethodUnsupportedError < Error; end
20
+ class BindingInformationInvalidError < Error; end
21
+ class NoBindResultError < Error; end
22
+ class SASLChallengeOverflowError < Error; end
23
+ class SearchSizeInvalidError < Error; end
24
+ class SearchScopeInvalidError < Error; end
25
+ class ResponseTypeInvalidError < Error; end
26
+ class ResponseMissingOrInvalidError < Error; end
27
+ class EmptyDNError < Error; end
28
+ class HashTypeUnsupportedError < Error; end
29
+ class OperatorError < Error; end
30
+ class SubstringFilterError < Error; end
31
+ class SearchFilterError < Error; end
32
+ class BERInvalidError < Error; end
33
+ class SearchFilterTypeUnknownError < Error; end
34
+ class BadAttributeError < Error; end
35
+ class FilterTypeUnknownError < Error; end
36
+ class FilterSyntaxInvalidError < Error; end
37
+ class EntryOverflowError < Error; end
38
+ end
@@ -0,0 +1,778 @@
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::OperatorError, "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::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0
294
+ str += escape(b)
295
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
296
+ str += "*#{escape(b)}"
297
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
298
+ str += "*#{escape(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::SearchFilterError, "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::BERInvalidError, "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::SearchFilterTypeUnknownError, "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, :bineq
418
+ "#{@left}=#{@right}"
419
+ when :ex
420
+ "#{@left}:=#{@right}"
421
+ when :ge
422
+ "#{@left}>=#{@right}"
423
+ when :le
424
+ "#{@left}<=#{@right}"
425
+ when :and
426
+ "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
427
+ when :or
428
+ "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
429
+ when :not
430
+ "!(#{@left.to_raw_rfc2254})"
431
+ end
432
+ end
433
+
434
+ ##
435
+ # Converts the Filter object to an RFC 2254-compatible text format.
436
+ def to_rfc2254
437
+ "(#{to_raw_rfc2254})"
438
+ end
439
+
440
+ def to_s
441
+ to_rfc2254
442
+ end
443
+
444
+ ##
445
+ # Converts the filter to BER format.
446
+ #--
447
+ # Filter ::=
448
+ # CHOICE {
449
+ # and [0] SET OF Filter,
450
+ # or [1] SET OF Filter,
451
+ # not [2] Filter,
452
+ # equalityMatch [3] AttributeValueAssertion,
453
+ # substrings [4] SubstringFilter,
454
+ # greaterOrEqual [5] AttributeValueAssertion,
455
+ # lessOrEqual [6] AttributeValueAssertion,
456
+ # present [7] AttributeType,
457
+ # approxMatch [8] AttributeValueAssertion,
458
+ # extensibleMatch [9] MatchingRuleAssertion
459
+ # }
460
+ #
461
+ # SubstringFilter ::=
462
+ # SEQUENCE {
463
+ # type AttributeType,
464
+ # SEQUENCE OF CHOICE {
465
+ # initial [0] LDAPString,
466
+ # any [1] LDAPString,
467
+ # final [2] LDAPString
468
+ # }
469
+ # }
470
+ #
471
+ # MatchingRuleAssertion ::=
472
+ # SEQUENCE {
473
+ # matchingRule [1] MatchingRuleId OPTIONAL,
474
+ # type [2] AttributeDescription OPTIONAL,
475
+ # matchValue [3] AssertionValue,
476
+ # dnAttributes [4] BOOLEAN DEFAULT FALSE
477
+ # }
478
+ #
479
+ # Matching Rule Suffixes
480
+ # Less than [.1] or .[lt]
481
+ # Less than or equal to [.2] or [.lte]
482
+ # Equality [.3] or [.eq] (default)
483
+ # Greater than or equal to [.4] or [.gte]
484
+ # Greater than [.5] or [.gt]
485
+ # Substring [.6] or [.sub]
486
+ #
487
+ #++
488
+ def to_ber
489
+ case @op
490
+ when :eq
491
+ if @right == "*" # presence test
492
+ @left.to_s.to_ber_contextspecific(7)
493
+ elsif @right =~ /[*]/ # substring
494
+ # Parsing substrings is a little tricky. We use String#split to
495
+ # break a string into substrings delimited by the * (star)
496
+ # character. But we also need to know whether there is a star at the
497
+ # head and tail of the string, so we use a limit parameter value of
498
+ # -1: "If negative, there is no limit to the number of fields
499
+ # returned, and trailing null fields are not suppressed."
500
+ #
501
+ # 20100320 AZ: This is much simpler than the previous verison. Also,
502
+ # unnecessary regex escaping has been removed.
503
+
504
+ ary = @right.split(/[*]+/, -1)
505
+
506
+ if ary.first.empty?
507
+ first = nil
508
+ ary.shift
509
+ else
510
+ first = unescape(ary.shift).to_ber_contextspecific(0)
511
+ end
512
+
513
+ if ary.last.empty?
514
+ last = nil
515
+ ary.pop
516
+ else
517
+ last = unescape(ary.pop).to_ber_contextspecific(2)
518
+ end
519
+
520
+ seq = ary.map { |e| unescape(e).to_ber_contextspecific(1) }
521
+ seq.unshift first if first
522
+ seq.push last if last
523
+
524
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
525
+ else # equality
526
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
527
+ end
528
+ when :bineq
529
+ # make sure data is not forced to UTF-8
530
+ [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3)
531
+ when :ex
532
+ seq = []
533
+
534
+ unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
535
+ raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}"
536
+ end
537
+ type, dn, rule = $1, $2, $4
538
+
539
+ seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
540
+ seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
541
+ seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
542
+ seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
543
+
544
+ seq.to_ber_contextspecific(9)
545
+ when :ge
546
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
547
+ when :le
548
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
549
+ when :ne
550
+ [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
551
+ when :and
552
+ ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
553
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
554
+ when :or
555
+ ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
556
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
557
+ when :not
558
+ [@left.to_ber].to_ber_contextspecific(2)
559
+ end
560
+ end
561
+
562
+ ##
563
+ # Perform filter operations against a user-supplied block. This is useful
564
+ # when implementing an LDAP directory server. The caller's block will be
565
+ # called with two arguments: first, a symbol denoting the "operation" of
566
+ # the filter; and second, an array consisting of arguments to the
567
+ # operation. The user-supplied block (which is MANDATORY) should perform
568
+ # some desired application-defined processing, and may return a
569
+ # locally-meaningful object that will appear as a parameter in the :and,
570
+ # :or and :not operations detailed below.
571
+ #
572
+ # A typical object to return from the user-supplied block is an array of
573
+ # Net::LDAP::Filter objects.
574
+ #
575
+ # These are the possible values that may be passed to the user-supplied
576
+ # block:
577
+ # * :equalityMatch (the arguments will be an attribute name and a value
578
+ # to be matched);
579
+ # * :substrings (two arguments: an attribute name and a value containing
580
+ # one or more "*" characters);
581
+ # * :present (one argument: an attribute name);
582
+ # * :greaterOrEqual (two arguments: an attribute name and a value to be
583
+ # compared against);
584
+ # * :lessOrEqual (two arguments: an attribute name and a value to be
585
+ # compared against);
586
+ # * :and (two or more arguments, each of which is an object returned
587
+ # from a recursive call to #execute, with the same block;
588
+ # * :or (two or more arguments, each of which is an object returned from
589
+ # a recursive call to #execute, with the same block; and
590
+ # * :not (one argument, which is an object returned from a recursive
591
+ # call to #execute with the the same block.
592
+ def execute(&block)
593
+ case @op
594
+ when :eq
595
+ if @right == "*"
596
+ yield :present, @left
597
+ elsif @right.index '*'
598
+ yield :substrings, @left, @right
599
+ else
600
+ yield :equalityMatch, @left, @right
601
+ end
602
+ when :ge
603
+ yield :greaterOrEqual, @left, @right
604
+ when :le
605
+ yield :lessOrEqual, @left, @right
606
+ when :or, :and
607
+ yield @op, (@left.execute(&block)), (@right.execute(&block))
608
+ when :not
609
+ yield @op, (@left.execute(&block))
610
+ end || []
611
+ end
612
+
613
+ ##
614
+ # This is a private helper method for dealing with chains of ANDs and ORs
615
+ # that are longer than two. If BOTH of our branches are of the specified
616
+ # type of joining operator, then return both of them as an array (calling
617
+ # coalesce recursively). If they're not, then return an array consisting
618
+ # only of self.
619
+ def coalesce(operator) #:nodoc:
620
+ if @op == operator
621
+ [@left.coalesce(operator), @right.coalesce(operator)]
622
+ else
623
+ [self]
624
+ end
625
+ end
626
+
627
+ ##
628
+ #--
629
+ # We got a hash of attribute values.
630
+ # Do we match the attributes?
631
+ # Return T/F, and call match recursively as necessary.
632
+ #++
633
+ def match(entry)
634
+ case @op
635
+ when :eq
636
+ if @right == "*"
637
+ l = entry[@left] and l.length > 0
638
+ else
639
+ l = entry[@left] and l = Array(l) and l.index(@right)
640
+ end
641
+ else
642
+ raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}"
643
+ end
644
+ end
645
+
646
+ ##
647
+ # Converts escaped characters (e.g., "\\28") to unescaped characters
648
+ def unescape(right)
649
+ right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
650
+ end
651
+ private :unescape
652
+
653
+ ##
654
+ # Parses RFC 2254-style string representations of LDAP filters into Filter
655
+ # object hierarchies.
656
+ class FilterParser #:nodoc:
657
+ ##
658
+ # The constructed filter.
659
+ attr_reader :filter
660
+
661
+ class << self
662
+ private :new
663
+
664
+ ##
665
+ # Construct a filter tree from the provided string and return it.
666
+ def parse(ldap_filter_string)
667
+ new(ldap_filter_string).filter
668
+ end
669
+ end
670
+
671
+ def initialize(str)
672
+ require 'strscan' # Don't load strscan until we need it.
673
+ @filter = parse(StringScanner.new(str))
674
+ raise Net::LDAP::FilterSyntaxInvalidError, "Invalid filter syntax." unless @filter
675
+ end
676
+
677
+ ##
678
+ # Parse the string contained in the StringScanner provided. Parsing
679
+ # tries to parse a standalone expression first. If that fails, it tries
680
+ # to parse a parenthesized expression.
681
+ def parse(scanner)
682
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
683
+ end
684
+ private :parse
685
+
686
+ ##
687
+ # Join ("&") and intersect ("|") operations are presented in branches.
688
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
689
+ # test1 and test2. Each of these is parsed separately and then pushed
690
+ # into a branch array for filter merging using the parent operation.
691
+ #
692
+ # This method parses the branch text out into an array of filter
693
+ # objects.
694
+ def parse_branches(scanner)
695
+ branches = []
696
+ while branch = parse_paren_expression(scanner)
697
+ branches << branch
698
+ end
699
+ branches
700
+ end
701
+ private :parse_branches
702
+
703
+ ##
704
+ # Join ("&") and intersect ("|") operations are presented in branches.
705
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
706
+ # test1 and test2. Each of these is parsed separately and then pushed
707
+ # into a branch array for filter merging using the parent operation.
708
+ #
709
+ # This method calls #parse_branches to generate the branch list and then
710
+ # merges them into a single Filter tree by calling the provided
711
+ # operation.
712
+ def merge_branches(op, scanner)
713
+ filter = nil
714
+ branches = parse_branches(scanner)
715
+
716
+ if branches.size >= 1
717
+ filter = branches.shift
718
+ while not branches.empty?
719
+ filter = filter.__send__(op, branches.shift)
720
+ end
721
+ end
722
+
723
+ filter
724
+ end
725
+ private :merge_branches
726
+
727
+ def parse_paren_expression(scanner)
728
+ if scanner.scan(/\s*\(\s*/)
729
+ expr = if scanner.scan(/\s*\&\s*/)
730
+ merge_branches(:&, scanner)
731
+ elsif scanner.scan(/\s*\|\s*/)
732
+ merge_branches(:|, scanner)
733
+ elsif scanner.scan(/\s*\!\s*/)
734
+ br = parse_paren_expression(scanner)
735
+ ~br if br
736
+ else
737
+ parse_filter_branch(scanner)
738
+ end
739
+
740
+ if expr and scanner.scan(/\s*\)\s*/)
741
+ expr
742
+ end
743
+ end
744
+ end
745
+ private :parse_paren_expression
746
+
747
+ ##
748
+ # This parses a given expression inside of parentheses.
749
+ def parse_filter_branch(scanner)
750
+ scanner.scan(/\s*/)
751
+ if token = scanner.scan(/[-\w:.]*[\w]/)
752
+ scanner.scan(/\s*/)
753
+ if op = scanner.scan(/<=|>=|!=|:=|=/)
754
+ scanner.scan(/\s*/)
755
+ if value = scanner.scan(/(?:[-\[\]{}\w*.+:@=,#\$%&!'^~\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u)
756
+ # 20100313 AZ: Assumes that "(uid=george*)" is the same as
757
+ # "(uid=george* )". The standard doesn't specify, but I can find
758
+ # no examples that suggest otherwise.
759
+ value.strip!
760
+ case op
761
+ when "="
762
+ Net::LDAP::Filter.eq(token, value)
763
+ when "!="
764
+ Net::LDAP::Filter.ne(token, value)
765
+ when "<="
766
+ Net::LDAP::Filter.le(token, value)
767
+ when ">="
768
+ Net::LDAP::Filter.ge(token, value)
769
+ when ":="
770
+ Net::LDAP::Filter.ex(token, value)
771
+ end
772
+ end
773
+ end
774
+ end
775
+ end
776
+ private :parse_filter_branch
777
+ end # class Net::LDAP::FilterParser
778
+ end # class Net::LDAP::Filter