net-ldap 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of net-ldap might be problematic. Click here for more details.
- data/COPYING +272 -0
- data/History.txt +96 -0
- data/LICENSE +55 -0
- data/Manifest.txt +30 -0
- data/README.txt +62 -0
- data/Rakefile +18 -0
- data/Release-Announcement +95 -0
- data/lib/net/ber.rb +557 -0
- data/lib/net/ldap.rb +1613 -0
- data/lib/net/ldap/dataset.rb +108 -0
- data/lib/net/ldap/entry.rb +269 -0
- data/lib/net/ldap/filter.rb +499 -0
- data/lib/net/ldap/pdu.rb +258 -0
- data/lib/net/ldap/psw.rb +64 -0
- data/lib/net/ldif.rb +39 -0
- data/lib/net/snmp.rb +297 -0
- data/pre-setup.rb +45 -0
- data/setup.rb +1366 -0
- data/test/common.rb +7 -0
- data/test/test_ber.rb +100 -0
- data/test/test_entry.rb +7 -0
- data/test/test_filter.rb +83 -0
- data/test/test_ldif.rb +59 -0
- data/test/test_password.rb +17 -0
- data/test/test_snmp.rb +130 -0
- data/test/testdata.ldif +101 -0
- data/tests/NOTICE.txt +6 -0
- data/tests/testldap.rb +190 -0
- data/testserver/ldapserver.rb +229 -0
- data/testserver/testdata.ldif +101 -0
- metadata +105 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# $Id$
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#----------------------------------------------------------------------------
|
5
|
+
#
|
6
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# Gmail: garbagecat10
|
9
|
+
#
|
10
|
+
# This program is free software; you can redistribute it and/or modify
|
11
|
+
# it under the terms of the GNU General Public License as published by
|
12
|
+
# the Free Software Foundation; either version 2 of the License, or
|
13
|
+
# (at your option) any later version.
|
14
|
+
#
|
15
|
+
# This program is distributed in the hope that it will be useful,
|
16
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
+
# GNU General Public License for more details.
|
19
|
+
#
|
20
|
+
# You should have received a copy of the GNU General Public License
|
21
|
+
# along with this program; if not, write to the Free Software
|
22
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
23
|
+
#
|
24
|
+
#---------------------------------------------------------------------------
|
25
|
+
#
|
26
|
+
#
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
module Net
|
32
|
+
class LDAP
|
33
|
+
|
34
|
+
class Dataset < Hash
|
35
|
+
|
36
|
+
attr_reader :comments
|
37
|
+
|
38
|
+
|
39
|
+
def Dataset::read_ldif io
|
40
|
+
ds = Dataset.new
|
41
|
+
|
42
|
+
line = io.gets && chomp
|
43
|
+
dn = nil
|
44
|
+
|
45
|
+
while line
|
46
|
+
io.gets and chomp
|
47
|
+
if $_ =~ /^[\s]+/
|
48
|
+
line << " " << $'
|
49
|
+
else
|
50
|
+
nextline = $_
|
51
|
+
|
52
|
+
if line =~ /^\#/
|
53
|
+
ds.comments << line
|
54
|
+
elsif line =~ /^dn:[\s]*/i
|
55
|
+
dn = $'
|
56
|
+
ds[dn] = Hash.new {|k,v| k[v] = []}
|
57
|
+
elsif line.length == 0
|
58
|
+
dn = nil
|
59
|
+
elsif line =~ /^([^:]+):([\:]?)[\s]*/
|
60
|
+
# $1 is the attribute name
|
61
|
+
# $2 is a colon iff the attr-value is base-64 encoded
|
62
|
+
# $' is the attr-value
|
63
|
+
# Avoid the Base64 class because not all Ruby versions have it.
|
64
|
+
attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
|
65
|
+
ds[dn][$1.downcase.intern] << attrvalue
|
66
|
+
end
|
67
|
+
|
68
|
+
line = nextline
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ds
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def initialize
|
77
|
+
@comments = []
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def to_ldif
|
82
|
+
ary = []
|
83
|
+
ary += (@comments || [])
|
84
|
+
|
85
|
+
keys.sort.each {|dn|
|
86
|
+
ary << "dn: #{dn}"
|
87
|
+
|
88
|
+
self[dn].keys.map {|sym| sym.to_s}.sort.each {|attr|
|
89
|
+
self[dn][attr.intern].each {|val|
|
90
|
+
ary << "#{attr}: #{val}"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
ary << ""
|
95
|
+
}
|
96
|
+
|
97
|
+
block_given? and ary.each {|line| yield line}
|
98
|
+
|
99
|
+
ary
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
end # Dataset
|
104
|
+
|
105
|
+
end # LDAP
|
106
|
+
end # Net
|
107
|
+
|
108
|
+
|
@@ -0,0 +1,269 @@
|
|
1
|
+
# $Id$
|
2
|
+
#
|
3
|
+
# LDAP Entry (search-result) support classes
|
4
|
+
#
|
5
|
+
#
|
6
|
+
#----------------------------------------------------------------------------
|
7
|
+
#
|
8
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
9
|
+
#
|
10
|
+
# Gmail: garbagecat10
|
11
|
+
#
|
12
|
+
# This program is free software; you can redistribute it and/or modify
|
13
|
+
# it under the terms of the GNU General Public License as published by
|
14
|
+
# the Free Software Foundation; either version 2 of the License, or
|
15
|
+
# (at your option) any later version.
|
16
|
+
#
|
17
|
+
# This program is distributed in the hope that it will be useful,
|
18
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
19
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
20
|
+
# GNU General Public License for more details.
|
21
|
+
#
|
22
|
+
# You should have received a copy of the GNU General Public License
|
23
|
+
# along with this program; if not, write to the Free Software
|
24
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
25
|
+
#
|
26
|
+
#---------------------------------------------------------------------------
|
27
|
+
#
|
28
|
+
|
29
|
+
|
30
|
+
require 'base64'
|
31
|
+
|
32
|
+
|
33
|
+
module Net
|
34
|
+
class LDAP
|
35
|
+
|
36
|
+
|
37
|
+
# Objects of this class represent individual entries in an LDAP
|
38
|
+
# directory. User code generally does not instantiate this class.
|
39
|
+
# Net::LDAP#search provides objects of this class to user code,
|
40
|
+
# either as block parameters or as return values.
|
41
|
+
#
|
42
|
+
# In LDAP-land, an "entry" is a collection of attributes that are
|
43
|
+
# uniquely and globally identified by a DN ("Distinguished Name").
|
44
|
+
# Attributes are identified by short, descriptive words or phrases.
|
45
|
+
# Although a directory is
|
46
|
+
# free to implement any attribute name, most of them follow rigorous
|
47
|
+
# standards so that the range of commonly-encountered attribute
|
48
|
+
# names is not large.
|
49
|
+
#
|
50
|
+
# An attribute name is case-insensitive. Most directories also
|
51
|
+
# restrict the range of characters allowed in attribute names.
|
52
|
+
# To simplify handling attribute names, Net::LDAP::Entry
|
53
|
+
# internally converts them to a standard format. Therefore, the
|
54
|
+
# methods which take attribute names can take Strings or Symbols,
|
55
|
+
# and work correctly regardless of case or capitalization.
|
56
|
+
#
|
57
|
+
# An attribute consists of zero or more data items called
|
58
|
+
# <i>values.</i> An entry is the combination of a unique DN, a set of attribute
|
59
|
+
# names, and a (possibly-empty) array of values for each attribute.
|
60
|
+
#
|
61
|
+
# Class Net::LDAP::Entry provides convenience methods for dealing
|
62
|
+
# with LDAP entries.
|
63
|
+
# In addition to the methods documented below, you may access individual
|
64
|
+
# attributes of an entry simply by giving the attribute name as
|
65
|
+
# the name of a method call. For example:
|
66
|
+
# ldap.search( ... ) do |entry|
|
67
|
+
# puts "Common name: #{entry.cn}"
|
68
|
+
# puts "Email addresses:"
|
69
|
+
# entry.mail.each {|ma| puts ma}
|
70
|
+
# end
|
71
|
+
# If you use this technique to access an attribute that is not present
|
72
|
+
# in a particular Entry object, a NoMethodError exception will be raised.
|
73
|
+
#
|
74
|
+
#--
|
75
|
+
# Ugly problem to fix someday: We key off the internal hash with
|
76
|
+
# a canonical form of the attribute name: convert to a string,
|
77
|
+
# downcase, then take the symbol. Unfortunately we do this in
|
78
|
+
# at least three places. Should do it in ONE place.
|
79
|
+
class Entry
|
80
|
+
|
81
|
+
|
82
|
+
# This constructor is not generally called by user code.
|
83
|
+
#--
|
84
|
+
# Originally, myhash took a block so we wouldn't have to
|
85
|
+
# make sure its elements returned empty arrays when necessary.
|
86
|
+
# Got rid of that to enable marshalling of Entry objects,
|
87
|
+
# but that doesn't work anyway, because Entry objects have
|
88
|
+
# singleton methods. So we define a custom dump and load.
|
89
|
+
def initialize dn = nil # :nodoc:
|
90
|
+
@myhash = {} # originally: Hash.new {|k,v| k[v] = [] }
|
91
|
+
@myhash[:dn] = [dn]
|
92
|
+
end
|
93
|
+
|
94
|
+
def _dump depth
|
95
|
+
to_ldif
|
96
|
+
end
|
97
|
+
|
98
|
+
class << self
|
99
|
+
def _load entry
|
100
|
+
from_single_ldif_string entry
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#--
|
105
|
+
# Discovered bug, 26Aug06: I noticed that we're not converting the
|
106
|
+
# incoming value to an array if it isn't already one.
|
107
|
+
def []= name, value # :nodoc:
|
108
|
+
sym = name.to_s.downcase.intern
|
109
|
+
value = [value] unless value.is_a?(Array)
|
110
|
+
@myhash[sym] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
#--
|
115
|
+
# We have to deal with this one as we do with []=
|
116
|
+
# because this one and not the other one gets called
|
117
|
+
# in formulations like entry["CN"] << cn.
|
118
|
+
#
|
119
|
+
def [] name # :nodoc:
|
120
|
+
name = name.to_s.downcase.intern unless name.is_a?(Symbol)
|
121
|
+
@myhash[name] || []
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the dn of the Entry as a String.
|
125
|
+
def dn
|
126
|
+
self[:dn][0].to_s
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns an array of the attribute names present in the Entry.
|
130
|
+
def attribute_names
|
131
|
+
@myhash.keys
|
132
|
+
end
|
133
|
+
|
134
|
+
# Accesses each of the attributes present in the Entry.
|
135
|
+
# Calls a user-supplied block with each attribute in turn,
|
136
|
+
# passing two arguments to the block: a Symbol giving
|
137
|
+
# the name of the attribute, and a (possibly empty)
|
138
|
+
# Array of data values.
|
139
|
+
#
|
140
|
+
def each
|
141
|
+
if block_given?
|
142
|
+
attribute_names.each {|a|
|
143
|
+
attr_name,values = a,self[a]
|
144
|
+
yield attr_name, values
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
alias_method :each_attribute, :each
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
# Converts the Entry to a String, representing the
|
154
|
+
# Entry's attributes in LDIF format.
|
155
|
+
#--
|
156
|
+
def to_ldif
|
157
|
+
ary = []
|
158
|
+
ary << "dn: #{dn}\n"
|
159
|
+
v2 = "" # temp value, save on GC
|
160
|
+
each_attribute do |k,v|
|
161
|
+
unless k == :dn
|
162
|
+
v.each {|v1|
|
163
|
+
v2 = if (k == :userpassword) || is_attribute_value_binary?(v1)
|
164
|
+
": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}"
|
165
|
+
else
|
166
|
+
" #{v1}"
|
167
|
+
end
|
168
|
+
ary << "#{k}:#{v2}\n"
|
169
|
+
}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
ary << "\n"
|
173
|
+
ary.join
|
174
|
+
end
|
175
|
+
|
176
|
+
#--
|
177
|
+
# TODO, doesn't support broken lines.
|
178
|
+
# It generates a SINGLE Entry object from an incoming LDIF stream
|
179
|
+
# which is of course useless for big LDIF streams that encode
|
180
|
+
# many objects.
|
181
|
+
# DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED.
|
182
|
+
# As it is, it's useful for unmarshalling objects that we create,
|
183
|
+
# but not for reading arbitrary LDIF files.
|
184
|
+
# Eventually, we should have a class method that parses large LDIF
|
185
|
+
# streams into individual LDIF blocks (delimited by blank lines)
|
186
|
+
# and passes them here.
|
187
|
+
#
|
188
|
+
# There is one oddity, noticed by Matthias Tarasiewicz: as originally
|
189
|
+
# written, this code would return an Entry object in which the DN
|
190
|
+
# attribute consisted of a two-element array, and the first element was
|
191
|
+
# nil. That's because Entry#initialize doesn't like to create an object
|
192
|
+
# without a DN attribute so it adds one: nil. The workaround here is
|
193
|
+
# to wipe out the nil DN after creating the Entry object, and trust the
|
194
|
+
# LDIF string to fill it in. If it doesn't we return a nil at the end.
|
195
|
+
# (30Sep06, FCianfrocca)
|
196
|
+
#
|
197
|
+
class << self
|
198
|
+
def from_single_ldif_string ldif
|
199
|
+
entry = Entry.new
|
200
|
+
entry[:dn] = []
|
201
|
+
ldif.split(/\r?\n/m).each {|line|
|
202
|
+
break if line.length == 0
|
203
|
+
if line =~ /\A([\w]+):(:?)[\s]*/
|
204
|
+
entry[$1] <<= if $2 == ':'
|
205
|
+
Base64.decode64($')
|
206
|
+
else
|
207
|
+
$'
|
208
|
+
end
|
209
|
+
end
|
210
|
+
}
|
211
|
+
entry.dn ? entry : nil
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
#--
|
216
|
+
# Convenience method to convert unknown method names
|
217
|
+
# to attribute references. Of course the method name
|
218
|
+
# comes to us as a symbol, so let's save a little time
|
219
|
+
# and not bother with the to_s.downcase two-step.
|
220
|
+
# Of course that means that a method name like mAIL
|
221
|
+
# won't work, but we shouldn't be encouraging that
|
222
|
+
# kind of bad behavior in the first place.
|
223
|
+
# Maybe we should thow something if the caller sends
|
224
|
+
# arguments or a block...
|
225
|
+
#
|
226
|
+
def method_missing *args, &block # :nodoc:
|
227
|
+
s = args[0].to_s.downcase.intern
|
228
|
+
if attribute_names.include?(s)
|
229
|
+
self[s]
|
230
|
+
elsif s.to_s[-1] == 61 and s.to_s.length > 1
|
231
|
+
value = args[1] or raise RuntimeError.new( "unable to set value" )
|
232
|
+
value = [value] unless value.is_a?(Array)
|
233
|
+
name = s.to_s[0..-2].intern
|
234
|
+
self[name] = value
|
235
|
+
else
|
236
|
+
raise NoMethodError.new( "undefined method '#{s}'" )
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def write
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
#--
|
245
|
+
# Internal convenience method. It seems like the standard
|
246
|
+
# approach in most LDAP tools to base64 encode an attribute
|
247
|
+
# value if its first or last byte is nonprintable, or if
|
248
|
+
# it's a password. But that turns out to be not nearly good
|
249
|
+
# enough. There are plenty of A/D attributes that are binary
|
250
|
+
# in the middle. This is probably a nasty performance killer.
|
251
|
+
def is_attribute_value_binary? value
|
252
|
+
v = value.to_s
|
253
|
+
v.each_byte {|byt|
|
254
|
+
return true if (byt < 32) || (byt > 126)
|
255
|
+
}
|
256
|
+
if v[0..0] == ':' or v[0..0] == '<'
|
257
|
+
return true
|
258
|
+
end
|
259
|
+
false
|
260
|
+
end
|
261
|
+
private :is_attribute_value_binary?
|
262
|
+
|
263
|
+
end # class Entry
|
264
|
+
|
265
|
+
|
266
|
+
end # class LDAP
|
267
|
+
end # module Net
|
268
|
+
|
269
|
+
|
@@ -0,0 +1,499 @@
|
|
1
|
+
# $Id$
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#----------------------------------------------------------------------------
|
5
|
+
#
|
6
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# Gmail: garbagecat10
|
9
|
+
#
|
10
|
+
# This program is free software; you can redistribute it and/or modify
|
11
|
+
# it under the terms of the GNU General Public License as published by
|
12
|
+
# the Free Software Foundation; either version 2 of the License, or
|
13
|
+
# (at your option) any later version.
|
14
|
+
#
|
15
|
+
# This program is distributed in the hope that it will be useful,
|
16
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
+
# GNU General Public License for more details.
|
19
|
+
#
|
20
|
+
# You should have received a copy of the GNU General Public License
|
21
|
+
# along with this program; if not, write to the Free Software
|
22
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
23
|
+
#
|
24
|
+
#---------------------------------------------------------------------------
|
25
|
+
#
|
26
|
+
#
|
27
|
+
|
28
|
+
|
29
|
+
module Net
|
30
|
+
class LDAP
|
31
|
+
|
32
|
+
|
33
|
+
# Class Net::LDAP::Filter is used to constrain
|
34
|
+
# LDAP searches. An object of this class is
|
35
|
+
# passed to Net::LDAP#search in the parameter :filter.
|
36
|
+
#
|
37
|
+
# Net::LDAP::Filter supports the complete set of search filters
|
38
|
+
# available in LDAP, including conjunction, disjunction and negation
|
39
|
+
# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
|
40
|
+
# standard notation for specifying LDAP search filters.
|
41
|
+
#
|
42
|
+
# Here's how to code the familiar "objectclass is present" filter:
|
43
|
+
# f = Net::LDAP::Filter.pres( "objectclass" )
|
44
|
+
# The object returned by this code can be passed directly to
|
45
|
+
# the <tt>:filter</tt> parameter of Net::LDAP#search.
|
46
|
+
#
|
47
|
+
# See the individual class and instance methods below for more examples.
|
48
|
+
#
|
49
|
+
class Filter
|
50
|
+
|
51
|
+
def initialize op, a, b
|
52
|
+
@op = op
|
53
|
+
@left = a
|
54
|
+
@right = b
|
55
|
+
end
|
56
|
+
|
57
|
+
# #eq creates a filter object indicating that the value of
|
58
|
+
# a paticular attribute must be either <i>present</i> or must
|
59
|
+
# match a particular string.
|
60
|
+
#
|
61
|
+
# To specify that an attribute is "present" means that only
|
62
|
+
# directory entries which contain a value for the particular
|
63
|
+
# attribute will be selected by the filter. This is useful
|
64
|
+
# in case of optional attributes such as <tt>mail.</tt>
|
65
|
+
# Presence is indicated by giving the value "*" in the second
|
66
|
+
# parameter to #eq. This example selects only entries that have
|
67
|
+
# one or more values for <tt>sAMAccountName:</tt>
|
68
|
+
# f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
|
69
|
+
#
|
70
|
+
# To match a particular range of values, pass a string as the
|
71
|
+
# second parameter to #eq. The string may contain one or more
|
72
|
+
# "*" characters as wildcards: these match zero or more occurrences
|
73
|
+
# of any character. Full regular-expressions are <i>not</i> supported
|
74
|
+
# due to limitations in the underlying LDAP protocol.
|
75
|
+
# This example selects any entry with a <tt>mail</tt> value containing
|
76
|
+
# the substring "anderson":
|
77
|
+
# f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
|
78
|
+
#--
|
79
|
+
# Removed gt and lt. They ain't in the standard!
|
80
|
+
#
|
81
|
+
def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
|
82
|
+
def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
|
83
|
+
#def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
|
84
|
+
#def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
|
85
|
+
def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
|
86
|
+
def Filter::le attribute, value; Filter.new :le, attribute, value; end
|
87
|
+
|
88
|
+
# #pres( attribute ) is a synonym for #eq( attribute, "*" )
|
89
|
+
#
|
90
|
+
def Filter::pres attribute; Filter.eq attribute, "*"; end
|
91
|
+
|
92
|
+
# operator & ("AND") is used to conjoin two or more filters.
|
93
|
+
# This expression will select only entries that have an <tt>objectclass</tt>
|
94
|
+
# attribute AND have a <tt>mail</tt> attribute that begins with "George":
|
95
|
+
# f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
|
96
|
+
#
|
97
|
+
def & filter; Filter.new :and, self, filter; end
|
98
|
+
|
99
|
+
# operator | ("OR") is used to disjoin two or more filters.
|
100
|
+
# This expression will select entries that have either an <tt>objectclass</tt>
|
101
|
+
# attribute OR a <tt>mail</tt> attribute that begins with "George":
|
102
|
+
# f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
|
103
|
+
#
|
104
|
+
def | filter; Filter.new :or, self, filter; end
|
105
|
+
|
106
|
+
|
107
|
+
#
|
108
|
+
# operator ~ ("NOT") is used to negate a filter.
|
109
|
+
# This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
|
110
|
+
# attribute:
|
111
|
+
# f = ~ Net::LDAP::Filter.pres( "objectclass" )
|
112
|
+
#
|
113
|
+
#--
|
114
|
+
# This operator can't be !, evidently. Try it.
|
115
|
+
# Removed GT and LT. They're not in the RFC.
|
116
|
+
def ~@; Filter.new :not, self, nil; end
|
117
|
+
|
118
|
+
# Equality operator for filters, useful primarily for constructing unit tests.
|
119
|
+
def == filter
|
120
|
+
str = "[@op,@left,@right]"
|
121
|
+
self.instance_eval(str) == filter.instance_eval(str)
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
case @op
|
126
|
+
when :ne
|
127
|
+
"(!(#{@left}=#{@right}))"
|
128
|
+
when :eq
|
129
|
+
"(#{@left}=#{@right})"
|
130
|
+
#when :gt
|
131
|
+
# "#{@left}>#{@right}"
|
132
|
+
#when :lt
|
133
|
+
# "#{@left}<#{@right}"
|
134
|
+
when :ge
|
135
|
+
"#{@left}>=#{@right}"
|
136
|
+
when :le
|
137
|
+
"#{@left}<=#{@right}"
|
138
|
+
when :and
|
139
|
+
"(&(#{@left})(#{@right}))"
|
140
|
+
when :or
|
141
|
+
"(|(#{@left})(#{@right}))"
|
142
|
+
when :not
|
143
|
+
"(!(#{@left}))"
|
144
|
+
else
|
145
|
+
raise "invalid or unsupported operator in LDAP Filter"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
#--
|
151
|
+
# to_ber
|
152
|
+
# Filter ::=
|
153
|
+
# CHOICE {
|
154
|
+
# and [0] SET OF Filter,
|
155
|
+
# or [1] SET OF Filter,
|
156
|
+
# not [2] Filter,
|
157
|
+
# equalityMatch [3] AttributeValueAssertion,
|
158
|
+
# substrings [4] SubstringFilter,
|
159
|
+
# greaterOrEqual [5] AttributeValueAssertion,
|
160
|
+
# lessOrEqual [6] AttributeValueAssertion,
|
161
|
+
# present [7] AttributeType,
|
162
|
+
# approxMatch [8] AttributeValueAssertion
|
163
|
+
# }
|
164
|
+
#
|
165
|
+
# SubstringFilter
|
166
|
+
# SEQUENCE {
|
167
|
+
# type AttributeType,
|
168
|
+
# SEQUENCE OF CHOICE {
|
169
|
+
# initial [0] LDAPString,
|
170
|
+
# any [1] LDAPString,
|
171
|
+
# final [2] LDAPString
|
172
|
+
# }
|
173
|
+
# }
|
174
|
+
#
|
175
|
+
# Parsing substrings is a little tricky.
|
176
|
+
# We use the split method to break a string into substrings
|
177
|
+
# delimited by the * (star) character. But we also need
|
178
|
+
# to know whether there is a star at the head and tail
|
179
|
+
# of the string. A Ruby particularity comes into play here:
|
180
|
+
# if you split on * and the first character of the string is
|
181
|
+
# a star, then split will return an array whose first element
|
182
|
+
# is an _empty_ string. But if the _last_ character of the
|
183
|
+
# string is star, then split will return an array that does
|
184
|
+
# _not_ add an empty string at the end. So we have to deal
|
185
|
+
# with all that specifically.
|
186
|
+
#
|
187
|
+
def to_ber
|
188
|
+
case @op
|
189
|
+
when :eq
|
190
|
+
if @right == "*" # present
|
191
|
+
@left.to_s.to_ber_contextspecific 7
|
192
|
+
elsif @right =~ /[\*]/ #substring
|
193
|
+
ary = @right.split( /[\*]+/ )
|
194
|
+
final_star = @right =~ /[\*]$/
|
195
|
+
initial_star = ary.first == "" and ary.shift
|
196
|
+
|
197
|
+
seq = []
|
198
|
+
unless initial_star
|
199
|
+
seq << ary.shift.to_ber_contextspecific(0)
|
200
|
+
end
|
201
|
+
n_any_strings = ary.length - (final_star ? 0 : 1)
|
202
|
+
#p n_any_strings
|
203
|
+
n_any_strings.times {
|
204
|
+
seq << ary.shift.to_ber_contextspecific(1)
|
205
|
+
}
|
206
|
+
unless final_star
|
207
|
+
seq << ary.shift.to_ber_contextspecific(2)
|
208
|
+
end
|
209
|
+
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
|
210
|
+
else #equality
|
211
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3
|
212
|
+
end
|
213
|
+
when :ge
|
214
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5
|
215
|
+
when :le
|
216
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6
|
217
|
+
when :and
|
218
|
+
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
219
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
|
220
|
+
when :or
|
221
|
+
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
222
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
|
223
|
+
when :not
|
224
|
+
[@left.to_ber].to_ber_contextspecific 2
|
225
|
+
else
|
226
|
+
# ERROR, we'll return objectclass=* to keep things from blowing up,
|
227
|
+
# but that ain't a good answer and we need to kick out an error of some kind.
|
228
|
+
raise "unimplemented search filter"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def unescape(right)
|
233
|
+
right.gsub(/\\([a-fA-F\d]{2,2})/) do
|
234
|
+
[$1.hex].pack("U")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
|
240
|
+
# object. The incoming BER object most likely came to us by parsing an
|
241
|
+
# LDAP searchRequest PDU.
|
242
|
+
# Cf the comments under #to_ber, including the grammar snippet from the RFC.
|
243
|
+
#--
|
244
|
+
# We're hardcoding the BER constants from the RFC. Ought to break them out
|
245
|
+
# into constants.
|
246
|
+
#
|
247
|
+
def Filter::parse_ber ber
|
248
|
+
case ber.ber_identifier
|
249
|
+
when 0xa0 # context-specific constructed 0, "and"
|
250
|
+
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
|
251
|
+
when 0xa1 # context-specific constructed 1, "or"
|
252
|
+
ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
|
253
|
+
when 0xa2 # context-specific constructed 2, "not"
|
254
|
+
~ Filter::parse_ber( ber.first )
|
255
|
+
when 0xa3 # context-specific constructed 3, "equalityMatch"
|
256
|
+
if ber.last == "*"
|
257
|
+
else
|
258
|
+
Filter.eq( ber.first, ber.last )
|
259
|
+
end
|
260
|
+
when 0xa4 # context-specific constructed 4, "substring"
|
261
|
+
str = ""
|
262
|
+
final = false
|
263
|
+
ber.last.each {|b|
|
264
|
+
case b.ber_identifier
|
265
|
+
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
|
266
|
+
raise "unrecognized substring filter, bad initial" if str.length > 0
|
267
|
+
str += b
|
268
|
+
when 0x81 # context-specific primitive 0, SubstringFilter "any"
|
269
|
+
str += "*#{b}"
|
270
|
+
when 0x82 # context-specific primitive 0, SubstringFilter "final"
|
271
|
+
str += "*#{b}"
|
272
|
+
final = true
|
273
|
+
end
|
274
|
+
}
|
275
|
+
str += "*" unless final
|
276
|
+
Filter.eq( ber.first.to_s, str )
|
277
|
+
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
|
278
|
+
Filter.ge( ber.first.to_s, ber.last.to_s )
|
279
|
+
when 0xa6 # context-specific constructed 5, "lessOrEqual"
|
280
|
+
Filter.le( ber.first.to_s, ber.last.to_s )
|
281
|
+
when 0x87 # context-specific primitive 7, "present"
|
282
|
+
# call to_s to get rid of the BER-identifiedness of the incoming string.
|
283
|
+
Filter.pres( ber.to_s )
|
284
|
+
else
|
285
|
+
raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
# Perform filter operations against a user-supplied block. This is useful when implementing
|
291
|
+
# an LDAP directory server. The caller's block will be called with two arguments: first, a
|
292
|
+
# symbol denoting the "operation" of the filter; and second, an array consisting of arguments
|
293
|
+
# to the operation. The user-supplied block (which is MANDATORY) should perform some desired
|
294
|
+
# application-defined processing, and may return a locally-meaningful object that will appear
|
295
|
+
# as a parameter in the :and, :or and :not operations detailed below.
|
296
|
+
#
|
297
|
+
# A typical object to return from the user-supplied block is an array of
|
298
|
+
# Net::LDAP::Filter objects.
|
299
|
+
#
|
300
|
+
# These are the possible values that may be passed to the user-supplied block:
|
301
|
+
# :equalityMatch (the arguments will be an attribute name and a value to be matched);
|
302
|
+
# :substrings (two arguments: an attribute name and a value containing one or more * characters);
|
303
|
+
# :present (one argument: an attribute name);
|
304
|
+
# :greaterOrEqual (two arguments: an attribute name and a value to be compared against);
|
305
|
+
# :lessOrEqual (two arguments: an attribute name and a value to be compared against);
|
306
|
+
# :and (two or more arguments, each of which is an object returned from a recursive call
|
307
|
+
# to #execute, with the same block;
|
308
|
+
# :or (two or more arguments, each of which is an object returned from a recursive call
|
309
|
+
# to #execute, with the same block;
|
310
|
+
# :not (one argument, which is an object returned from a recursive call to #execute with the
|
311
|
+
# the same block.
|
312
|
+
#
|
313
|
+
def execute &block
|
314
|
+
case @op
|
315
|
+
when :eq
|
316
|
+
if @right == "*"
|
317
|
+
yield :present, @left
|
318
|
+
elsif @right.index '*'
|
319
|
+
yield :substrings, @left, @right
|
320
|
+
else
|
321
|
+
yield :equalityMatch, @left, @right
|
322
|
+
end
|
323
|
+
when :ge
|
324
|
+
yield :greaterOrEqual, @left, @right
|
325
|
+
when :le
|
326
|
+
yield :lessOrEqual, @left, @right
|
327
|
+
when :or, :and
|
328
|
+
yield @op, (@left.execute(&block)), (@right.execute(&block))
|
329
|
+
when :not
|
330
|
+
yield @op, (@left.execute(&block))
|
331
|
+
end || []
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
#--
|
336
|
+
# coalesce
|
337
|
+
# This is a private helper method for dealing with chains of ANDs and ORs
|
338
|
+
# that are longer than two. If BOTH of our branches are of the specified
|
339
|
+
# type of joining operator, then return both of them as an array (calling
|
340
|
+
# coalesce recursively). If they're not, then return an array consisting
|
341
|
+
# only of self.
|
342
|
+
#
|
343
|
+
def coalesce operator
|
344
|
+
if @op == operator
|
345
|
+
[@left.coalesce( operator ), @right.coalesce( operator )]
|
346
|
+
else
|
347
|
+
[self]
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
|
353
|
+
#--
|
354
|
+
# We get a Ruby object which comes from parsing an RFC-1777 "Filter"
|
355
|
+
# object. Convert it to a Net::LDAP::Filter.
|
356
|
+
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
357
|
+
# filter types. Could pull them out into a constant.
|
358
|
+
#
|
359
|
+
def Filter::parse_ldap_filter obj
|
360
|
+
case obj.ber_identifier
|
361
|
+
when 0x87 # present. context-specific primitive 7.
|
362
|
+
Filter.eq( obj.to_s, "*" )
|
363
|
+
when 0xa3 # equalityMatch. context-specific constructed 3.
|
364
|
+
Filter.eq( obj[0], obj[1] )
|
365
|
+
else
|
366
|
+
raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
|
371
|
+
|
372
|
+
|
373
|
+
#--
|
374
|
+
# We got a hash of attribute values.
|
375
|
+
# Do we match the attributes?
|
376
|
+
# Return T/F, and call match recursively as necessary.
|
377
|
+
def match entry
|
378
|
+
case @op
|
379
|
+
when :eq
|
380
|
+
if @right == "*"
|
381
|
+
l = entry[@left] and l.length > 0
|
382
|
+
else
|
383
|
+
l = entry[@left] and l = l.to_a and l.index(@right)
|
384
|
+
end
|
385
|
+
else
|
386
|
+
raise LdapError.new( "unknown filter type in match: #{@op}" )
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
|
391
|
+
# to a Net::LDAP::Filter.
|
392
|
+
def self.construct ldap_filter_string
|
393
|
+
FilterParser.new(ldap_filter_string).filter
|
394
|
+
end
|
395
|
+
|
396
|
+
# Synonym for #construct.
|
397
|
+
# to a Net::LDAP::Filter.
|
398
|
+
def self.from_rfc2254 ldap_filter_string
|
399
|
+
construct ldap_filter_string
|
400
|
+
end
|
401
|
+
|
402
|
+
end # class Net::LDAP::Filter
|
403
|
+
|
404
|
+
|
405
|
+
|
406
|
+
class FilterParser #:nodoc:
|
407
|
+
|
408
|
+
attr_reader :filter
|
409
|
+
|
410
|
+
def initialize str
|
411
|
+
require 'strscan'
|
412
|
+
@filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
|
413
|
+
end
|
414
|
+
|
415
|
+
def parse scanner
|
416
|
+
parse_filter_branch(scanner) or parse_paren_expression(scanner)
|
417
|
+
end
|
418
|
+
|
419
|
+
def parse_paren_expression scanner
|
420
|
+
if scanner.scan(/\s*\(\s*/)
|
421
|
+
b = if scanner.scan(/\s*\&\s*/)
|
422
|
+
a = nil
|
423
|
+
branches = []
|
424
|
+
while br = parse_paren_expression(scanner)
|
425
|
+
branches << br
|
426
|
+
end
|
427
|
+
if branches.length >= 2
|
428
|
+
a = branches.shift
|
429
|
+
while branches.length > 0
|
430
|
+
a = a & branches.shift
|
431
|
+
end
|
432
|
+
a
|
433
|
+
end
|
434
|
+
elsif scanner.scan(/\s*\|\s*/)
|
435
|
+
# TODO: DRY!
|
436
|
+
a = nil
|
437
|
+
branches = []
|
438
|
+
while br = parse_paren_expression(scanner)
|
439
|
+
branches << br
|
440
|
+
end
|
441
|
+
if branches.length >= 2
|
442
|
+
a = branches.shift
|
443
|
+
while branches.length > 0
|
444
|
+
a = a | branches.shift
|
445
|
+
end
|
446
|
+
a
|
447
|
+
end
|
448
|
+
elsif scanner.scan(/\s*\!\s*/)
|
449
|
+
br = parse_paren_expression(scanner)
|
450
|
+
if br
|
451
|
+
~ br
|
452
|
+
end
|
453
|
+
else
|
454
|
+
parse_filter_branch( scanner )
|
455
|
+
end
|
456
|
+
|
457
|
+
if b and scanner.scan( /\s*\)\s*/ )
|
458
|
+
b
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Added a greatly-augmented filter contributed by Andre Nathan
|
464
|
+
# for detecting special characters in values. (15Aug06)
|
465
|
+
# Added blanks to the attribute filter (26Oct06)
|
466
|
+
def parse_filter_branch scanner
|
467
|
+
scanner.scan(/\s*/)
|
468
|
+
if token = scanner.scan( /[\w\-_]+/ )
|
469
|
+
scanner.scan(/\s*/)
|
470
|
+
if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
|
471
|
+
scanner.scan(/\s*/)
|
472
|
+
#if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
|
473
|
+
#if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou
|
474
|
+
if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ )
|
475
|
+
case op
|
476
|
+
when "="
|
477
|
+
Filter.eq( token, value )
|
478
|
+
when "!="
|
479
|
+
Filter.ne( token, value )
|
480
|
+
when "<"
|
481
|
+
Filter.lt( token, value )
|
482
|
+
when "<="
|
483
|
+
Filter.le( token, value )
|
484
|
+
when ">"
|
485
|
+
Filter.gt( token, value )
|
486
|
+
when ">="
|
487
|
+
Filter.ge( token, value )
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
end # class Net::LDAP::FilterParser
|
495
|
+
|
496
|
+
end # class Net::LDAP
|
497
|
+
end # module Net
|
498
|
+
|
499
|
+
|