net-ldap 0.1.1 → 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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