net-ldap 0.1.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of net-ldap might be problematic. Click here for more details.

Files changed (58) hide show
  1. data/.autotest +11 -0
  2. data/.gemtest +0 -0
  3. data/.rspec +2 -0
  4. data/Contributors.rdoc +21 -0
  5. data/Hacking.rdoc +68 -0
  6. data/{History.txt → History.rdoc} +65 -1
  7. data/License.rdoc +29 -0
  8. data/Manifest.txt +25 -14
  9. data/README.rdoc +52 -0
  10. data/Rakefile +52 -96
  11. data/autotest/discover.rb +1 -0
  12. data/lib/net-ldap.rb +1 -0
  13. data/lib/net/ber.rb +302 -81
  14. data/lib/net/ber/ber_parser.rb +153 -97
  15. data/lib/net/ber/core_ext.rb +62 -0
  16. data/lib/net/ber/core_ext/array.rb +82 -0
  17. data/lib/net/ber/core_ext/bignum.rb +22 -0
  18. data/lib/net/ber/core_ext/false_class.rb +10 -0
  19. data/lib/net/ber/core_ext/fixnum.rb +66 -0
  20. data/lib/net/ber/core_ext/string.rb +48 -0
  21. data/lib/net/ber/core_ext/true_class.rb +12 -0
  22. data/lib/net/ldap.rb +1455 -1475
  23. data/lib/net/ldap/dataset.rb +134 -79
  24. data/lib/net/ldap/dn.rb +225 -0
  25. data/lib/net/ldap/entry.rb +168 -249
  26. data/lib/net/ldap/filter.rb +654 -387
  27. data/lib/net/ldap/password.rb +31 -0
  28. data/lib/net/ldap/pdu.rb +232 -233
  29. data/lib/net/snmp.rb +4 -31
  30. data/net-ldap.gemspec +59 -0
  31. data/spec/integration/ssl_ber_spec.rb +3 -0
  32. data/spec/spec_helper.rb +2 -2
  33. data/spec/unit/ber/ber_spec.rb +82 -6
  34. data/spec/unit/ber/core_ext/string_spec.rb +51 -0
  35. data/spec/unit/ldap/dn_spec.rb +80 -0
  36. data/spec/unit/ldap/entry_spec.rb +51 -0
  37. data/spec/unit/ldap/filter_spec.rb +84 -0
  38. data/spec/unit/ldap_spec.rb +48 -0
  39. data/test/test_entry.rb +54 -2
  40. data/test/test_filter.rb +93 -54
  41. data/test/test_ldap_connection.rb +24 -0
  42. data/test/test_ldif.rb +31 -23
  43. data/test/test_rename.rb +77 -0
  44. data/test/test_snmp.rb +34 -33
  45. metadata +88 -52
  46. data/COPYING +0 -272
  47. data/LICENSE +0 -56
  48. data/README.txt +0 -68
  49. data/lib/net/ldap/core_ext/all.rb +0 -43
  50. data/lib/net/ldap/core_ext/array.rb +0 -42
  51. data/lib/net/ldap/core_ext/bignum.rb +0 -25
  52. data/lib/net/ldap/core_ext/false_class.rb +0 -11
  53. data/lib/net/ldap/core_ext/fixnum.rb +0 -74
  54. data/lib/net/ldap/core_ext/string.rb +0 -40
  55. data/lib/net/ldap/core_ext/true_class.rb +0 -11
  56. data/lib/net/ldap/psw.rb +0 -57
  57. data/lib/net/ldif.rb +0 -34
  58. data/test/test_ber.rb +0 -78
@@ -1,266 +1,185 @@
1
- # LDAP Entry (search-result) support classes
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # Objects of this class represent individual entries in an LDAP directory.
4
+ # User code generally does not instantiate this class. Net::LDAP#search
5
+ # provides objects of this class to user code, either as block parameters or
6
+ # as return values.
2
7
  #
3
- #----------------------------------------------------------------------------
8
+ # In LDAP-land, an "entry" is a collection of attributes that are uniquely
9
+ # and globally identified by a DN ("Distinguished Name"). Attributes are
10
+ # identified by short, descriptive words or phrases. Although a directory is
11
+ # free to implement any attribute name, most of them follow rigorous
12
+ # standards so that the range of commonly-encountered attribute names is not
13
+ # large.
4
14
  #
5
- # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
15
+ # An attribute name is case-insensitive. Most directories also restrict the
16
+ # range of characters allowed in attribute names. To simplify handling
17
+ # attribute names, Net::LDAP::Entry internally converts them to a standard
18
+ # format. Therefore, the methods which take attribute names can take Strings
19
+ # or Symbols, and work correctly regardless of case or capitalization.
6
20
  #
7
- # Gmail: garbagecat10
21
+ # An attribute consists of zero or more data items called <i>values.</i> An
22
+ # entry is the combination of a unique DN, a set of attribute names, and a
23
+ # (possibly-empty) array of values for each attribute.
8
24
  #
9
- # This program is free software; you can redistribute it and/or modify
10
- # it under the terms of the GNU General Public License as published by
11
- # the Free Software Foundation; either version 2 of the License, or
12
- # (at your option) any later version.
25
+ # Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
26
+ # entries. In addition to the methods documented below, you may access
27
+ # individual attributes of an entry simply by giving the attribute name as
28
+ # the name of a method call. For example:
13
29
  #
14
- # This program is distributed in the hope that it will be useful,
15
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
- # GNU General Public License for more details.
30
+ # ldap.search( ... ) do |entry|
31
+ # puts "Common name: #{entry.cn}"
32
+ # puts "Email addresses:"
33
+ # entry.mail.each {|ma| puts ma}
34
+ # end
18
35
  #
19
- # You should have received a copy of the GNU General Public License
20
- # along with this program; if not, write to the Free Software
21
- # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
36
+ # If you use this technique to access an attribute that is not present in a
37
+ # particular Entry object, a NoMethodError exception will be raised.
22
38
  #
