ruby-net-ldap 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +272 -0
- data/ChangeLog +28 -0
- data/LICENCE +55 -0
- data/README +29 -0
- data/lib/net/ber.rb +278 -0
- data/lib/net/ldap.rb +1041 -0
- data/lib/net/ldap/dataset.rb +108 -0
- data/lib/net/ldap/entry.rb +82 -0
- data/lib/net/ldap/filter.rb +279 -0
- data/lib/net/ldap/pdu.rb +155 -0
- data/lib/net/ldap/psw.rb +64 -0
- data/lib/net/ldif.rb +39 -0
- data/tests/testber.rb +42 -0
- data/tests/testdata.ldif +101 -0
- data/tests/testem.rb +11 -0
- data/tests/testldap.rb +190 -0
- data/tests/testldif.rb +69 -0
- data/tests/testpsw.rb +28 -0
- metadata +71 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# $Id: dataset.rb 78 2006-04-26 02:57:34Z blackhedd $
|
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,82 @@
|
|
1
|
+
# $Id: entry.rb 95 2006-05-01 12:19:16Z blackhedd $
|
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
|
+
|
31
|
+
|
32
|
+
module Net
|
33
|
+
class LDAP
|
34
|
+
|
35
|
+
|
36
|
+
class Entry
|
37
|
+
|
38
|
+
def initialize dn = nil
|
39
|
+
@myhash = Hash.new {|k,v| k[v] = [] }
|
40
|
+
@myhash[:dn] = [dn]
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def []= name, value
|
45
|
+
sym = name.to_s.downcase.intern
|
46
|
+
@myhash[sym] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
#--
|
51
|
+
# We have to deal with this one as we do []=
|
52
|
+
# because this one and not the other one gets called
|
53
|
+
# in formulations like entry["CN"] << cn.
|
54
|
+
#
|
55
|
+
def [] name
|
56
|
+
name = name.to_s.downcase.intern unless name.is_a?(Symbol)
|
57
|
+
@myhash[name]
|
58
|
+
end
|
59
|
+
|
60
|
+
def dn
|
61
|
+
self[:dn][0]
|
62
|
+
end
|
63
|
+
|
64
|
+
def attribute_names
|
65
|
+
@myhash.keys
|
66
|
+
end
|
67
|
+
|
68
|
+
def each
|
69
|
+
if block_given?
|
70
|
+
attribute_names.each {|a| yield a, self[a] }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :each_attribute, :each
|
75
|
+
|
76
|
+
end # class Entry
|
77
|
+
|
78
|
+
|
79
|
+
end # class LDAP
|
80
|
+
end # module Net
|
81
|
+
|
82
|
+
|
@@ -0,0 +1,279 @@
|
|
1
|
+
# $Id: filter.rb 78 2006-04-26 02:57:34Z blackhedd $
|
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
|
+
def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
|
80
|
+
def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
|
81
|
+
def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
|
82
|
+
def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
|
83
|
+
def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
|
84
|
+
def Filter::le attribute, value; Filter.new :le, attribute, value; end
|
85
|
+
|
86
|
+
# #pres( attribute ) is a synonym for #eq( attribute, "*" )
|
87
|
+
#
|
88
|
+
def Filter::pres attribute; Filter.eq attribute, "*"; end
|
89
|
+
|
90
|
+
# operator & ("AND") is used to conjoin two or more filters.
|
91
|
+
# This expression will select only entries that have an <tt>objectclass</tt>
|
92
|
+
# attribute AND have a <tt>mail</tt> attribute that begins with "George":
|
93
|
+
# f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
|
94
|
+
#
|
95
|
+
def & filter; Filter.new :and, self, filter; end
|
96
|
+
|
97
|
+
# operator | ("OR") is used to disjoin two or more filters.
|
98
|
+
# This expression will select entries that have either an <tt>objectclass</tt>
|
99
|
+
# attribute OR a <tt>mail</tt> attribute that begins with "George":
|
100
|
+
# f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
|
101
|
+
#
|
102
|
+
def | filter; Filter.new :or, self, filter; end
|
103
|
+
|
104
|
+
|
105
|
+
#
|
106
|
+
# operator ~ ("NOT") is used to negate a filter.
|
107
|
+
# This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
|
108
|
+
# attribute:
|
109
|
+
# f = ~ Net::LDAP::Filter.pres( "objectclass" )
|
110
|
+
#
|
111
|
+
#--
|
112
|
+
# This operator can't be !, evidently. Try it.
|
113
|
+
def ~@; Filter.new :not, self, nil; end
|
114
|
+
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
case @op
|
118
|
+
when :ne
|
119
|
+
"(!(#{@left}=#{@right}))"
|
120
|
+
when :eq
|
121
|
+
"(#{@left}=#{@right})"
|
122
|
+
when :gt
|
123
|
+
"#{@left}>#{@right}"
|
124
|
+
when :lt
|
125
|
+
"#{@left}<#{@right}"
|
126
|
+
when :ge
|
127
|
+
"#{@left}>=#{@right}"
|
128
|
+
when :le
|
129
|
+
"#{@left}<=#{@right}"
|
130
|
+
when :and
|
131
|
+
"(&(#{@left})(#{@right}))"
|
132
|
+
when :or
|
133
|
+
"(|(#{@left})(#{@right}))"
|
134
|
+
when :not
|
135
|
+
"(!(#{@left}))"
|
136
|
+
else
|
137
|
+
raise "invalid or unsupported operator in LDAP Filter"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
#--
|
143
|
+
# to_ber
|
144
|
+
# Filter ::=
|
145
|
+
# CHOICE {
|
146
|
+
# and [0] SET OF Filter,
|
147
|
+
# or [1] SET OF Filter,
|
148
|
+
# not [2] Filter,
|
149
|
+
# equalityMatch [3] AttributeValueAssertion,
|
150
|
+
# substrings [4] SubstringFilter,
|
151
|
+
# greaterOrEqual [5] AttributeValueAssertion,
|
152
|
+
# lessOrEqual [6] AttributeValueAssertion,
|
153
|
+
# present [7] AttributeType,
|
154
|
+
# approxMatch [8] AttributeValueAssertion
|
155
|
+
# }
|
156
|
+
#
|
157
|
+
# SubstringFilter
|
158
|
+
# SEQUENCE {
|
159
|
+
# type AttributeType,
|
160
|
+
# SEQUENCE OF CHOICE {
|
161
|
+
# initial [0] LDAPString,
|
162
|
+
# any [1] LDAPString,
|
163
|
+
# final [2] LDAPString
|
164
|
+
# }
|
165
|
+
# }
|
166
|
+
#
|
167
|
+
# Parsing substrings is a little tricky.
|
168
|
+
# We use the split method to break a string into substrings
|
169
|
+
# delimited by the * (star) character. But we also need
|
170
|
+
# to know whether there is a star at the head and tail
|
171
|
+
# of the string. A Ruby particularity comes into play here:
|
172
|
+
# if you split on * and the first character of the string is
|
173
|
+
# a star, then split will return an array whose first element
|
174
|
+
# is an _empty_ string. But if the _last_ character of the
|
175
|
+
# string is star, then split will return an array that does
|
176
|
+
# _not_ add an empty string at the end. So we have to deal
|
177
|
+
# with all that specifically.
|
178
|
+
#
|
179
|
+
def to_ber
|
180
|
+
case @op
|
181
|
+
when :eq
|
182
|
+
if @right == "*" # present
|
183
|
+
@left.to_ber_contextspecific 7
|
184
|
+
elsif @right =~ /[\*]/ #substring
|
185
|
+
ary = @right.split( /[\*]+/ )
|
186
|
+
final_star = @right =~ /[\*]$/
|
187
|
+
initial_star = ary.first == "" and ary.shift
|
188
|
+
|
189
|
+
seq = []
|
190
|
+
unless initial_star
|
191
|
+
seq << ary.shift.to_ber_contextspecific(0)
|
192
|
+
end
|
193
|
+
n_any_strings = ary.length - (final_star ? 0 : 1)
|
194
|
+
p n_any_strings
|
195
|
+
n_any_strings.times {
|
196
|
+
seq << ary.shift.to_ber_contextspecific(1)
|
197
|
+
}
|
198
|
+
unless final_star
|
199
|
+
seq << ary.shift.to_ber_contextspecific(2)
|
200
|
+
end
|
201
|
+
[@left.to_ber, seq.to_ber].to_ber_contextspecific 4
|
202
|
+
else #equality
|
203
|
+
[@left.to_ber, @right.to_ber].to_ber_contextspecific 3
|
204
|
+
end
|
205
|
+
when :and
|
206
|
+
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
207
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
|
208
|
+
when :or
|
209
|
+
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
210
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
|
211
|
+
when :not
|
212
|
+
[@left.to_ber].to_ber_contextspecific 2
|
213
|
+
else
|
214
|
+
# ERROR, we'll return objectclass=* to keep things from blowing up,
|
215
|
+
# but that ain't a good answer and we need to kick out an error of some kind.
|
216
|
+
raise "unimplemented search filter"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
#--
|
221
|
+
# coalesce
|
222
|
+
# This is a private helper method for dealing with chains of ANDs and ORs
|
223
|
+
# that are longer than two. If BOTH of our branches are of the specified
|
224
|
+
# type of joining operator, then return both of them as an array (calling
|
225
|
+
# coalesce recursively). If they're not, then return an array consisting
|
226
|
+
# only of self.
|
227
|
+
#
|
228
|
+
def coalesce operator
|
229
|
+
if @op == operator
|
230
|
+
[@left.coalesce( operator ), @right.coalesce( operator )]
|
231
|
+
else
|
232
|
+
[self]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
|
238
|
+
#--
|
239
|
+
# We get a Ruby object which comes from parsing an RFC-1777 "Filter"
|
240
|
+
# object. Convert it to a Net::LDAP::Filter.
|
241
|
+
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
242
|
+
# filter types. Could pull them out into a constant.
|
243
|
+
#
|
244
|
+
def Filter::parse_ldap_filter obj
|
245
|
+
case obj.ber_identifier
|
246
|
+
when 0x87 # present. context-specific primitive 7.
|
247
|
+
Filter.eq( obj.to_s, "*" )
|
248
|
+
when 0xa3 # equalityMatch. context-specific constructed 3.
|
249
|
+
Filter.eq( obj[0], obj[1] )
|
250
|
+
else
|
251
|
+
raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
#--
|
257
|
+
# We got a hash of attribute values.
|
258
|
+
# Do we match the attributes?
|
259
|
+
# Return T/F, and call match recursively as necessary.
|
260
|
+
def match entry
|
261
|
+
case @op
|
262
|
+
when :eq
|
263
|
+
if @right == "*"
|
264
|
+
l = entry[@left] and l.length > 0
|
265
|
+
else
|
266
|
+
l = entry[@left] and l = l.to_a and l.index(@right)
|
267
|
+
end
|
268
|
+
else
|
269
|
+
raise LdapError.new( "unknown filter type in match: #{@op}" )
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
end # class Net::LDAP::Filter
|
275
|
+
|
276
|
+
end # class Net::LDAP
|
277
|
+
end # module Net
|
278
|
+
|
279
|
+
|