socialcast-net-ldap 0.1.5

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