23
- #---------------------------------------------------------------------------
24
- #
25
-
26
- module Net
27
- class LDAP
28
-
29
-
30
- # Objects of this class represent individual entries in an LDAP directory.
31
- # User code generally does not instantiate this class. Net::LDAP#search
32
- # provides objects of this class to user code, either as block parameters or
33
- # as return values.
34
- #
35
- # In LDAP-land, an "entry" is a collection of attributes that are uniquely
36
- # and globally identified by a DN ("Distinguished Name"). Attributes are
37
- # identified by short, descriptive words or phrases. Although a directory is
38
- # free to implement any attribute name, most of them follow rigorous
39
- # standards so that the range of commonly-encountered attribute names is not
40
- # large.
41
- #
42
- # An attribute name is case-insensitive. Most directories also restrict the
43
- # range of characters allowed in attribute names. To simplify handling
44
- # attribute names, Net::LDAP::Entry internally converts them to a standard
45
- # format. Therefore, the methods which take attribute names can take Strings
46
- # or Symbols, and work correctly regardless of case or capitalization.
39
+ #--
40
+ # Ugly problem to fix someday: We key off the internal hash with a canonical
41
+ # form of the attribute name: convert to a string, downcase, then take the
42
+ # symbol. Unfortunately we do this in at least three places. Should do it in
43
+ # ONE place.
44
+ class Net::LDAP::Entry
45
+ ##
46
+ # This constructor is not generally called by user code.
47
+ def initialize(dn = nil) #:nodoc:
48
+ @myhash = {}
49
+ @myhash[:dn] = [dn]
50
+ end
51
+
52
+ ##
53
+ # Use the LDIF format for Marshal serialization.
54
+ def _dump(depth) #:nodoc:
55
+ to_ldif
56
+ end
57
+
58
+ ##
59
+ # Use the LDIF format for Marshal serialization.
60
+ def self._load(entry) #:nodoc:
61
+ from_single_ldif_string(entry)
62
+ end
63
+
64
+ class << self
65
+ ##
66
+ # Converts a single LDIF entry string into an Entry object. Useful for
67
+ # Marshal serialization. If a string with multiple LDIF entries is
68
+ # provided, an exception will be raised.
69
+ def from_single_ldif_string(ldif)
70
+ ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif))
71
+
72
+ return nil if ds.empty?
73
+
74
+ raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1
75
+
76
+ entry = ds.to_entries.first
77
+
78
+ return nil if entry.dn.nil?
79
+ entry
80
+ end
81
+
82
+ ##
83
+ # Canonicalizes an LDAP attribute name as a \Symbol. The name is
84
+ # lowercased and, if present, a trailing equals sign is removed.
85
+ def attribute_name(name)
86
+ name = name.to_s.downcase
87
+ name = name[0..-2] if name[-1] == ?=
88
+ name.to_sym
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Sets or replaces the array of values for the provided attribute. The
94
+ # attribute name is canonicalized prior to assignment.
47
95
  #
48
- # An attribute consists of zero or more data items called <i>values.</i> An
49
- # entry is the combination of a unique DN, a set of attribute names, and a
50
- # (possibly-empty) array of values for each attribute.
96
+ # When an attribute is set using this, that attribute is now made
97
+ # accessible through methods as well.
51
98
  #
52
- # Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
53
- # entries. In addition to the methods documented below, you may access
54
- # individual attributes of an entry simply by giving the attribute name as
55
- # the name of a method call. For example:
99
+ # entry = Net::LDAP::Entry.new("dc=com")
100
+ # entry.foo # => NoMethodError
101
+ # entry["foo"] = 12345 # => [12345]
102
+ # entry.foo # => [12345]
103
+ def []=(name, value)
104
+ @myhash[self.class.attribute_name(name)] = Kernel::Array(value)
105
+ end
106
+
107
+ ##
108
+ # Reads the array of values for the provided attribute. The attribute name
109
+ # is canonicalized prior to reading. Returns an empty array if the
110
+ # attribute does not exist.
111
+ def [](name)
112
+ name = self.class.attribute_name(name)
113
+ @myhash[name] || []
114
+ end
115
+
116
+ ##
117
+ # Returns the first distinguished name (dn) of the Entry as a \String.
118
+ def dn
119
+ self[:dn].first.to_s
120
+ end
121
+
122
+ ##
123
+ # Returns an array of the attribute names present in the Entry.
124
+ def attribute_names
125
+ @myhash.keys
126
+ end
127
+
128
+ ##
129
+ # Accesses each of the attributes present in the Entry.
56
130
  #
