rubinius-net-ldap 0.11

Sign up to get free protection for your applications and to get access to all the features.
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