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.
@@ -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