ruby-ldapserver 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +27 -0
- data/ChangeLog +83 -0
- data/Manifest.txt +32 -0
- data/README +222 -0
- data/Rakefile +22 -0
- data/examples/README +89 -0
- data/examples/mkcert.rb +31 -0
- data/examples/rbslapd1.rb +111 -0
- data/examples/rbslapd2.rb +161 -0
- data/examples/rbslapd3.rb +172 -0
- data/examples/speedtest.rb +37 -0
- data/lib/ldap/server.rb +4 -0
- data/lib/ldap/server/connection.rb +273 -0
- data/lib/ldap/server/filter.rb +223 -0
- data/lib/ldap/server/match.rb +283 -0
- data/lib/ldap/server/operation.rb +487 -0
- data/lib/ldap/server/preforkserver.rb +93 -0
- data/lib/ldap/server/result.rb +71 -0
- data/lib/ldap/server/schema.rb +592 -0
- data/lib/ldap/server/server.rb +89 -0
- data/lib/ldap/server/syntax.rb +235 -0
- data/lib/ldap/server/tcpserver.rb +91 -0
- data/lib/ldap/server/util.rb +88 -0
- data/lib/ldap/server/version.rb +11 -0
- data/test/core.schema +582 -0
- data/test/encoding_test.rb +279 -0
- data/test/filter_test.rb +107 -0
- data/test/match_test.rb +59 -0
- data/test/schema_test.rb +113 -0
- data/test/syntax_test.rb +40 -0
- data/test/test_helper.rb +2 -0
- data/test/util_test.rb +51 -0
- metadata +98 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'ldap/server/syntax'
|
2
|
+
require 'ldap/server/result'
|
3
|
+
|
4
|
+
module LDAP
|
5
|
+
class Server
|
6
|
+
|
7
|
+
# A class which holds LDAP MatchingRules. For now there is a global pool
|
8
|
+
# of MatchingRule objects (rather than each Schema object having
|
9
|
+
# its own pool)
|
10
|
+
|
11
|
+
class MatchingRule
|
12
|
+
attr_reader :oid, :names, :syntax, :desc, :obsolete
|
13
|
+
|
14
|
+
# Create a new MatchingRule object
|
15
|
+
|
16
|
+
def initialize(oid, names, syntax, desc=nil, obsolete=false, &blk)
|
17
|
+
@oid = oid
|
18
|
+
@names = names
|
19
|
+
@names = [@names] unless @names.is_a?(Array)
|
20
|
+
@desc = desc
|
21
|
+
@obsolete = obsolete
|
22
|
+
@syntax = LDAP::Server::Syntax.find(syntax) # creates new obj if reqd
|
23
|
+
@def = nil
|
24
|
+
# initialization hook
|
25
|
+
self.instance_eval(&blk) if blk
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
(@names && names[0]) || @oid
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
(@names && names[0]) || @oid
|
34
|
+
end
|
35
|
+
|
36
|
+
def normalize(x)
|
37
|
+
x
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new MatchingRule object, given its description string
|
41
|
+
|
42
|
+
def self.from_def(str, &blk)
|
43
|
+
m = LDAP::Server::Syntax::MatchingRuleDescription.match(str)
|
44
|
+
raise LDAP::ResultError::InvalidAttributeSyntax,
|
45
|
+
"Bad MatchingRuleDescription #{str.inspect}" unless m
|
46
|
+
new(m[1], m[2].scan(/'(.*?)'/).flatten, m[5], m[3], m[4], &blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_def
|
50
|
+
return @def if @def
|
51
|
+
ans = "( #{@oid} "
|
52
|
+
if names.nil? or @names.empty?
|
53
|
+
# nothing
|
54
|
+
elsif @names.size == 1
|
55
|
+
ans << "NAME '#{@names[0]}' "
|
56
|
+
else
|
57
|
+
ans << "NAME ( "
|
58
|
+
@names.each { |n| ans << "'#{n}' " }
|
59
|
+
ans << ") "
|
60
|
+
end
|
61
|
+
ans << "DESC '#@desc' " if @desc
|
62
|
+
ans << "OBSOLETE " if @obsolete
|
63
|
+
ans << "SYNTAX #@syntax " if @syntax
|
64
|
+
ans << ")"
|
65
|
+
@def = ans
|
66
|
+
end
|
67
|
+
|
68
|
+
@@rules = {} # oid / name / alias => object
|
69
|
+
|
70
|
+
# Add a new matching rule
|
71
|
+
|
72
|
+
def self.add(*args, &blk)
|
73
|
+
s = new(*args, &blk)
|
74
|
+
@@rules[s.oid] = s
|
75
|
+
return if s.names.nil?
|
76
|
+
s.names.each do |n|
|
77
|
+
@@rules[n.downcase] = s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Find a MatchingRule object given a name or oid, or return nil
|
82
|
+
# (? should we create one automatically, like Syntax)
|
83
|
+
|
84
|
+
def self.find(x)
|
85
|
+
return x if x.nil? or x.is_a?(LDAP::Server::MatchingRule)
|
86
|
+
@@rules[x.downcase]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return all known matching rules
|
90
|
+
|
91
|
+
def self.all_matching_rules
|
92
|
+
@@rules.values.uniq
|
93
|
+
end
|
94
|
+
|
95
|
+
# Now some things we can mixin to a MatchingRule when needed.
|
96
|
+
# Replace 'normalize' with a function which gives the canonical
|
97
|
+
# version of a value for comparison.
|
98
|
+
|
99
|
+
module Equality
|
100
|
+
def eq(vals, m)
|
101
|
+
return false if vals.nil?
|
102
|
+
m = normalize(m)
|
103
|
+
vals.each { |v| return true if normalize(v) == m }
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module Ordering
|
109
|
+
def ge(vals, m)
|
110
|
+
return false if vals.nil?
|
111
|
+
m = normalize(m)
|
112
|
+
vals.each { |v| return true if normalize(v) >= m }
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
|
116
|
+
def le(vals, m)
|
117
|
+
return false if vals.nil?
|
118
|
+
m = normalize(m)
|
119
|
+
vals.each { |v| return true if normalize(v) <= m }
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module Substrings
|
125
|
+
def substrings(vals, *ss)
|
126
|
+
return false if vals.nil?
|
127
|
+
|
128
|
+
# convert the condition list into a regexp
|
129
|
+
re = []
|
130
|
+
re << "^#{Regexp.escape(normalize(ss[0]).to_s)}" if ss[0]
|
131
|
+
ss[1..-2].each { |s| re << Regexp.escape(normalize(s).to_s) }
|
132
|
+
re << "#{Regexp.escape(normalize(ss[-1]).to_s)}$" if ss[-1]
|
133
|
+
re = Regexp.new(re.join(".*"))
|
134
|
+
|
135
|
+
vals.each do |v|
|
136
|
+
v = normalize(v).to_s
|
137
|
+
return true if re.match(v)
|
138
|
+
end
|
139
|
+
return false
|
140
|
+
end
|
141
|
+
end # module Substrings
|
142
|
+
|
143
|
+
class DefaultMatchingClass
|
144
|
+
include MatchingRule::Equality
|
145
|
+
include MatchingRule::Ordering
|
146
|
+
include MatchingRule::Substrings
|
147
|
+
def normalize(x)
|
148
|
+
x
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
DefaultMatch = DefaultMatchingClass.new
|
153
|
+
|
154
|
+
end # class MatchingRule
|
155
|
+
|
156
|
+
#
|
157
|
+
# And now, here are some matching rules you can use (RFC2252 section 8)
|
158
|
+
#
|
159
|
+
|
160
|
+
class MatchingRule
|
161
|
+
|
162
|
+
add('2.5.13.0', 'objectIdentifierMatch', '1.3.6.1.4.1.1466.115.121.1.38') do
|
163
|
+
extend Equality
|
164
|
+
end
|
165
|
+
# FIXME: Filters should return undef if the OID is not in the schema
|
166
|
+
# (which means passing in the schema to every equality test)
|
167
|
+
|
168
|
+
add('2.5.13.1', 'distinguishedNameMatch', '1.3.6.1.4.1.1466.115.121.1.12') do
|
169
|
+
extend Equality
|
170
|
+
end
|
171
|
+
# FIXME: Distinguished Name matching is supposed to parse the DN into
|
172
|
+
# its parts and then apply the schema equality rules to each part
|
173
|
+
# (i.e. some parts may be case-sensitive, others case-insensitive)
|
174
|
+
# This is just one of the many nonsense design decisions in LDAP :-(
|
175
|
+
|
176
|
+
# How is a DirectoryString different to an IA5String or a PrintableString?
|
177
|
+
|
178
|
+
module StringTrim
|
179
|
+
def normalize(x); x.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
|
180
|
+
end
|
181
|
+
|
182
|
+
module StringDowncase
|
183
|
+
def normalize(x); x.downcase.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
|
184
|
+
end
|
185
|
+
|
186
|
+
add('2.5.13.2', 'caseIgnoreMatch', '1.3.6.1.4.1.1466.115.1') do
|
187
|
+
extend Equality
|
188
|
+
extend StringDowncase
|
189
|
+
end
|
190
|
+
|
191
|
+
module Integer
|
192
|
+
def normalize(x); x.to_i; end
|
193
|
+
end
|
194
|
+
|
195
|
+
add('2.5.13.8', 'numericStringMatch', '1.3.6.1.4.1.1466.115.121.1.36') do
|
196
|
+
extend Equality
|
197
|
+
extend Integer
|
198
|
+
end
|
199
|
+
|
200
|
+
# TODO: Add semantics for these (difficult since RFC2252 doesn't give
|
201
|
+
# them, so we presumably have to go through X.500)
|
202
|
+
add('2.5.13.11', 'caseIgnoreListMatch', '1.3.6.1.4.1.1466.115.121.1.41')
|
203
|
+
add('2.5.13.14', 'integerMatch', '1.3.6.1.4.1.1466.115.121.1.27') do
|
204
|
+
extend Equality
|
205
|
+
extend Integer
|
206
|
+
end
|
207
|
+
add('2.5.13.16', 'bitStringMatch', '1.3.6.1.4.1.1466.115.121.1.6')
|
208
|
+
add('2.5.13.20', 'telephoneNumberMatch', '1.3.6.1.4.1.1466.115.121.1.50') do
|
209
|
+
extend Equality
|
210
|
+
extend StringTrim
|
211
|
+
end
|
212
|
+
add('2.5.13.22', 'presentationAddressMatch', '1.3.6.1.4.1.1466.115.121.1.43')
|
213
|
+
add('2.5.13.23', 'uniqueMemberMatch', '1.3.6.1.4.1.1466.115.121.1.34')
|
214
|
+
add('2.5.13.24', 'protocolInformationMatch', '1.3.6.1.4.1.1466.115.121.1.42')
|
215
|
+
add('2.5.13.27', 'generalizedTimeMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Equality }
|
216
|
+
|
217
|
+
# IA5 stuff. FIXME: What's the correct way to 'downcase' UTF8 strings?
|
218
|
+
|
219
|
+
module IA5Trim
|
220
|
+
def normalize(x); x.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
|
221
|
+
end
|
222
|
+
|
223
|
+
module IA5Downcase
|
224
|
+
def normalize(x); x.downcase.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
|
225
|
+
end
|
226
|
+
|
227
|
+
add('1.3.6.1.4.1.1466.109.114.1', 'caseExactIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
|
228
|
+
extend Equality
|
229
|
+
extend IA5Trim
|
230
|
+
end
|
231
|
+
|
232
|
+
add('1.3.6.1.4.1.1466.109.114.2', 'caseIgnoreIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
|
233
|
+
extend Equality
|
234
|
+
extend IA5Downcase
|
235
|
+
end
|
236
|
+
|
237
|
+
add('2.5.13.28', 'generalizedTimeOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Ordering }
|
238
|
+
add('2.5.13.3', 'caseIgnoreOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') do
|
239
|
+
extend Ordering
|
240
|
+
extend StringDowncase
|
241
|
+
end
|
242
|
+
|
243
|
+
add('2.5.13.4', 'caseIgnoreSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
244
|
+
extend Substrings
|
245
|
+
extend StringDowncase
|
246
|
+
end
|
247
|
+
add('2.5.13.21', 'telephoneNumberSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
248
|
+
extend Substrings
|
249
|
+
end
|
250
|
+
add('2.5.13.10', 'numericStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
251
|
+
extend Substrings
|
252
|
+
end
|
253
|
+
|
254
|
+
# from OpenLDAP
|
255
|
+
add('1.3.6.1.4.1.4203.1.2.1', 'caseExactIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
|
256
|
+
extend Substrings
|
257
|
+
extend IA5Trim
|
258
|
+
end
|
259
|
+
add('1.3.6.1.4.1.1466.109.114.3', 'caseIgnoreIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
|
260
|
+
extend Substrings
|
261
|
+
extend IA5Downcase
|
262
|
+
end
|
263
|
+
add('2.5.13.5', 'caseExactMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Equality }
|
264
|
+
add('2.5.13.6', 'caseExactOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Ordering }
|
265
|
+
add('2.5.13.7', 'caseExactSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') { extend Substrings }
|
266
|
+
add('2.5.13.9', 'numericStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.36') { extend Ordering; extend Integer }
|
267
|
+
add('2.5.13.13', 'booleanMatch', '1.3.6.1.4.1.1466.115.121.1.7') do
|
268
|
+
extend Equality
|
269
|
+
def self.normalize(x)
|
270
|
+
return true if x == 'TRUE'
|
271
|
+
return false if x == 'FALSE'
|
272
|
+
x
|
273
|
+
end
|
274
|
+
end
|
275
|
+
add('2.5.13.15', 'integerOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.27') { extend Ordering; extend Integer }
|
276
|
+
add('2.5.13.17', 'octetStringMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Equality }
|
277
|
+
add('2.5.13.18', 'octetStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Ordering }
|
278
|
+
add('2.5.13.19', 'octetStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Substrings }
|
279
|
+
|
280
|
+
end # class MatchingRule
|
281
|
+
|
282
|
+
end # class Server
|
283
|
+
end # module LDAP
|
@@ -0,0 +1,487 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'ldap/server/result'
|
3
|
+
require 'ldap/server/filter'
|
4
|
+
|
5
|
+
module LDAP
|
6
|
+
class Server
|
7
|
+
|
8
|
+
# Scope
|
9
|
+
BaseObject = 0
|
10
|
+
SingleLevel = 1
|
11
|
+
WholeSubtree = 2
|
12
|
+
|
13
|
+
# DerefAliases
|
14
|
+
NeverDerefAliases = 0
|
15
|
+
DerefInSearching = 1
|
16
|
+
DerefFindingBaseObj = 2
|
17
|
+
DerefAlways = 3
|
18
|
+
|
19
|
+
# Object to handle a single LDAP request. Typically you would
|
20
|
+
# subclass this object and override methods 'simple_bind', 'search' etc.
|
21
|
+
# The do_xxx methods are internal, and handle the parsing of requests
|
22
|
+
# and the sending of responses.
|
23
|
+
|
24
|
+
class Operation
|
25
|
+
|
26
|
+
# An instance of this object is created by the Connection object
|
27
|
+
# for each operation which is requested by the client. If you subclass
|
28
|
+
# Operation, and you override initialize, make sure you call 'super'.
|
29
|
+
|
30
|
+
def initialize(connection, messageID)
|
31
|
+
@connection = connection
|
32
|
+
@respEnvelope = OpenSSL::ASN1::Sequence([
|
33
|
+
OpenSSL::ASN1::Integer(messageID),
|
34
|
+
# protocolOp,
|
35
|
+
# controls [0] OPTIONAL,
|
36
|
+
])
|
37
|
+
@schema = @connection.opt[:schema]
|
38
|
+
@server = @connection.opt[:server]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Send a log message
|
42
|
+
|
43
|
+
def log(*args)
|
44
|
+
@connection.log(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Send an exception report to the log
|
48
|
+
|
49
|
+
def log_exception(e)
|
50
|
+
@connection.log "#{e}: #{e.backtrace.join("\n\tfrom ")}"
|
51
|
+
end
|
52
|
+
|
53
|
+
##################################################
|
54
|
+
### Utility methods to send protocol responses ###
|
55
|
+
##################################################
|
56
|
+
|
57
|
+
def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
|
58
|
+
@respEnvelope.value[1] = protocolOp
|
59
|
+
if opt[:controls]
|
60
|
+
@respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
|
61
|
+
else
|
62
|
+
@respEnvelope.value.delete_at(2)
|
63
|
+
end
|
64
|
+
|
65
|
+
if false # $debug
|
66
|
+
puts "Response:"
|
67
|
+
p @respEnvelope
|
68
|
+
p @respEnvelope.to_der.unpack("H*")
|
69
|
+
end
|
70
|
+
|
71
|
+
@connection.write(@respEnvelope.to_der)
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
|
75
|
+
seq = [
|
76
|
+
OpenSSL::ASN1::Enumerated(resultCode),
|
77
|
+
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
|
78
|
+
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
|
79
|
+
]
|
80
|
+
if opt[:referral]
|
81
|
+
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
|
82
|
+
seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
|
83
|
+
end
|
84
|
+
yield seq if block_given? # opportunity to add more elements
|
85
|
+
|
86
|
+
send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_BindResponse(resultCode, opt={})
|
90
|
+
send_LDAPResult(1, resultCode, opt) do |resp|
|
91
|
+
if opt[:serverSaslCreds]
|
92
|
+
resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
|
98
|
+
# If schema given, return operational attributes only if
|
99
|
+
# explicitly requested
|
100
|
+
|
101
|
+
def send_SearchResultEntry(dn, avs, opt={})
|
102
|
+
@rescount += 1
|
103
|
+
if @sizelimit
|
104
|
+
raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
|
105
|
+
end
|
106
|
+
|
107
|
+
if @schema
|
108
|
+
# normalize the attribute names
|
109
|
+
@attributes = @attributes.collect { |a| @schema.find_attrtype(a).to_s }
|
110
|
+
end
|
111
|
+
|
112
|
+
sendall = @attributes == [] || @attributes.include?("*")
|
113
|
+
avseq = []
|
114
|
+
|
115
|
+
avs.each do |attr, vals|
|
116
|
+
if !@attributes.include?(attr)
|
117
|
+
next unless sendall
|
118
|
+
if @schema
|
119
|
+
a = @schema.find_attrtype(attr)
|
120
|
+
next unless a and (a.usage.nil? or a.usage == :userApplications)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
if @typesOnly
|
125
|
+
vals = []
|
126
|
+
else
|
127
|
+
vals = [vals] unless vals.kind_of?(Array)
|
128
|
+
# FIXME: optionally do a value_to_s conversion here?
|
129
|
+
# FIXME: handle attribute;binary
|
130
|
+
end
|
131
|
+
|
132
|
+
avseq << OpenSSL::ASN1::Sequence([
|
133
|
+
OpenSSL::ASN1::OctetString(attr),
|
134
|
+
OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
|
135
|
+
])
|
136
|
+
end
|
137
|
+
|
138
|
+
send_LDAPMessage(OpenSSL::ASN1::Sequence([
|
139
|
+
OpenSSL::ASN1::OctetString(dn),
|
140
|
+
OpenSSL::ASN1::Sequence(avseq),
|
141
|
+
], 4, :IMPLICIT, :APPLICATION), opt)
|
142
|
+
end
|
143
|
+
|
144
|
+
def send_SearchResultReference(urls, opt={})
|
145
|
+
send_LDAPMessage(OpenSSL::ASN1::Sequence(
|
146
|
+
urls.collect { |url| OpenSSL::ASN1::OctetString(url) }
|
147
|
+
),
|
148
|
+
opt
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
def send_SearchResultDone(resultCode, opt={})
|
153
|
+
send_LDAPResult(5, resultCode, opt)
|
154
|
+
end
|
155
|
+
|
156
|
+
def send_ModifyResponse(resultCode, opt={})
|
157
|
+
send_LDAPResult(7, resultCode, opt)
|
158
|
+
end
|
159
|
+
|
160
|
+
def send_AddResponse(resultCode, opt={})
|
161
|
+
send_LDAPResult(9, resultCode, opt)
|
162
|
+
end
|
163
|
+
|
164
|
+
def send_DelResponse(resultCode, opt={})
|
165
|
+
send_LDAPResult(11, resultCode, opt)
|
166
|
+
end
|
167
|
+
|
168
|
+
def send_ModifyDNResponse(resultCode, opt={})
|
169
|
+
send_LDAPResult(13, resultCode, opt)
|
170
|
+
end
|
171
|
+
|
172
|
+
def send_CompareResponse(resultCode, opt={})
|
173
|
+
send_LDAPResult(15, resultCode, opt)
|
174
|
+
end
|
175
|
+
|
176
|
+
def send_ExtendedResponse(resultCode, opt={})
|
177
|
+
send_LDAPResult(24, resultCode, opt) do |resp|
|
178
|
+
if opt[:responseName]
|
179
|
+
resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
|
180
|
+
end
|
181
|
+
if opt[:response]
|
182
|
+
resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
##########################################
|
188
|
+
### Methods to parse each request type ###
|
189
|
+
##########################################
|
190
|
+
|
191
|
+
def do_bind(protocolOp, controls) # :nodoc:
|
192
|
+
version = protocolOp.value[0].value
|
193
|
+
dn = protocolOp.value[1].value
|
194
|
+
dn = nil if dn == ""
|
195
|
+
authentication = protocolOp.value[2]
|
196
|
+
|
197
|
+
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
|
198
|
+
when 0
|
199
|
+
simple_bind(version, dn, authentication.value)
|
200
|
+
when 3
|
201
|
+
mechanism = authentication.value[0].value
|
202
|
+
credentials = authentication.value[1].value
|
203
|
+
# sasl_bind(version, dn, mechanism, credentials)
|
204
|
+
# FIXME: needs to exchange further BindRequests
|
205
|
+
raise LDAP::ResultError::AuthMethodNotSupported
|
206
|
+
else
|
207
|
+
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
|
208
|
+
end
|
209
|
+
send_BindResponse(0)
|
210
|
+
return dn, version
|
211
|
+
|
212
|
+
rescue LDAP::ResultError => e
|
213
|
+
send_BindResponse(e.to_i, :errorMessage=>e.message)
|
214
|
+
return nil, version
|
215
|
+
end
|
216
|
+
|
217
|
+
# reformat ASN1 into {attr=>[vals], attr=>[vals]}
|
218
|
+
#
|
219
|
+
# AttributeList ::= SEQUENCE OF SEQUENCE {
|
220
|
+
# type AttributeDescription,
|
221
|
+
# vals SET OF AttributeValue }
|
222
|
+
|
223
|
+
def attributelist(set) # :nodoc:
|
224
|
+
av = {}
|
225
|
+
set.value.each do |seq|
|
226
|
+
a = seq.value[0].value
|
227
|
+
if @schema
|
228
|
+
a = @schema.find_attrtype(a).to_s
|
229
|
+
end
|
230
|
+
v = seq.value[1].value.collect { |asn1| asn1.value }
|
231
|
+
# Not clear from the spec whether the same attribute (with
|
232
|
+
# distinct values) can appear more than once in AttributeList
|
233
|
+
raise LDAP::ResultError::AttributeOrValueExists, a if av[a]
|
234
|
+
av[a] = v
|
235
|
+
end
|
236
|
+
return av
|
237
|
+
end
|
238
|
+
|
239
|
+
def do_search(protocolOp, controls) # :nodoc:
|
240
|
+
baseObject = protocolOp.value[0].value
|
241
|
+
scope = protocolOp.value[1].value
|
242
|
+
deref = protocolOp.value[2].value
|
243
|
+
client_sizelimit = protocolOp.value[3].value
|
244
|
+
client_timelimit = protocolOp.value[4].value
|
245
|
+
@typesOnly = protocolOp.value[5].value
|
246
|
+
filter = Filter::parse(protocolOp.value[6], @schema)
|
247
|
+
@attributes = protocolOp.value[7].value.collect {|x| x.value}
|
248
|
+
|
249
|
+
@rescount = 0
|
250
|
+
@sizelimit = server_sizelimit
|
251
|
+
@sizelimit = client_sizelimit if client_sizelimit > 0 and
|
252
|
+
(@sizelimit.nil? or client_sizelimit < @sizelimit)
|
253
|
+
|
254
|
+
if baseObject.empty? and scope == BaseObject
|
255
|
+
send_SearchResultEntry("", @server.root_dse) if
|
256
|
+
@server.root_dse and LDAP::Server::Filter.run(filter, @server.root_dse)
|
257
|
+
send_SearchResultDone(0)
|
258
|
+
return
|
259
|
+
elsif @schema and baseObject == @schema.subschema_dn
|
260
|
+
send_SearchResultEntry(baseObject, @schema.subschema_subentry) if
|
261
|
+
@schema and @schema.subschema_subentry and
|
262
|
+
LDAP::Server::Filter.run(filter, @schema.subschema_subentry)
|
263
|
+
send_SearchResultDone(0)
|
264
|
+
return
|
265
|
+
end
|
266
|
+
|
267
|
+
t = server_timelimit || 10
|
268
|
+
t = client_timelimit if client_timelimit > 0 and client_timelimit < t
|
269
|
+
|
270
|
+
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do
|
271
|
+
search(baseObject, scope, deref, filter)
|
272
|
+
end
|
273
|
+
send_SearchResultDone(0)
|
274
|
+
|
275
|
+
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
|
276
|
+
rescue LDAP::ResultError => e
|
277
|
+
send_SearchResultDone(e.to_i, :errorMessage=>e.message)
|
278
|
+
|
279
|
+
rescue Abandon
|
280
|
+
# send no response
|
281
|
+
|
282
|
+
# Since this Operation is running in its own thread, we have to
|
283
|
+
# catch all other exceptions. Otherwise, in the event of a programming
|
284
|
+
# error, this thread will silently terminate and the client will wait
|
285
|
+
# forever for a response.
|
286
|
+
|
287
|
+
rescue Exception => e
|
288
|
+
log_exception(e)
|
289
|
+
send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
|
290
|
+
end
|
291
|
+
|
292
|
+
def do_modify(protocolOp, controls) # :nodoc:
|
293
|
+
dn = protocolOp.value[0].value
|
294
|
+
modinfo = {}
|
295
|
+
protocolOp.value[1].value.each do |seq|
|
296
|
+
attr = seq.value[1].value[0].value
|
297
|
+
if @schema
|
298
|
+
attr = @schema.find_attrtype(attr).to_s
|
299
|
+
end
|
300
|
+
vals = seq.value[1].value[1].value.collect { |v| v.value }
|
301
|
+
case seq.value[0].value
|
302
|
+
when 0
|
303
|
+
modinfo[attr] = [:add] + vals
|
304
|
+
when 1
|
305
|
+
modinfo[attr] = [:delete] + vals
|
306
|
+
when 2
|
307
|
+
modinfo[attr] = [:replace] + vals
|
308
|
+
else
|
309
|
+
raise LDAP::ResultError::ProtocolError, "Bad modify operation #{seq.value[0].value}"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
modify(dn, modinfo)
|
314
|
+
send_ModifyResponse(0)
|
315
|
+
|
316
|
+
rescue LDAP::ResultError => e
|
317
|
+
send_ModifyResponse(e.to_i, :errorMessage=>e.message)
|
318
|
+
rescue Abandon
|
319
|
+
# no response
|
320
|
+
rescue Exception => e
|
321
|
+
log_exception(e)
|
322
|
+
send_ModifyResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
323
|
+
end
|
324
|
+
|
325
|
+
def do_add(protocolOp, controls) # :nodoc:
|
326
|
+
dn = protocolOp.value[0].value
|
327
|
+
av = attributelist(protocolOp.value[1])
|
328
|
+
add(dn, av)
|
329
|
+
send_AddResponse(0)
|
330
|
+
|
331
|
+
rescue LDAP::ResultError => e
|
332
|
+
send_AddResponse(e.to_i, :errorMessage=>e.message)
|
333
|
+
rescue Abandon
|
334
|
+
# no response
|
335
|
+
rescue Exception => e
|
336
|
+
log_exception(e)
|
337
|
+
send_AddResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
338
|
+
end
|
339
|
+
|
340
|
+
def do_del(protocolOp, controls) # :nodoc:
|
341
|
+
dn = protocolOp.value
|
342
|
+
del(dn)
|
343
|
+
send_DelResponse(0)
|
344
|
+
|
345
|
+
rescue LDAP::ResultError => e
|
346
|
+
send_DelResponse(e.to_i, :errorMessage=>e.message)
|
347
|
+
rescue Abandon
|
348
|
+
# no response
|
349
|
+
rescue Exception => e
|
350
|
+
log_exception(e)
|
351
|
+
send_DelResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
352
|
+
end
|
353
|
+
|
354
|
+
def do_modifydn(protocolOp, controls) # :nodoc:
|
355
|
+
entry = protocolOp.value[0].value
|
356
|
+
newrdn = protocolOp.value[1].value
|
357
|
+
deleteoldrdn = protocolOp.value[2].value
|
358
|
+
if protocolOp.value.size > 3 and protocolOp.value[3].tag == 0
|
359
|
+
newSuperior = protocolOp.value[3].value
|
360
|
+
end
|
361
|
+
modifydn(entry, newrdn, deleteoldrdn, newSuperior)
|
362
|
+
send_ModifyDNResponse(0)
|
363
|
+
|
364
|
+
rescue LDAP::ResultError => e
|
365
|
+
send_ModifyDNResponse(e.to_i, :errorMessage=>e.message)
|
366
|
+
rescue Abandon
|
367
|
+
# no response
|
368
|
+
rescue Exception => e
|
369
|
+
log_exception(e)
|
370
|
+
send_ModifyDNResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
371
|
+
end
|
372
|
+
|
373
|
+
def do_compare(protocolOp, controls) # :nodoc:
|
374
|
+
entry = protocolOp.value[0].value
|
375
|
+
ava = protocolOp.value[1].value
|
376
|
+
attr = ava[0].value
|
377
|
+
if @schema
|
378
|
+
attr = @schema.find_attrtype(attr).to_s
|
379
|
+
end
|
380
|
+
val = ava[1].value
|
381
|
+
if compare(entry, attr, val)
|
382
|
+
send_CompareResponse(6) # compareTrue
|
383
|
+
else
|
384
|
+
send_CompareResponse(5) # compareFalse
|
385
|
+
end
|
386
|
+
|
387
|
+
rescue LDAP::ResultError => e
|
388
|
+
send_CompareResponse(e.to_i, :errorMessage=>e.message)
|
389
|
+
rescue Abandon
|
390
|
+
# no response
|
391
|
+
rescue Exception => e
|
392
|
+
log_exception(e)
|
393
|
+
send_CompareResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
394
|
+
end
|
395
|
+
|
396
|
+
############################################################
|
397
|
+
### Methods to get parameters related to this connection ###
|
398
|
+
############################################################
|
399
|
+
|
400
|
+
# Server-set maximum time limit. Override for more complex behaviour
|
401
|
+
# (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
|
402
|
+
|
403
|
+
def server_timelimit
|
404
|
+
@connection.opt[:timelimit]
|
405
|
+
end
|
406
|
+
|
407
|
+
# Server-set maximum size limit. Override for more complex behaviour
|
408
|
+
# (e.g. limit depends on @connection.binddn). Return nil for unlimited.
|
409
|
+
|
410
|
+
def server_sizelimit
|
411
|
+
@connection.opt[:sizelimit]
|
412
|
+
end
|
413
|
+
|
414
|
+
######################################################
|
415
|
+
### Methods to actually perform the work requested ###
|
416
|
+
######################################################
|
417
|
+
|
418
|
+
# Handle a simple bind request; raise an exception if the bind is
|
419
|
+
# not acceptable, otherwise just return to accept the bind.
|
420
|
+
#
|
421
|
+
# Override this method in your own subclass.
|
422
|
+
|
423
|
+
def simple_bind(version, dn, password)
|
424
|
+
if version != 3
|
425
|
+
raise LDAP::ResultError::ProtocolError, "version 3 only"
|
426
|
+
end
|
427
|
+
if dn
|
428
|
+
raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# Handle a search request; override this.
|
433
|
+
#
|
434
|
+
# Call send_SearchResultEntry for each result found. Raise an exception
|
435
|
+
# if there is a problem. timeLimit, sizeLimit and typesOnly are taken
|
436
|
+
# care of, but you need to perform all authorisation checks yourself,
|
437
|
+
# using @connection.binddn
|
438
|
+
|
439
|
+
def search(basedn, scope, deref, filter, attrs)
|
440
|
+
raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
|
441
|
+
end
|
442
|
+
|
443
|
+
# Handle a modify request; override this
|
444
|
+
#
|
445
|
+
# dn is the object to modify; modification is a hash of
|
446
|
+
# attr => [:add, val, val...] -- add operation
|
447
|
+
# attr => [:replace, val, val...] -- replace operation
|
448
|
+
# attr => [:delete, val, val...] -- delete these values
|
449
|
+
# attr => [:delete] -- delete all values
|
450
|
+
|
451
|
+
def modify(dn, modification)
|
452
|
+
raise LDAP::ResultError::UnwillingToPerform, "modify not implemented"
|
453
|
+
end
|
454
|
+
|
455
|
+
# Handle an add request; override this
|
456
|
+
#
|
457
|
+
# Parameters are the dn of the entry to add, and a hash of
|
458
|
+
# attr=>[val...]
|
459
|
+
# Raise an exception if there is a problem; it is up to you to check
|
460
|
+
# that the connection has sufficient authorisation using @connection.binddn
|
461
|
+
|
462
|
+
def add(dn, av)
|
463
|
+
raise LDAP::ResultError::UnwillingToPerform, "add not implemented"
|
464
|
+
end
|
465
|
+
|
466
|
+
# Handle a del request; override this
|
467
|
+
|
468
|
+
def del(dn)
|
469
|
+
raise LDAP::ResultError::UnwillingToPerform, "delete not implemented"
|
470
|
+
end
|
471
|
+
|
472
|
+
# Handle a modifydn request; override this
|
473
|
+
|
474
|
+
def modifydn(entry, newrdn, deleteoldrdn, newSuperior)
|
475
|
+
raise LDAP::ResultError::UnwillingToPerform, "modifydn not implemented"
|
476
|
+
end
|
477
|
+
|
478
|
+
# Handle a compare request; override this. Return true or false,
|
479
|
+
# or raise an exception for errors.
|
480
|
+
|
481
|
+
def compare(entry, attr, val)
|
482
|
+
raise LDAP::ResultError::UnwillingToPerform, "compare not implemented"
|
483
|
+
end
|
484
|
+
|
485
|
+
end # class Operation
|
486
|
+
end # class Server
|
487
|
+
end # module LDAP
|