deveo-ruby-ldapserver 0.5.2

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,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