deveo-ruby-ldapserver 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,492 @@
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
+ def log msg, severity = Logger::INFO
42
+ @connection.log msg, severity
43
+ end
44
+
45
+ def debug msg
46
+ @connection.debug msg
47
+ end
48
+
49
+ # Send an exception report to the log
50
+
51
+ def log_exception msg
52
+ @connection.log_exception msg
53
+ end
54
+
55
+ ##################################################
56
+ ### Utility methods to send protocol responses ###
57
+ ##################################################
58
+
59
+ def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
60
+ @respEnvelope.value[1] = protocolOp
61
+ if opt[:controls]
62
+ @respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
63
+ else
64
+ @respEnvelope.value.delete_at(2)
65
+ end
66
+
67
+ if false # $debug
68
+ puts "Response:"
69
+ p @respEnvelope
70
+ p @respEnvelope.to_der.unpack("H*")
71
+ end
72
+
73
+ @connection.write(@respEnvelope.to_der)
74
+ end
75
+
76
+ def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
77
+ seq = [
78
+ OpenSSL::ASN1::Enumerated(resultCode),
79
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
80
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
81
+ ]
82
+ if opt[:referral]
83
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
84
+ seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
85
+ end
86
+ yield seq if block_given? # opportunity to add more elements
87
+
88
+ send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
89
+ end
90
+
91
+ def send_BindResponse(resultCode, opt={})
92
+ send_LDAPResult(1, resultCode, opt) do |resp|
93
+ if opt[:serverSaslCreds]
94
+ resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
95
+ end
96
+ end
97
+ end
98
+
99
+ # Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
100
+ # If schema given, return operational attributes only if
101
+ # explicitly requested
102
+
103
+ def send_SearchResultEntry(dn, avs, opt={})
104
+ @rescount += 1
105
+ if @sizelimit
106
+ raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
107
+ end
108
+
109
+ if @schema
110
+ # normalize the attribute names
111
+ @attributes = @attributes.collect { |a| @schema.find_attrtype(a).to_s }
112
+ else
113
+ @attributes = @attributes.map(&:downcase)
114
+ end
115
+
116
+ sendall = @attributes == [] || @attributes.include?("*")
117
+ avseq = []
118
+
119
+ avs.each do |attr, vals|
120
+ if !@attributes.include?(@schema ? attr : attr.downcase)
121
+ next unless sendall
122
+ if @schema
123
+ a = @schema.find_attrtype(attr)
124
+ next unless a and (a.usage.nil? or a.usage == :userApplications)
125
+ end
126
+ end
127
+
128
+ if @typesOnly
129
+ vals = []
130
+ else
131
+ vals = [vals] unless vals.kind_of?(Array)
132
+ # FIXME: optionally do a value_to_s conversion here?
133
+ # FIXME: handle attribute;binary
134
+ end
135
+
136
+ avseq << OpenSSL::ASN1::Sequence([
137
+ OpenSSL::ASN1::OctetString(attr),
138
+ OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
139
+ ])
140
+ end
141
+
142
+ send_LDAPMessage(OpenSSL::ASN1::Sequence([
143
+ OpenSSL::ASN1::OctetString(dn),
144
+ OpenSSL::ASN1::Sequence(avseq),
145
+ ], 4, :IMPLICIT, :APPLICATION), opt)
146
+ end
147
+
148
+ def send_SearchResultReference(urls, opt={})
149
+ send_LDAPMessage(OpenSSL::ASN1::Sequence(
150
+ urls.collect { |url| OpenSSL::ASN1::OctetString(url) }
151
+ ),
152
+ opt
153
+ )
154
+ end
155
+
156
+ def send_SearchResultDone(resultCode, opt={})
157
+ send_LDAPResult(5, resultCode, opt)
158
+ end
159
+
160
+ def send_ModifyResponse(resultCode, opt={})
161
+ send_LDAPResult(7, resultCode, opt)
162
+ end
163
+
164
+ def send_AddResponse(resultCode, opt={})
165
+ send_LDAPResult(9, resultCode, opt)
166
+ end
167
+
168
+ def send_DelResponse(resultCode, opt={})
169
+ send_LDAPResult(11, resultCode, opt)
170
+ end
171
+
172
+ def send_ModifyDNResponse(resultCode, opt={})
173
+ send_LDAPResult(13, resultCode, opt)
174
+ end
175
+
176
+ def send_CompareResponse(resultCode, opt={})
177
+ send_LDAPResult(15, resultCode, opt)
178
+ end
179
+
180
+ def send_ExtendedResponse(resultCode, opt={})
181
+ send_LDAPResult(24, resultCode, opt) do |resp|
182
+ if opt[:responseName]
183
+ resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
184
+ end
185
+ if opt[:response]
186
+ resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
187
+ end
188
+ end
189
+ end
190
+
191
+ ##########################################
192
+ ### Methods to parse each request type ###
193
+ ##########################################
194
+
195
+ def do_bind(protocolOp, controls) # :nodoc:
196
+ version = protocolOp.value[0].value
197
+ dn = protocolOp.value[1].value
198
+ dn = nil if dn == ""
199
+ authentication = protocolOp.value[2]
200
+
201
+ case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
202
+ when 0
203
+ simple_bind(version, dn, authentication.value)
204
+ when 3
205
+ mechanism = authentication.value[0].value
206
+ credentials = authentication.value[1].value
207
+ # sasl_bind(version, dn, mechanism, credentials)
208
+ # FIXME: needs to exchange further BindRequests
209
+ raise LDAP::ResultError::AuthMethodNotSupported
210
+ else
211
+ raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
212
+ end
213
+ send_BindResponse(0)
214
+ return dn, version
215
+
216
+ rescue LDAP::ResultError => e
217
+ send_BindResponse(e.to_i, :errorMessage=>e.message)
218
+ return nil, version
219
+ end
220
+
221
+ # reformat ASN1 into {attr=>[vals], attr=>[vals]}
222
+ #
223
+ # AttributeList ::= SEQUENCE OF SEQUENCE {
224
+ # type AttributeDescription,
225
+ # vals SET OF AttributeValue }
226
+
227
+ def attributelist(set) # :nodoc:
228
+ av = {}
229
+ set.value.each do |seq|
230
+ a = seq.value[0].value
231
+ if @schema
232
+ a = @schema.find_attrtype(a).to_s
233
+ end
234
+ v = seq.value[1].value.collect { |asn1| asn1.value }
235
+ # Not clear from the spec whether the same attribute (with
236
+ # distinct values) can appear more than once in AttributeList
237
+ raise LDAP::ResultError::AttributeOrValueExists, a if av[a]
238
+ av[a] = v
239
+ end
240
+ return av
241
+ end
242
+
243
+ def do_search(protocolOp, controls) # :nodoc:
244
+ baseObject = protocolOp.value[0].value
245
+ scope = protocolOp.value[1].value
246
+ deref = protocolOp.value[2].value
247
+ client_sizelimit = protocolOp.value[3].value
248
+ client_timelimit = protocolOp.value[4].value
249
+ @typesOnly = protocolOp.value[5].value
250
+ filter = Filter::parse(protocolOp.value[6], @schema)
251
+ @attributes = protocolOp.value[7].value.collect {|x| x.value}
252
+
253
+ @rescount = 0
254
+ @sizelimit = server_sizelimit
255
+ @sizelimit = client_sizelimit if client_sizelimit > 0 and
256
+ (@sizelimit.nil? or client_sizelimit < @sizelimit)
257
+
258
+ if baseObject.empty? and scope == BaseObject
259
+ send_SearchResultEntry("", @server.root_dse) if
260
+ @server.root_dse and LDAP::Server::Filter.run(filter, @server.root_dse)
261
+ send_SearchResultDone(0)
262
+ return
263
+ elsif @schema and baseObject == @schema.subschema_dn
264
+ send_SearchResultEntry(baseObject, @schema.subschema_subentry) if
265
+ @schema and @schema.subschema_subentry and
266
+ LDAP::Server::Filter.run(filter, @schema.subschema_subentry)
267
+ send_SearchResultDone(0)
268
+ return
269
+ end
270
+
271
+ t = server_timelimit || 10
272
+ t = client_timelimit if client_timelimit > 0 and client_timelimit < t
273
+
274
+ Timeout::timeout(t.to_i, LDAP::ResultError::TimeLimitExceeded) do
275
+ search(baseObject, scope, deref, filter)
276
+ end
277
+ send_SearchResultDone(0)
278
+
279
+ # Note that TimeLimitExceeded is a subclass of LDAP::ResultError
280
+ rescue LDAP::ResultError => e
281
+ send_SearchResultDone(e.to_i, :errorMessage=>e.message)
282
+
283
+ rescue Abandon
284
+ # send no response
285
+
286
+ # Since this Operation is running in its own thread, we have to
287
+ # catch all other exceptions. Otherwise, in the event of a programming
288
+ # error, this thread will silently terminate and the client will wait
289
+ # forever for a response.
290
+
291
+ rescue Exception => e
292
+ log_exception(e)
293
+ send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
294
+ end
295
+
296
+ def do_modify(protocolOp, controls) # :nodoc:
297
+ dn = protocolOp.value[0].value
298
+ modinfo = {}
299
+ protocolOp.value[1].value.each do |seq|
300
+ attr = seq.value[1].value[0].value
301
+ if @schema
302
+ attr = @schema.find_attrtype(attr).to_s
303
+ end
304
+ vals = seq.value[1].value[1].value.collect { |v| v.value }
305
+ case seq.value[0].value.to_i
306
+ when 0
307
+ modinfo[attr] = [:add] + vals
308
+ when 1
309
+ modinfo[attr] = [:delete] + vals
310
+ when 2
311
+ modinfo[attr] = [:replace] + vals
312
+ else
313
+ raise LDAP::ResultError::ProtocolError, "Bad modify operation #{seq.value[0].value}"
314
+ end
315
+ end
316
+
317
+ modify(dn, modinfo)
318
+ send_ModifyResponse(0)
319
+
320
+ rescue LDAP::ResultError => e
321
+ send_ModifyResponse(e.to_i, :errorMessage=>e.message)
322
+ rescue Abandon
323
+ # no response
324
+ rescue Exception => e
325
+ log_exception(e)
326
+ send_ModifyResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
327
+ end
328
+
329
+ def do_add(protocolOp, controls) # :nodoc:
330
+ dn = protocolOp.value[0].value
331
+ av = attributelist(protocolOp.value[1])
332
+ add(dn, av)
333
+ send_AddResponse(0)
334
+
335
+ rescue LDAP::ResultError => e
336
+ send_AddResponse(e.to_i, :errorMessage=>e.message)
337
+ rescue Abandon
338
+ # no response
339
+ rescue Exception => e
340
+ log_exception(e)
341
+ send_AddResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
342
+ end
343
+
344
+ def do_del(protocolOp, controls) # :nodoc:
345
+ dn = protocolOp.value
346
+ del(dn)
347
+ send_DelResponse(0)
348
+
349
+ rescue LDAP::ResultError => e
350
+ send_DelResponse(e.to_i, :errorMessage=>e.message)
351
+ rescue Abandon
352
+ # no response
353
+ rescue Exception => e
354
+ log_exception(e)
355
+ send_DelResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
356
+ end
357
+
358
+ def do_modifydn(protocolOp, controls) # :nodoc:
359
+ entry = protocolOp.value[0].value
360
+ newrdn = protocolOp.value[1].value
361
+ deleteoldrdn = protocolOp.value[2].value
362
+ if protocolOp.value.size > 3 and protocolOp.value[3].tag == 0
363
+ newSuperior = protocolOp.value[3].value
364
+ end
365
+ modifydn(entry, newrdn, deleteoldrdn, newSuperior)
366
+ send_ModifyDNResponse(0)
367
+
368
+ rescue LDAP::ResultError => e
369
+ send_ModifyDNResponse(e.to_i, :errorMessage=>e.message)
370
+ rescue Abandon
371
+ # no response
372
+ rescue Exception => e
373
+ log_exception(e)
374
+ send_ModifyDNResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
375
+ end
376
+
377
+ def do_compare(protocolOp, controls) # :nodoc:
378
+ entry = protocolOp.value[0].value
379
+ ava = protocolOp.value[1].value
380
+ attr = ava[0].value
381
+ if @schema
382
+ attr = @schema.find_attrtype(attr).to_s
383
+ end
384
+ val = ava[1].value
385
+ if compare(entry, attr, val)
386
+ send_CompareResponse(6) # compareTrue
387
+ else
388
+ send_CompareResponse(5) # compareFalse
389
+ end
390
+
391
+ rescue LDAP::ResultError => e
392
+ send_CompareResponse(e.to_i, :errorMessage=>e.message)
393
+ rescue Abandon
394
+ # no response
395
+ rescue Exception => e
396
+ log_exception(e)
397
+ send_CompareResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
398
+ end
399
+
400
+ ############################################################
401
+ ### Methods to get parameters related to this connection ###
402
+ ############################################################
403
+
404
+ # Server-set maximum time limit. Override for more complex behaviour
405
+ # (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
406
+
407
+ def server_timelimit
408
+ @connection.opt[:timelimit]
409
+ end
410
+
411
+ # Server-set maximum size limit. Override for more complex behaviour
412
+ # (e.g. limit depends on @connection.binddn). Return nil for unlimited.
413
+
414
+ def server_sizelimit
415
+ @connection.opt[:sizelimit]
416
+ end
417
+
418
+ ######################################################
419
+ ### Methods to actually perform the work requested ###
420
+ ######################################################
421
+
422
+ # Handle a simple bind request; raise an exception if the bind is
423
+ # not acceptable, otherwise just return to accept the bind.
424
+ #
425
+ # Override this method in your own subclass.
426
+
427
+ def simple_bind(version, dn, password)
428
+ if version != 3
429
+ raise LDAP::ResultError::ProtocolError, "version 3 only"
430
+ end
431
+ if dn
432
+ raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
433
+ end
434
+ end
435
+
436
+ # Handle a search request; override this.
437
+ #
438
+ # Call send_SearchResultEntry for each result found. Raise an exception
439
+ # if there is a problem. timeLimit, sizeLimit and typesOnly are taken
440
+ # care of, but you need to perform all authorisation checks yourself,
441
+ # using @connection.binddn
442
+
443
+ def search(basedn, scope, deref, filter)
444
+ debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
445
+ raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
446
+ end
447
+
448
+ # Handle a modify request; override this
449
+ #
450
+ # dn is the object to modify; modification is a hash of
451
+ # attr => [:add, val, val...] -- add operation
452
+ # attr => [:replace, val, val...] -- replace operation
453
+ # attr => [:delete, val, val...] -- delete these values
454
+ # attr => [:delete] -- delete all values
455
+
456
+ def modify(dn, modification)
457
+ raise LDAP::ResultError::UnwillingToPerform, "modify not implemented"
458
+ end
459
+
460
+ # Handle an add request; override this
461
+ #
462
+ # Parameters are the dn of the entry to add, and a hash of
463
+ # attr=>[val...]
464
+ # Raise an exception if there is a problem; it is up to you to check
465
+ # that the connection has sufficient authorisation using @connection.binddn
466
+
467
+ def add(dn, av)
468
+ raise LDAP::ResultError::UnwillingToPerform, "add not implemented"
469
+ end
470
+
471
+ # Handle a del request; override this
472
+
473
+ def del(dn)
474
+ raise LDAP::ResultError::UnwillingToPerform, "delete not implemented"
475
+ end
476
+
477
+ # Handle a modifydn request; override this
478
+
479
+ def modifydn(entry, newrdn, deleteoldrdn, newSuperior)
480
+ raise LDAP::ResultError::UnwillingToPerform, "modifydn not implemented"
481
+ end
482
+
483
+ # Handle a compare request; override this. Return true or false,
484
+ # or raise an exception for errors.
485
+
486
+ def compare(entry, attr, val)
487
+ raise LDAP::ResultError::UnwillingToPerform, "compare not implemented"
488
+ end
489
+
490
+ end # class Operation
491
+ end # class Server
492
+ end # module LDAP