57
- # ldap.search( ... ) do |entry|
58
- # puts "Common name: #{entry.cn}"
59
- # puts "Email addresses:"
60
- # entry.mail.each {|ma| puts ma}
61
- # end
62
- #
63
- # If you use this technique to access an attribute that is not present in a
64
- # particular Entry object, a NoMethodError exception will be raised.
65
- #
66
- #--
67
- # Ugly problem to fix someday: We key off the internal hash with a canonical
68
- # form of the attribute name: convert to a string, downcase, then take the
69
- # symbol. Unfortunately we do this in at least three places. Should do it in
70
- # ONE place.
71
- #
72
- class Entry
73
- # This constructor is not generally called by user code.
74
- #
75
- def initialize dn = nil # :nodoc:
76
- @myhash = {}
77
- @myhash[:dn] = [dn]
78
- end
79
-
80
- def _dump depth
81
- to_ldif
82
- end
83
-
84
- class << self
85
- def _load entry
86
- from_single_ldif_string entry
87
- end
88
- end
89
-
90
- #--
91
- # Discovered bug, 26Aug06: I noticed that we're not converting the
92
- # incoming value to an array if it isn't already one.
93
- def []=(name, value) # :nodoc:
94
- sym = attribute_name(name)
95
- value = [value] unless value.is_a?(Array)
96
- @myhash[sym] = value
97
- end
98
-
99
- #--
100
- # We have to deal with this one as we do with []= because this one and not
101
- # the other one gets called in formulations like entry["CN"] << cn.
102
- #
103
- def [](name) # :nodoc:
104
- name = attribute_name(name) unless name.is_a?(Symbol)
105
- @myhash[name] || []
106
- end
107
-
108
- # Returns the dn of the Entry as a String.
109
- def dn
110
- self[:dn][0].to_s
111
- end
112
-
113
- # Returns an array of the attribute names present in the Entry.
114
- def attribute_names
115
- @myhash.keys
116
- end
117
-
118
- # Accesses each of the attributes present in the Entry.
119
- # Calls a user-supplied block with each attribute in turn,
120
- # passing two arguments to the block: a Symbol giving
121
- # the name of the attribute, and a (possibly empty)
122
- # Array of data values.
123
- #
124
- def each
125
- if block_given?
126
- attribute_names.each {|a|
127
- attr_name,values = a,self[a]
128
- yield attr_name, values
129
- }
130
- end
131
- end
132
-
133
- alias_method :each_attribute, :each
134
-
135
- # Converts the Entry to a String, representing the
136
- # Entry's attributes in LDIF format.
137
- #--
138
- def to_ldif
139
- ary = []
140
- ary << "dn: #{dn}\n"
141
- v2 = "" # temp value, save on GC
142
- each_attribute do |k,v|
143
- unless k == :dn
144
- v.each {|v1|
145
- v2 = if (k == :userpassword) || is_attribute_value_binary?(v1)
146
- ": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}"
147
- else
148
- " #{v1}"
149
- end
150
- ary << "#{k}:#{v2}\n"
151
- }
152
- end
153
- end
154
- ary << "\n"
155
- ary.join
156
- end
157
-
158
- #--
159
- # TODO, doesn't support broken lines.
160
- # It generates a SINGLE Entry object from an incoming LDIF stream which is
161
- # of course useless for big LDIF streams that encode many objects.
162
- #
163
- # DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED.
164
- #
165
- # As it is, it's useful for unmarshalling objects that we create, but not
166
- # for reading arbitrary LDIF files. Eventually, we should have a class
167
- # method that parses large LDIF streams into individual LDIF blocks
168
- # (delimited by blank lines) and passes them here.
169
- #
170
- class << self
171
- def from_single_ldif_string ldif
172
- entry = Entry.new
173
- entry[:dn] = []
174
- ldif.split(/\r?\n/m).each {|line|
175
- break if line.length == 0
176
- if line =~ /\A([\w]+):(:?)[\s]*/
177
- entry[$1] <<= if $2 == ':'
178
- Base64.decode64($')
179
- else
180
- $'
181
- end
182
- end
183
- }
184
- entry.dn ? entry : nil
185
- end
186
- end
187
-
188
- #--
189
- # Part of the support for getter and setter style access to attributes.
190
- #
191
- def respond_to?(sym)
192
- name = attribute_name(sym)
193
- return true if valid_attribute?(name)
194
- return super
131
+ # Calls a user-supplied block with each attribute in turn, passing two
132
+ # arguments to the block: a Symbol giving the name of the attribute, and a
133
+ # (possibly empty) \Array of data values.
134
+ def each # :yields: attribute-name, data-values-array
135
+ if block_given?
136
+ attribute_names.each {|a|
137
+ attr_name,values = a,self[a]
138
+ yield attr_name, values
139
+ }
195
140
  end
196
-
197
- #--
198
- # Supports getter and setter style access for all the attributes that this
199
- # entry holds.
200
- #
201
- def method_missing sym, *args, &block # :nodoc:
202
- name = attribute_name(sym)
203
-
204
- if valid_attribute? name
205
- if setter?(sym) && args.size == 1
206
- value = args.first
207
- value = [value] unless value.instance_of?(Array)
208
- self[name]= value
209
-
210
- return value
211
- elsif args.empty?
212
- return self[name]
213
- end
141
+ end
142
+ alias_method :each_attribute, :each
143
+
144
+ ##
145
+ # Converts the Entry to an LDIF-formatted String
146
+ def to_ldif
147
+ Net::LDAP::Dataset.from_entry(self).to_ldif_string
148
+ end
149
+
150
+ def respond_to?(sym) #:nodoc:
151
+ return true if valid_attribute?(self.class.attribute_name(sym))
152
+ return super
153
+ end
154
+
155
+ def method_missing(sym, *args, &block) #:nodoc:
156
+ name = self.class.attribute_name(sym)
157
+
158
+ if valid_attribute?(name )
159
+ if setter?(sym) && args.size == 1
160
+ value = args.first
161
+ value = Array(value)
162
+ self[name]= value
163
+ return value
164
+ elsif args.empty?
165
+ return self[name]
214
166
  end
215
-
216
- super
217
167
  end
218
168
 
219
- def write
220
- end
221
-
222
- private
169
+ super
170
+ end
223
171
 
224
- #--
225
- # Internal convenience method. It seems like the standard
226
- # approach in most LDAP tools to base64 encode an attribute
227
- # value if its first or last byte is nonprintable, or if
228
- # it's a password. But that turns out to be not nearly good
229
- # enough. There are plenty of A/D attributes that are binary
230
- # in the middle. This is probably a nasty performance killer.
231
- def is_attribute_value_binary? value
232
- v = value.to_s
233
- v.each_byte {|byt|
234
- return true if (byt < 32) || (byt > 126)
235
- }
236
- if v[0..0] == ':' or v[0..0] == '<'
237
- return true
238
- end
239
- false
240
- end
241
-
242
- # Returns the symbol that can be used to access the attribute that
243
- # sym_or_str designates.
244
- #
245
- def attribute_name(sym_or_str)
246
- str = sym_or_str.to_s.downcase
247
-
248
- # Does str match 'something='? Still only returns :something
249
- return str[0...-1].to_sym if str.size>1 && str[-1] == ?=
250
- return str.to_sym
251
- end
252
-
253
- # Given a valid attribute symbol, returns true.
254
- #
255
- def valid_attribute?(attr_name)
256
- attribute_names.include?(attr_name)
257
- end
258
-
259
- def setter?(sym)
260
- sym.to_s[-1] == ?=
261
- end
262
- end # class Entry
172
+ # Given a valid attribute symbol, returns true.
173
+ def valid_attribute?(attr_name)
174
+ attribute_names.include?(attr_name)
175
+ end
176
+ private :valid_attribute?
263
177
 
178
+ # Returns true if the symbol ends with an equal sign.
179
+ def setter?(sym)
180
+ sym.to_s[-1] == ?=
181
+ end
182
+ private :setter?
183
+ end # class Entry
264
184
 
265
- end # class LDAP
266
- end # module Net
185
+ require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset
@@ -1,164 +1,447 @@
1
- # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
2
- #
3
- # Gmail: garbagecat10
4
- #
5
- # This program is free software; you can redistribute it and/or modify
6
- # it under the terms of the GNU General Public License as published by
7
- # the Free Software Foundation; either version 2 of the License, or
8
- # (at your option) any later version.
9
- #
10
- # This program is distributed in the hope that it will be useful,
11
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- # GNU General Public License for more details.
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.
14
6
  #
15
- # You should have received a copy of the GNU General Public License
16
- # along with this program; if not, write to the Free Software
17
- # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
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
+ #++
18
15
  #
19
- #---------------------------------------------------------------------------
16
+ # Here's how to code the familiar "objectclass is present" filter:
17
+ # f = Net::LDAP::Filter.present("objectclass")
20
18
  #
19
+ # The object returned by this code can be passed directly to the
20
+ # <tt>:filter</tt> parameter of Net::LDAP#search.
21
21
  #
22
+ # See the individual class and instance methods below for more examples.
23
+ class Net::LDAP::Filter
24
+ ##
25
+ # Known filter types.
26
+ FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
27
+
28
+ def initialize(op, left, right) #:nodoc:
29
+ unless FilterTypes.include?(op)
30
+ raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
31
+ end
32
+ @op = op
33
+ @left = left
34
+ @right = right
35
+ end
22
36
 
23
- require 'strscan'
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
24
67
 
25
- module Net
26
- class LDAP
68
+ ##
69
+ # Creates a Filter object indicating extensible comparison. This Filter
70
+ # object is currently considered EXPERIMENTAL.
71
+ #
72
+ # sample_attributes = ['cn:fr', 'cn:fr.eq',
73
+ # 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
74
+ # attr = sample_attributes.first # Pick an extensible attribute
75
+ # value = 'roberts'
76
+ #
77
+ # filter = "#{attr}:=#{value}" # Basic String Filter
78
+ # filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
79
+ #
80
+ # # Perform a search with the Extensible Match Filter
81
+ # Net::LDAP.search(:filter => filter)
82
+ #--
83
+ # The LDIF required to support the above examples on the OpenDS LDAP
84
+ # server:
85
+ #
86
+ # version: 1
87
+ #
88
+ # dn: dc=example,dc=com
89
+ # objectClass: domain
90
+ # objectClass: top
91
+ # dc: example
92
+ #
93
+ # dn: ou=People,dc=example,dc=com
94
+ # objectClass: organizationalUnit
95
+ # objectClass: top
96
+ # ou: People
97
+ #
98
+ # dn: uid=1,ou=People,dc=example,dc=com
99
+ # objectClass: person
100
+ # objectClass: organizationalPerson
101
+ # objectClass: inetOrgPerson
102
+ # objectClass: top
103
+ # cn:: csO0YsOpcnRz
104
+ # sn:: YsO0YiByw7Riw6lydHM=
105
+ # givenName:: YsO0Yg==
106
+ # uid: 1
107
+ #
108
+ # =Refs:
109
+ # * http://www.ietf.org/rfc/rfc2251.txt
110
+ # * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
111
+ # * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
112
+ #++
113
+ def ex(attribute, value)
114
+ new(:ex, attribute, value)
115
+ end
27
116
 
117
+ ##
118
+ # Creates a Filter object indicating that a particular attribute value
119
+ # is either not present or does not match a particular string; see
120
+ # Filter::eq for more information.
121
+ #
122
+ # This filter does not perform any escaping
123
+ def ne(attribute, value)
124
+ new(:ne, attribute, value)
125
+ end
28
126
 
29
- # Class Net::LDAP::Filter is used to constrain
30
- # LDAP searches. An object of this class is
31
- # passed to Net::LDAP#search in the parameter :filter.
32
- #
33
- # Net::LDAP::Filter supports the complete set of search filters
34
- # available in LDAP, including conjunction, disjunction and negation
35
- # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
36
- # standard notation for specifying LDAP search filters.
37
- #
38
- # Here's how to code the familiar "objectclass is present" filter:
39
- # f = Net::LDAP::Filter.pres( "objectclass" )
40
- # The object returned by this code can be passed directly to
41
- # the <tt>:filter</tt> parameter of Net::LDAP#search.
42
- #
43
- # See the individual class and instance methods below for more examples.
44
- #
45
- class Filter
127
+ ##
128
+ # Creates a Filter object indicating that the value of a particular
129
+ # attribute must match a particular string. The attribute value is
130
+ # escaped, so the "*" character is interpreted literally.
131
+ def equals(attribute, value)
132
+ new(:eq, attribute, escape(value))
133
+ end
46
134
 
47
- def initialize op, a, b
48
- @op = op
49
- @left = a
50
- @right = b
51
- end
135
+ ##
136
+ # Creates a Filter object indicating that the value of a particular
137
+ # attribute must begin with a particular string. The attribute value is
138
+ # escaped, so the "*" character is interpreted literally.
139
+ def begins(attribute, value)
140
+ new(:eq, attribute, escape(value) + "*")
141
+ end
52
142
 
53
- # #eq creates a filter object indicating that the value of
54
- # a paticular attribute must be either <i>present</i> or must
55
- # match a particular string.
56
- #
57
- # To specify that an attribute is "present" means that only
58
- # directory entries which contain a value for the particular
59
- # attribute will be selected by the filter. This is useful
60
- # in case of optional attributes such as <tt>mail.</tt>
61
- # Presence is indicated by giving the value "*" in the second
62
- # parameter to #eq. This example selects only entries that have
63
- # one or more values for <tt>sAMAccountName:</tt>
64
- # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
65
- #
66
- # To match a particular range of values, pass a string as the
67
- # second parameter to #eq. The string may contain one or more
68
- # "*" characters as wildcards: these match zero or more occurrences
69
- # of any character. Full regular-expressions are <i>not</i> supported
70
- # due to limitations in the underlying LDAP protocol.
71
- # This example selects any entry with a <tt>mail</tt> value containing
72
- # the substring "anderson":
73
- # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
74
- #--
75
- # Removed gt and lt. They ain't in the standard!
76
- #
77
- def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
78
- def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
79
- #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
80
- #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
81
- def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
82
- def Filter::le attribute, value; Filter.new :le, attribute, value; end
83
-
84
- # #pres( attribute ) is a synonym for #eq( attribute, "*" )
85
- #
86
- def Filter::pres attribute; Filter.eq attribute, "*"; end
143
+ ##
144
+ # Creates a Filter object indicating that the value of a particular
145
+ # attribute must end with a particular string. The attribute value is
146
+ # escaped, so the "*" character is interpreted literally.
147
+ def ends(attribute, value)
148
+ new(:eq, attribute, "*" + escape(value))
149
+ end
87
150
 
88
- # operator & ("AND") is used to conjoin two or more filters.
89
- # This expression will select only entries that have an <tt>objectclass</tt>
90
- # attribute AND have a <tt>mail</tt> attribute that begins with "George":
91
- # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
92
- #
93
- def & filter; Filter.new :and, self, filter; end
151
+ ##
152
+ # Creates a Filter object indicating that the value of a particular
153
+ # attribute must contain a particular string. The attribute value is
154
+ # escaped, so the "*" character is interpreted literally.
155
+ def contains(attribute, value)
156
+ new(:eq, attribute, "*" + escape(value) + "*")
157
+ end
94
158
 
95
- # operator | ("OR") is used to disjoin two or more filters.
96
- # This expression will select entries that have either an <tt>objectclass</tt>
97
- # attribute OR a <tt>mail</tt> attribute that begins with "George":
98
- # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
99
- #
100
- def | filter; Filter.new :or, self, filter; end
159
+ ##
160
+ # Creates a Filter object indicating that a particular attribute value
161
+ # is greater than or equal to the specified value.
162
+ def ge(attribute, value)
163
+ new(:ge, attribute, value)
164
+ end
101
165
 
166
+ ##
167
+ # Creates a Filter object indicating that a particular attribute value
168
+ # is less than or equal to the specified value.
169
+ def le(attribute, value)
170
+ new(:le, attribute, value)
171
+ end
172
+
173
+ ##
174
+ # Joins two or more filters so that all conditions must be true. Calling
175
+ # <tt>Filter.join(left, right)</tt> is the same as <tt>left &
176
+ # right</tt>.
177
+ #
178
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
179
+ # x = Net::LDAP::Filter.present("objectclass")
180
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
181
+ # # with "George".
182
+ # y = Net::LDAP::Filter.eq("mail", "George*")
183
+ # # Selects only entries that meet both conditions above.
184
+ # z = Net::LDAP::Filter.join(x, y)
185
+ def join(left, right)
186
+ new(:and, left, right)
187
+ end
188
+
189
+ ##
190
+ # Creates a disjoint comparison between two or more filters. Selects
191
+ # entries where either the left or right side are true. Calling
192
+ # <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
193
+ # right</tt>.
194
+ #
195
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
196
+ # x = Net::LDAP::Filter.present("objectclass")
197
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
198
+ # # with "George".
199
+ # y = Net::LDAP::Filter.eq("mail", "George*")
200
+ # # Selects only entries that meet either condition above.
201
+ # z = x | y
202
+ def intersect(left, right)
203
+ new(:or, left, right)
204
+ end
102
205
 
206
+ ##
207
+ # Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
208
+ # as <tt>~filter</tt>.
209
+ #
210
+ # # Selects only entries that do not have an <tt>objectclass</tt>
211
+ # # attribute.
212
+ # x = ~Net::LDAP::Filter.present("objectclass")
213
+ def negate(filter)
214
+ new(:not, filter, nil)
215
+ end
216
+
217
+ ##
218
+ # This is a synonym for #eq(attribute, "*"). Also known as #present and
219
+ # #pres.
220
+ def present?(attribute)
221
+ eq(attribute, "*")
222
+ end
223
+ alias_method :present, :present?
224
+ alias_method :pres, :present?
225
+
226
+ # http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
227
+ # charset for filters. All of the following must be escaped in any normal
228
+ # string using a single backslash ('\') as escape.
229
+ #
230
+ ESCAPES = {
231
+ "\0" => '00', # NUL = %x00 ; null character
232
+ '*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
233
+ '(' => '28', # LPARENS = %x28 ; left parenthesis ("(")
234
+ ')' => '29', # RPARENS = %x29 ; right parenthesis (")")
235
+ '\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\")
236
+ }
237
+ # Compiled character class regexp using the keys from the above hash.
238
+ ESCAPE_RE = Regexp.new(
239
+ "[" +
240
+ ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
241
+ "]")
242
+
243
+ ##
244
+ # Escape a string for use in an LDAP filter
245
+ def escape(string)
246
+ string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
247
+ end
248
+
249
+ ##
250
+ # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
251
+ # object. The incoming BER object most likely came to us by parsing an
252
+ # LDAP searchRequest PDU. See also the comments under #to_ber, including
253
+ # the grammar snippet from the RFC.
254
+ #--
255
+ # We're hardcoding the BER constants from the RFC. These should be
256
+ # broken out insto constants.
257
+ def parse_ber(ber)
258
+ case ber.ber_identifier
259
+ when 0xa0 # context-specific constructed 0, "and"
260
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
261
+ when 0xa1 # context-specific constructed 1, "or"
262
+ ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
263
+ when 0xa2 # context-specific constructed 2, "not"
264
+ ~parse_ber(ber.first)
265
+ when 0xa3 # context-specific constructed 3, "equalityMatch"
266
+ if ber.last == "*"
267
+ else
268
+ eq(ber.first, ber.last)
269
+ end
270
+ when 0xa4 # context-specific constructed 4, "substring"
271
+ str = ""
272
+ final = false
273
+ ber.last.each { |b|
274
+ case b.ber_identifier
275
+ when 0x80 # context-specific primitive 0, SubstringFilter "initial"
276
+ raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
277
+ str += b
278
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
279
+ str += "*#{b}"
280
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
281
+ str += "*#{b}"
282
+ final = true
283
+ end
284
+ }
285
+ str += "*" unless final
286
+ eq(ber.first.to_s, str)
287
+ when 0xa5 # context-specific constructed 5, "greaterOrEqual"
288
+ ge(ber.first.to_s, ber.last.to_s)
289
+ when 0xa6 # context-specific constructed 6, "lessOrEqual"
290
+ le(ber.first.to_s, ber.last.to_s)
291
+ when 0x87 # context-specific primitive 7, "present"
292
+ # call to_s to get rid of the BER-identifiedness of the incoming string.
293
+ present?(ber.to_s)
294
+ when 0xa9 # context-specific constructed 9, "extensible comparison"
295
+ raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
296
+
297
+ # Reassembles the extensible filter parts
298
+ # (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
299
+ type = value = dn = rule = nil
300
+ ber.each do |element|
301
+ case element.ber_identifier
302
+ when 0x81 then rule=element
303
+ when 0x82 then type=element
304
+ when 0x83 then value=element
305
+ when 0x84 then dn='dn'
306
+ end
307
+ end
308
+
309
+ attribute = ''
310
+ attribute << type if type
311
+ attribute << ":#{dn}" if dn
312
+ attribute << ":#{rule}" if rule
313
+
314
+ ex(attribute, value)
315
+ else
316
+ raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
317
+ end
318
+ end
319
+
320
+ ##
321
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
322
+ # to a Net::LDAP::Filter.
323
+ def construct(ldap_filter_string)
324
+ FilterParser.parse(ldap_filter_string)
325
+ end
326
+ alias_method :from_rfc2254, :construct
327
+ alias_method :from_rfc4515, :construct
328
+
329
+ ##
330
+ # Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
331
+ # object.
332
+ #--
333
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
334
+ # filter types. Could pull them out into a constant.
335
+ #++
336
+ def parse_ldap_filter(obj)
337
+ case obj.ber_identifier
338
+ when 0x87 # present. context-specific primitive 7.
339
+ eq(obj.to_s, "*")
340
+ when 0xa3 # equalityMatch. context-specific constructed 3.
341
+ eq(obj[0], obj[1])
342
+ else
343
+ raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
344
+ end
345
+ end
346
+ end
347
+
348
+ ##
349
+ # Joins two or more filters so that all conditions must be true.
103
350
  #
104
- # operator ~ ("NOT") is used to negate a filter.
105
- # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
106
- # attribute:
107
- # f = ~ Net::LDAP::Filter.pres( "objectclass" )
351
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
352
+ # x = Net::LDAP::Filter.present("objectclass")
353
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
354
+ # # with "George".
355
+ # y = Net::LDAP::Filter.eq("mail", "George*")
356
+ # # Selects only entries that meet both conditions above.
357
+ # z = x & y
358
+ def &(filter)
359
+ self.class.join(self, filter)
360
+ end
361
+
362
+ ##
363
+ # Creates a disjoint comparison between two or more filters. Selects
364
+ # entries where either the left or right side are true.
108
365
  #
109
- #--
110
- # This operator can't be !, evidently. Try it.
111
- # Removed GT and LT. They're not in the RFC.
112
- def ~@; Filter.new :not, self, nil; end
366
+ # # Selects only entries that have an <tt>objectclass</tt> attribute.
367
+ # x = Net::LDAP::Filter.present("objectclass")
368
+ # # Selects only entries that have a <tt>mail</tt> attribute that begins
369
+ # # with "George".
370
+ # y = Net::LDAP::Filter.eq("mail", "George*")
371
+ # # Selects only entries that meet either condition above.
372
+ # z = x | y
373
+ def |(filter)
374
+ self.class.intersect(self, filter)
375
+ end
376
+
377
+ ##
378
+ # Negates a filter.
379
+ #
380
+ # # Selects only entries that do not have an <tt>objectclass</tt>
381
+ # # attribute.
382
+ # x = ~Net::LDAP::Filter.present("objectclass")
383
+ def ~@
384
+ self.class.negate(self)
385
+ end
113
386
 
114
- # Equality operator for filters, useful primarily for constructing unit tests.
115
- def == filter
116
- str = "[@op,@left,@right]"
117
- self.instance_eval(str) == filter.instance_eval(str)
118
- end
387
+ ##
388
+ # Equality operator for filters, useful primarily for constructing unit tests.
389
+ def ==(filter)
390
+ # 20100320 AZ: We need to come up with a better way of doing this. This
391
+ # is just nasty.
392
+ str = "[@op,@left,@right]"
393
+ self.instance_eval(str) == filter.instance_eval(str)
394
+ end
119
395
 
120
- def to_s
396
+ def to_raw_rfc2254
121
397
  case @op
122
398
  when :ne
123
- "(!(#{@left}=#{@right}))"
399
+ "!(#{@left}=#{@right})"
124
400
  when :eq
125
- "(#{@left}=#{@right})"
126
- #when :gt
127
- # "#{@left}>#{@right}"
128
- #when :lt
129
- # "#{@left}<#{@right}"
401
+ "#{@left}=#{@right}"
402
+ when :ex
403
+ "#{@left}:=#{@right}"
130
404
  when :ge
131
405
  "#{@left}>=#{@right}"
132
406
  when :le
133
407
  "#{@left}<=#{@right}"
134
408
  when :and
135
- "(&(#{@left})(#{@right}))"
409
+ "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
136
410
  when :or
137
- "(|(#{@left})(#{@right}))"
411
+ "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
138
412
  when :not
139
- "(!(#{@left}))"
140
- else
141
- raise "invalid or unsupported operator in LDAP Filter"
413
+ "!(#{@left.to_raw_rfc2254})"
142
414
  end
143
415
  end
144
416
 
417
+ ##
418
+ # Converts the Filter object to an RFC 2254-compatible text format.
419
+ def to_rfc2254
420
+ "(#{to_raw_rfc2254})"
421
+ end
145
422
 
423
+ def to_s
424
+ to_rfc2254
425
+ end
426
+
427
+ ##
428
+ # Converts the filter to BER format.
146
429
  #--
147
- # to_ber
148
430
  # Filter ::=
149
431
  # CHOICE {
150
- # and [0] SET OF Filter,
151
- # or [1] SET OF Filter,
152
- # not [2] Filter,
153
- # equalityMatch [3] AttributeValueAssertion,
154
- # substrings [4] SubstringFilter,
155
- # greaterOrEqual [5] AttributeValueAssertion,
156
- # lessOrEqual [6] AttributeValueAssertion,
157
- # present [7] AttributeType,
158
- # approxMatch [8] AttributeValueAssertion
432
+ # and [0] SET OF Filter,
433
+ # or [1] SET OF Filter,
434
+ # not [2] Filter,
435
+ # equalityMatch [3] AttributeValueAssertion,
436
+ # substrings [4] SubstringFilter,
437
+ # greaterOrEqual [5] AttributeValueAssertion,
438
+ # lessOrEqual [6] AttributeValueAssertion,
439
+ # present [7] AttributeType,
440
+ # approxMatch [8] AttributeValueAssertion,
441
+ # extensibleMatch [9] MatchingRuleAssertion
159
442
  # }
160
443
  #
161
- # SubstringFilter
444
+ # SubstringFilter ::=
162
445
  # SEQUENCE {
163
446
  # type AttributeType,
164
447
  # SEQUENCE OF CHOICE {
@@ -168,325 +451,309 @@ class Filter
168
451
  # }
169
452
  # }
170
453
  #
171
- # Parsing substrings is a little tricky.
172
- # We use the split method to break a string into substrings
173
- # delimited by the * (star) character. But we also need
174
- # to know whether there is a star at the head and tail
175
- # of the string. A Ruby particularity comes into play here:
176
- # if you split on * and the first character of the string is
177
- # a star, then split will return an array whose first element
178
- # is an _empty_ string. But if the _last_ character of the
179
- # string is star, then split will return an array that does
180
- # _not_ add an empty string at the end. So we have to deal
181
- # with all that specifically.
454
+ # MatchingRuleAssertion ::=
455
+ # SEQUENCE {
456
+ # matchingRule [1] MatchingRuleId OPTIONAL,
457
+ # type [2] AttributeDescription OPTIONAL,
458
+ # matchValue [3] AssertionValue,
459
+ # dnAttributes [4] BOOLEAN DEFAULT FALSE
460
+ # }
461
+ #
462
+ # Matching Rule Suffixes
463
+ # Less than [.1] or .[lt]
464
+ # Less than or equal to [.2] or [.lte]
465
+ # Equality [.3] or [.eq] (default)
466
+ # Greater than or equal to [.4] or [.gte]
467
+ # Greater than [.5] or [.gt]
468
+ # Substring [.6] or [.sub]
182
469
  #
470
+ #++
183
471
  def to_ber
184
472
  case @op
185
473
  when :eq
186
- if @right == "*" # present
187
- @left.to_s.to_ber_contextspecific 7
188
- elsif @right =~ /[\*]/ #substring
189
- ary = @right.split( /[\*]+/ )
190
- final_star = @right =~ /[\*]$/
191
- initial_star = ary.first == "" and ary.shift
192
-
193
- seq = []
194
- unless initial_star
195
- seq << ary.shift.to_ber_contextspecific(0)
474
+ if @right == "*" # presence test
475
+ @left.to_s.to_ber_contextspecific(7)
476
+ elsif @right =~ /[*]/ # substring
477
+ # Parsing substrings is a little tricky. We use String#split to
478
+ # break a string into substrings delimited by the * (star)
479
+ # character. But we also need to know whether there is a star at the
480
+ # head and tail of the string, so we use a limit parameter value of
481
+ # -1: "If negative, there is no limit to the number of fields
482
+ # returned, and trailing null fields are not suppressed."
483
+ #
484
+ # 20100320 AZ: This is much simpler than the previous verison. Also,
485
+ # unnecessary regex escaping has been removed.
486
+
487
+ ary = @right.split(/[*]+/, -1)
488
+
489
+ if ary.first.empty?
490
+ first = nil
491
+ ary.shift
492
+ else
493
+ first = ary.shift.to_ber_contextspecific(0)
196
494
  end
197
- n_any_strings = ary.length - (final_star ? 0 : 1)
198
- #p n_any_strings
199
- n_any_strings.times {
200
- seq << ary.shift.to_ber_contextspecific(1)
201
- }
202
- unless final_star
203
- seq << ary.shift.to_ber_contextspecific(2)
495
+
496
+ if ary.last.empty?
497
+ last = nil
498
+ ary.pop
499
+ else
500
+ last = ary.pop.to_ber_contextspecific(2)
204
501
  end
205
- [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
206
- else #equality
207
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3
502
+
503
+ seq = ary.map { |e| e.to_ber_contextspecific(1) }
504
+ seq.unshift first if first
505
+ seq.push last if last
506
+
507
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
508
+ else # equality
509
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
510
+ end
511
+ when :ex
512
+ seq = []
513
+
514
+ unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
515
+ raise Net::LDAP::LdapError, "Bad attribute #{@left}"
208
516
  end
517
+ type, dn, rule = $1, $2, $4
518
+
519
+ seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
520
+ seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
521
+ seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
522
+ seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
523
+
524
+ seq.to_ber_contextspecific(9)
209
525
  when :ge
210
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5
526
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
211
527
  when :le
212
- [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6
528
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
529
+ when :ne
530
+ [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
213
531
  when :and
214
532
  ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
215
- ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
533
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
216
534
  when :or
217
535
  ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
218
- ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
536
+ ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
219
537
  when :not
220
- [@left.to_ber].to_ber_contextspecific 2
221
- else
222
- # ERROR, we'll return objectclass=* to keep things from blowing up,
223
- # but that ain't a good answer and we need to kick out an error of some kind.
224
- raise "unimplemented search filter"
538
+ [@left.to_ber].to_ber_contextspecific(2)
225
539
  end
226
540
  end
227
541
 
228
- def unescape(right)
229
- right.gsub(/\\([a-fA-F\d]{2,2})/) do
230
- [$1.hex].pack("U")
231
- end
542
+ ##
543
+ # Perform filter operations against a user-supplied block. This is useful
544
+ # when implementing an LDAP directory server. The caller's block will be
545
+ # called with two arguments: first, a symbol denoting the "operation" of
546
+ # the filter; and second, an array consisting of arguments to the
547
+ # operation. The user-supplied block (which is MANDATORY) should perform
548
+ # some desired application-defined processing, and may return a
549
+ # locally-meaningful object that will appear as a parameter in the :and,
550
+ # :or and :not operations detailed below.
551
+ #
552
+ # A typical object to return from the user-supplied block is an array of
553
+ # Net::LDAP::Filter objects.
554
+ #
555
+ # These are the possible values that may be passed to the user-supplied
556
+ # block:
557
+ # * :equalityMatch (the arguments will be an attribute name and a value
558
+ # to be matched);
559
+ # * :substrings (two arguments: an attribute name and a value containing
560
+ # one or more "*" characters);
561
+ # * :present (one argument: an attribute name);
562
+ # * :greaterOrEqual (two arguments: an attribute name and a value to be
563
+ # compared against);
564
+ # * :lessOrEqual (two arguments: an attribute name and a value to be
565
+ # compared against);
566
+ # * :and (two or more arguments, each of which is an object returned
567
+ # from a recursive call to #execute, with the same block;
568
+ # * :or (two or more arguments, each of which is an object returned from
569
+ # a recursive call to #execute, with the same block; and
570
+ # * :not (one argument, which is an object returned from a recursive
571
+ # call to #execute with the the same block.
572
+ def execute(&block)
573
+ case @op
574
+ when :eq
575
+ if @right == "*"
576
+ yield :present, @left
577
+ elsif @right.index '*'
578
+ yield :substrings, @left, @right
579
+ else
580
+ yield :equalityMatch, @left, @right
581
+ end
582
+ when :ge
583
+ yield :greaterOrEqual, @left, @right
584
+ when :le
585
+ yield :lessOrEqual, @left, @right
586
+ when :or, :and
587
+ yield @op, (@left.execute(&block)), (@right.execute(&block))
588
+ when :not
589
+ yield @op, (@left.execute(&block))
590
+ end || []
232
591
  end
233
592
 
234
-
235
- # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
236
- # object. The incoming BER object most likely came to us by parsing an
237
- # LDAP searchRequest PDU.
238
- # Cf the comments under #to_ber, including the grammar snippet from the RFC.
239
- #--
240
- # We're hardcoding the BER constants from the RFC. Ought to break them out
241
- # into constants.
242
- #
243
- def Filter::parse_ber ber
244
- case ber.ber_identifier
245
- when 0xa0 # context-specific constructed 0, "and"
246
- ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
247
- when 0xa1 # context-specific constructed 1, "or"
248
- ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
249
- when 0xa2 # context-specific constructed 2, "not"
250
- ~ Filter::parse_ber( ber.first )
251
- when 0xa3 # context-specific constructed 3, "equalityMatch"
252
- if ber.last == "*"
253
- else
254
- Filter.eq( ber.first, ber.last )
255
- end
256
- when 0xa4 # context-specific constructed 4, "substring"
257
- str = ""
258
- final = false
259
- ber.last.each {|b|
260
- case b.ber_identifier
261
- when 0x80 # context-specific primitive 0, SubstringFilter "initial"
262
- raise "unrecognized substring filter, bad initial" if str.length > 0
263
- str += b
264
- when 0x81 # context-specific primitive 0, SubstringFilter "any"
265
- str += "*#{b}"
266
- when 0x82 # context-specific primitive 0, SubstringFilter "final"
267
- str += "*#{b}"
268
- final = true
269
- end
270
- }
271
- str += "*" unless final
272
- Filter.eq( ber.first.to_s, str )
273
- when 0xa5 # context-specific constructed 5, "greaterOrEqual"
274
- Filter.ge( ber.first.to_s, ber.last.to_s )
275
- when 0xa6 # context-specific constructed 5, "lessOrEqual"
276
- Filter.le( ber.first.to_s, ber.last.to_s )
277
- when 0x87 # context-specific primitive 7, "present"
278
- # call to_s to get rid of the BER-identifiedness of the incoming string.
279
- Filter.pres( ber.to_s )
280
- else
281
- raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
282
- end
283
- end
284
-
285
-
286
- # Perform filter operations against a user-supplied block. This is useful when implementing
287
- # an LDAP directory server. The caller's block will be called with two arguments: first, a
288
- # symbol denoting the "operation" of the filter; and second, an array consisting of arguments
289
- # to the operation. The user-supplied block (which is MANDATORY) should perform some desired
290
- # application-defined processing, and may return a locally-meaningful object that will appear
291
- # as a parameter in the :and, :or and :not operations detailed below.
292
- #
293
- # A typical object to return from the user-supplied block is an array of
294
- # Net::LDAP::Filter objects.
295
- #
296
- # These are the possible values that may be passed to the user-supplied block:
297
- # :equalityMatch (the arguments will be an attribute name and a value to be matched);
298
- # :substrings (two arguments: an attribute name and a value containing one or more * characters);
299
- # :present (one argument: an attribute name);
300
- # :greaterOrEqual (two arguments: an attribute name and a value to be compared against);
301
- # :lessOrEqual (two arguments: an attribute name and a value to be compared against);
302
- # :and (two or more arguments, each of which is an object returned from a recursive call
303
- # to #execute, with the same block;
304
- # :or (two or more arguments, each of which is an object returned from a recursive call
305
- # to #execute, with the same block;
306
- # :not (one argument, which is an object returned from a recursive call to #execute with the
307
- # the same block.
308
- #
309
- def execute &block
310
- case @op
311
- when :eq
312
- if @right == "*"
313
- yield :present, @left
314
- elsif @right.index '*'
315
- yield :substrings, @left, @right
316
- else
317
- yield :equalityMatch, @left, @right
318
- end
319
- when :ge
320
- yield :greaterOrEqual, @left, @right
321
- when :le
322
- yield :lessOrEqual, @left, @right
323
- when :or, :and
324
- yield @op, (@left.execute(&block)), (@right.execute(&block))
325
- when :not
326
- yield @op, (@left.execute(&block))
327
- end || []
328
- end
329
-
330
-
331
- #--
332
- # coalesce
593
+ ##
333
594
  # This is a private helper method for dealing with chains of ANDs and ORs
334
595
  # that are longer than two. If BOTH of our branches are of the specified
335
596
  # type of joining operator, then return both of them as an array (calling
336
597
  # coalesce recursively). If they're not, then return an array consisting
337
598
  # only of self.
338
- #
339
- def coalesce operator
599
+ def coalesce(operator) #:nodoc:
340
600
  if @op == operator
341
- [@left.coalesce( operator ), @right.coalesce( operator )]
601
+ [@left.coalesce(operator), @right.coalesce(operator)]
342
602
  else
343
603
  [self]
344
604
  end
345
605
  end
346
606
 
347
-
348
-
349
- #--
350
- # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
351
- # object. Convert it to a Net::LDAP::Filter.
352
- # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
353
- # filter types. Could pull them out into a constant.
354
- #
355
- def Filter::parse_ldap_filter obj
356
- case obj.ber_identifier
357
- when 0x87 # present. context-specific primitive 7.
358
- Filter.eq( obj.to_s, "*" )
359
- when 0xa3 # equalityMatch. context-specific constructed 3.
360
- Filter.eq( obj[0], obj[1] )
361
- else
362
- raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
363
- end
364
- end
365
-
366
-
367
-
368
-
607
+ ##
369
608
  #--
370
609
  # We got a hash of attribute values.
371
610
  # Do we match the attributes?
372
611
  # Return T/F, and call match recursively as necessary.
373
- def match entry
612
+ #++
613
+ def match(entry)
374
614
  case @op
375
615
  when :eq
376
616
  if @right == "*"
377
617
  l = entry[@left] and l.length > 0
378
618
  else
379
- l = entry[@left] and l = l.to_a and l.index(@right)
619
+ l = entry[@left] and l = Array(l) and l.index(@right)
380
620
  end
381
621
  else
382
- raise LdapError.new( "unknown filter type in match: #{@op}" )
622
+ raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
383
623
  end
384
624
  end
385
625
 
386
- # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
387
- # to a Net::LDAP::Filter.
388
- def self.construct ldap_filter_string
389
- FilterParser.new(ldap_filter_string).filter
390
- end
391
-
392
- # Synonym for #construct.
393
- # to a Net::LDAP::Filter.
394
- def self.from_rfc2254 ldap_filter_string
395
- construct ldap_filter_string
396
- end
397
-
398
- end # class Net::LDAP::Filter
399
-
400
-
401
-
402
- class FilterParser #:nodoc:
403
-
404
- attr_reader :filter
405
-
406
- def initialize str
407
- @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
626
+ ##
627
+ # Converts escaped characters (e.g., "\\28") to unescaped characters
628
+ # ("(").
629
+ def unescape(right)
630
+ right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
408
631
  end
632
+ private :unescape
633
+
634
+ ##
635
+ # Parses RFC 2254-style string representations of LDAP filters into Filter
636
+ # object hierarchies.
637
+ class FilterParser #:nodoc:
638
+ ##
639
+ # The constructed filter.
640
+ attr_reader :filter
641
+
642
+ class << self
643
+ private :new
644
+
645
+ ##
646
+ # Construct a filter tree from the provided string and return it.
647
+ def parse(ldap_filter_string)
648
+ new(ldap_filter_string).filter
649
+ end
650
+ end
409
651
 
410
- def parse scanner
411
- parse_filter_branch(scanner) or parse_paren_expression(scanner)
412
- end
652
+ def initialize(str)
653
+ require 'strscan' # Don't load strscan until we need it.
654
+ @filter = parse(StringScanner.new(str))
655
+ raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
656
+ end
413
657
 
414
- def parse_paren_expression scanner
415
- if scanner.scan(/\s*\(\s*/)
416
- b = if scanner.scan(/\s*\&\s*/)
417
- a = nil
418
- branches = []
419
- while br = parse_paren_expression(scanner)
420
- branches << br
421
- end
422
- if branches.length >= 2
423
- a = branches.shift
424
- while branches.length > 0
425
- a = a & branches.shift
426
- end
427
- a
428
- end
429
- elsif scanner.scan(/\s*\|\s*/)
430
- # TODO: DRY!
431
- a = nil
432
- branches = []
433
- while br = parse_paren_expression(scanner)
434
- branches << br
435
- end
436
- if branches.length >= 2
437
- a = branches.shift
438
- while branches.length > 0
439
- a = a | branches.shift
440
- end
441
- a
442
- end
443
- elsif scanner.scan(/\s*\!\s*/)
444
- br = parse_paren_expression(scanner)
445
- if br
446
- ~ br
658
+ ##
659
+ # Parse the string contained in the StringScanner provided. Parsing
660
+ # tries to parse a standalone expression first. If that fails, it tries
661
+ # to parse a parenthesized expression.
662
+ def parse(scanner)
663
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
664
+ end
665
+ private :parse
666
+
667
+ ##
668
+ # Join ("&") and intersect ("|") operations are presented in branches.
669
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
670
+ # test1 and test2. Each of these is parsed separately and then pushed
671
+ # into a branch array for filter merging using the parent operation.
672
+ #
673
+ # This method parses the branch text out into an array of filter
674
+ # objects.
675
+ def parse_branches(scanner)
676
+ branches = []
677
+ while branch = parse_paren_expression(scanner)
678
+ branches << branch
679
+ end
680
+ branches
681
+ end
682
+ private :parse_branches
683
+
684
+ ##
685
+ # Join ("&") and intersect ("|") operations are presented in branches.
686
+ # That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
687
+ # test1 and test2. Each of these is parsed separately and then pushed
688
+ # into a branch array for filter merging using the parent operation.
689
+ #
690
+ # This method calls #parse_branches to generate the branch list and then
691
+ # merges them into a single Filter tree by calling the provided
692
+ # operation.
693
+ def merge_branches(op, scanner)
694
+ filter = nil
695
+ branches = parse_branches(scanner)
696
+
697
+ if branches.size >= 1
698
+ filter = branches.shift
699
+ while not branches.empty?
700
+ filter = filter.__send__(op, branches.shift)
447
701
  end
448
- else
449
- parse_filter_branch( scanner )
450
702
  end
451
703
 
452
- if b and scanner.scan( /\s*\)\s*/ )
453
- b
704
+ filter
705
+ end
706
+ private :merge_branches
707
+
708
+ def parse_paren_expression(scanner)
709
+ if scanner.scan(/\s*\(\s*/)
710
+ expr = if scanner.scan(/\s*\&\s*/)
711
+ merge_branches(:&, scanner)
712
+ elsif scanner.scan(/\s*\|\s*/)
713
+ merge_branches(:|, scanner)
714
+ elsif scanner.scan(/\s*\!\s*/)
715
+ br = parse_paren_expression(scanner)
716
+ ~br if br
717
+ else
718
+ parse_filter_branch(scanner)
719
+ end
720
+
721
+ if expr and scanner.scan(/\s*\)\s*/)
722
+ expr
723
+ end
454
724
  end
455
725
  end
456
- end
726
+ private :parse_paren_expression
457
727
 
458
- # Added a greatly-augmented filter contributed by Andre Nathan
459
- # for detecting special characters in values. (15Aug06)
460
- # Added blanks to the attribute filter (26Oct06)
461
- def parse_filter_branch scanner
462
- scanner.scan(/\s*/)
463
- if token = scanner.scan( /[\w\-_]+/ )
728
+ ##
729
+ # This parses a given expression inside of parentheses.
730
+ def parse_filter_branch(scanner)
464
731
  scanner.scan(/\s*/)
465
- if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
732
+ if token = scanner.scan(/[-\w:.]*[\w]/)
466
733
  scanner.scan(/\s*/)
467
- #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
468
- #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou
469
- if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ )
470
- case op
471
- when "="
472
- Filter.eq( token, value )
473
- when "!="
474
- Filter.ne( token, value )
475
- when "<"
476
- Filter.lt( token, value )
477
- when "<="
478
- Filter.le( token, value )
479
- when ">"
480
- Filter.gt( token, value )
481
- when ">="
482
- Filter.ge( token, value )
734
+ if op = scanner.scan(/<=|>=|!=|:=|=/)
735
+ scanner.scan(/\s*/)
736
+ if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s]|\\[a-fA-F\d]{2})+/)
737
+ # 20100313 AZ: Assumes that "(uid=george*)" is the same as
738
+ # "(uid=george* )". The standard doesn't specify, but I can find
739
+ # no examples that suggest otherwise.
740
+ value.strip!
741
+ case op
742
+ when "="
743
+ Net::LDAP::Filter.eq(token, value)
744
+ when "!="
745
+ Net::LDAP::Filter.ne(token, value)
746
+ when "<="
747
+ Net::LDAP::Filter.le(token, value)
748
+ when ">="
749
+ Net::LDAP::Filter.ge(token, value)
750
+ when ":="
751
+ Net::LDAP::Filter.ex(token, value)
752
+ end
483
753
  end
484
754
  end
485
755
  end
486
756
  end
487
- end
488
-
489
- end # class Net::LDAP::FilterParser
490
-
491
- end # class Net::LDAP
492
- end # module Net
757
+ private :parse_filter_branch
758
+ end # class Net::LDAP::FilterParser
759
+ end # class Net::LDAP::Filter