Class: LDAP::Server::Operation

Inherits:
Object
  • Object
show all
Defined in:
lib/ldap/server/operation.rb,
lib/ldap/server/util.rb

Overview

Object to handle a single LDAP request. Typically you would subclass this object and override methods ‘simple_bind’, ‘search’ etc. The do_xxx methods are internal, and handle the parsing of requests and the sending of responses.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection, messageID) ⇒ Operation

An instance of this object is created by the Connection object for each operation which is requested by the client. If you subclass Operation, and you override initialize, make sure you call ‘super’.



30
31
32
33
34
35
36
37
38
39
# File 'lib/ldap/server/operation.rb', line 30

def initialize(connection, messageID)
  @connection = connection
  @respEnvelope = OpenSSL::ASN1::Sequence([
    OpenSSL::ASN1::Integer(messageID),
    # protocolOp,
    # controls [0] OPTIONAL,
  ])
  @schema = @connection.opt[:schema]
  @server = @connection.opt[:server]
end

Class Method Details

.join_dn(elements) ⇒ Object

Reverse of split_dn. Join [elements…] where each element can be attr=>val,… or [[attr,val],…] or just [attr,val]



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ldap/server/util.rb', line 66

def self.join_dn(elements)
  dn = ""
  elements.each do |elem|
    av = ""
    elem = [elem] if elem[0].is_a?(String)
    elem.each do |attr,val|
      av << "+" unless av == ""

      av << attr << "=" <<
                 val.sub(/^([# ])/, '\\\\\\1').
                 sub(/( )$/, '\\\\\\1').
                 gsub(/([,+"\\<>;])/, '\\\\\\1')
    end
    dn << "," unless dn == ""
    dn << av
  end
  dn
end

.split_dn(dn) ⇒ Object

Split dn string into its component parts, returning

[ {attr=>val}, {attr=>val}, ... ]

This is pretty horrible legacy stuff from X500; see RFC2253 for the full gore. It’s stupid that the LDAP protocol sends the DN in string form, rather than in ASN1 form (as it does with search filters, for example), even though the DN syntax is defined in terms of ASN1!

Attribute names are downcased, but values are not. For any case-insensitive attributes it’s up to you to downcase them.

Note that only v2 clients should add extra space around the comma. This is accepted, and so is semicolon instead of comma, but the full RFC1779 backwards-compatibility rules (e.g. quoted values) are not implemented.

I think these functions will work correctly with UTF8-encoded characters, given that a multibyte UTF8 character does not contain the bytes 00-7F and therefore we cannot confuse ‘', ’+‘ etc



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/ldap/server/util.rb', line 34

def self.split_dn(dn)
  # convert \\ to \5c, \+ to \2b etc
  dn2 = dn.gsub(/\\([ #,+"\\<>;])/) { "\\%02x" % $1[0].ord }

  # Now we know that \\ and \, do not exist, it's safe to split
  parts = dn2.split(/\s*[,;]\s*/)

  parts.collect do |part|
    res = {}

    # Split each part into attr=val+attr=val
    avs = part.split(/\+/)

    avs.each do |av|
      # These should all be of form attr=value
      unless av =~ /^([^=]+)=(.*)$/
        raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}"
      end
      attr, val = $1.downcase, $2
      # Now we can decode those bits
      attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
      val.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
      res[attr] = val
    end
    res
  end
end

Instance Method Details

#add(dn, av) ⇒ Object

Handle an add request; override this

Parameters are the dn of the entry to add, and a hash of

attr=>[val...]

Raise an exception if there is a problem; it is up to you to check that the connection has sufficient authorisation using @connection.binddn



467
468
469
# File 'lib/ldap/server/operation.rb', line 467

def add(dn, av)
  raise LDAP::ResultError::UnwillingToPerform, "add not implemented"
end

#anonymous?Boolean

Return true if connection is not authenticated

Returns:

  • (Boolean)


10
11
12
# File 'lib/ldap/server/util.rb', line 10

def anonymous?
  @connection.binddn.nil?
end

#attributelist(set) ⇒ Object

reformat ASN1 into vals

AttributeList ::= SEQUENCE OF SEQUENCE {
       type    AttributeDescription,
       vals    SET OF AttributeValue }


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/ldap/server/operation.rb', line 227

def attributelist(set) # :nodoc:
  av = {}
  set.value.each do |seq|
    a = seq.value[0].value
    if @schema
      a = @schema.find_attrtype(a).to_s
    end
    v = seq.value[1].value.collect { |asn1| asn1.value  }
    # Not clear from the spec whether the same attribute (with
    # distinct values) can appear more than once in AttributeList
    raise LDAP::ResultError::AttributeOrValueExists, a if av[a]
    av[a] = v
  end
  return av
end

#compare(entry, attr, val) ⇒ Object

Handle a compare request; override this. Return true or false, or raise an exception for errors.



486
487
488
# File 'lib/ldap/server/operation.rb', line 486

def compare(entry, attr, val)
  raise LDAP::ResultError::UnwillingToPerform, "compare not implemented"
end

#debug(msg) ⇒ Object



45
46
47
# File 'lib/ldap/server/operation.rb', line 45

def debug msg
  @connection.debug msg
end

#del(dn) ⇒ Object

Handle a del request; override this



473
474
475
# File 'lib/ldap/server/operation.rb', line 473

def del(dn)
  raise LDAP::ResultError::UnwillingToPerform, "delete not implemented"
end

#do_add(protocolOp, controls) ⇒ Object

:nodoc:



329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/ldap/server/operation.rb', line 329

def do_add(protocolOp, controls) # :nodoc:
  dn = protocolOp.value[0].value
  av = attributelist(protocolOp.value[1])
  add(dn, av)
  send_AddResponse(0)

rescue LDAP::ResultError => e
  send_AddResponse(e.to_i, :errorMessage=>e.message)
rescue Abandon
  # no response
rescue Exception => e
  log_exception(e)
  send_AddResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
end

#do_bind(protocolOp, controls) ⇒ Object

Methods to parse each request type ###



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ldap/server/operation.rb', line 195

def do_bind(protocolOp, controls) # :nodoc:
  version = protocolOp.value[0].value
  dn = protocolOp.value[1].value
  dn = nil if dn == ""
  authentication = protocolOp.value[2]

  case authentication.tag   # tag_class == :CONTEXT_SPECIFIC (check why)
  when 0
    simple_bind(version, dn, authentication.value)
  when 3
    mechanism = authentication.value[0].value
    credentials = authentication.value[1].value
    # sasl_bind(version, dn, mechanism, credentials)
    # FIXME: needs to exchange further BindRequests
    raise LDAP::ResultError::AuthMethodNotSupported
  else
    raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
  end
  send_BindResponse(0)
  return dn, version

rescue LDAP::ResultError => e
  send_BindResponse(e.to_i, :errorMessage=>e.message)
  return nil, version
end

#do_compare(protocolOp, controls) ⇒ Object

:nodoc:



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/ldap/server/operation.rb', line 377

def do_compare(protocolOp, controls) # :nodoc:
  entry = protocolOp.value[0].value
  ava = protocolOp.value[1].value
  attr = ava[0].value
  if @schema
    attr = @schema.find_attrtype(attr).to_s
  end
  val = ava[1].value
  if compare(entry, attr, val)
    send_CompareResponse(6)  # compareTrue
  else
    send_CompareResponse(5)  # compareFalse
  end

rescue LDAP::ResultError => e
  send_CompareResponse(e.to_i, :errorMessage=>e.message)
rescue Abandon
  # no response
rescue Exception => e
  log_exception(e)
  send_CompareResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
end

#do_del(protocolOp, controls) ⇒ Object

:nodoc:



344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/ldap/server/operation.rb', line 344

def do_del(protocolOp, controls) # :nodoc:
  dn = protocolOp.value
  del(dn)
  send_DelResponse(0)

rescue LDAP::ResultError => e
  send_DelResponse(e.to_i, :errorMessage=>e.message)
rescue Abandon
  # no response
rescue Exception => e
  log_exception(e)
  send_DelResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
end

#do_modify(protocolOp, controls) ⇒ Object

:nodoc:



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/ldap/server/operation.rb', line 296

def do_modify(protocolOp, controls) # :nodoc:
  dn = protocolOp.value[0].value
  modinfo = {}
  protocolOp.value[1].value.each do |seq|
    attr = seq.value[1].value[0].value
    if @schema
      attr = @schema.find_attrtype(attr).to_s
    end
    vals = seq.value[1].value[1].value.collect { |v| v.value }
    case seq.value[0].value.to_i
    when 0
      modinfo[attr] = [:add] + vals
    when 1
      modinfo[attr] = [:delete] + vals
    when 2
      modinfo[attr] = [:replace] + vals
    else
      raise LDAP::ResultError::ProtocolError, "Bad modify operation #{seq.value[0].value}"
    end
  end

  modify(dn, modinfo)
  send_ModifyResponse(0)

rescue LDAP::ResultError => e
  send_ModifyResponse(e.to_i, :errorMessage=>e.message)
rescue Abandon
  # no response
rescue Exception => e
  log_exception(e)
  send_ModifyResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
end

#do_modifydn(protocolOp, controls) ⇒ Object

:nodoc:



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/ldap/server/operation.rb', line 358

def do_modifydn(protocolOp, controls) # :nodoc:
  entry = protocolOp.value[0].value
  newrdn = protocolOp.value[1].value
  deleteoldrdn = protocolOp.value[2].value
  if protocolOp.value.size > 3 and protocolOp.value[3].tag == 0
    newSuperior = protocolOp.value[3].value
  end
  modifydn(entry, newrdn, deleteoldrdn, newSuperior)
  send_ModifyDNResponse(0)

rescue LDAP::ResultError => e
  send_ModifyDNResponse(e.to_i, :errorMessage=>e.message)
rescue Abandon
  # no response
rescue Exception => e
  log_exception(e)
  send_ModifyDNResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
end

#do_search(protocolOp, controls) ⇒ Object

:nodoc:



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/ldap/server/operation.rb', line 243

def do_search(protocolOp, controls) # :nodoc:
  baseObject = protocolOp.value[0].value
  scope = protocolOp.value[1].value
  deref = protocolOp.value[2].value
  client_sizelimit = protocolOp.value[3].value
  client_timelimit = protocolOp.value[4].value
  @typesOnly = protocolOp.value[5].value
  filter = Filter::parse(protocolOp.value[6], @schema)
  @attributes = protocolOp.value[7].value.collect {|x| x.value}

  @rescount = 0
  @sizelimit = server_sizelimit
  @sizelimit = client_sizelimit if client_sizelimit > 0 and
               (@sizelimit.nil? or client_sizelimit < @sizelimit)

  if baseObject.empty? and scope == BaseObject
    send_SearchResultEntry("", @server.root_dse) if
      @server.root_dse and LDAP::Server::Filter.run(filter, @server.root_dse)
    send_SearchResultDone(0)
    return
  elsif @schema and baseObject == @schema.subschema_dn
    send_SearchResultEntry(baseObject, @schema.subschema_subentry) if
      @schema and @schema.subschema_subentry and
      LDAP::Server::Filter.run(filter, @schema.subschema_subentry)
    send_SearchResultDone(0)
    return
  end

  t = server_timelimit || 10
  t = client_timelimit if client_timelimit > 0 and client_timelimit < t

  Timeout::timeout(t.to_i, LDAP::ResultError::TimeLimitExceeded) do
    search(baseObject, scope, deref, filter)
  end
  send_SearchResultDone(0)

# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
rescue LDAP::ResultError => e
  send_SearchResultDone(e.to_i, :errorMessage=>e.message)

rescue Abandon
  # send no response

# Since this Operation is running in its own thread, we have to
# catch all other exceptions. Otherwise, in the event of a programming
# error, this thread will silently terminate and the client will wait
# forever for a response.

rescue Exception => e
  log_exception(e)
  send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
end

#log(msg, severity = Logger::INFO) ⇒ Object



41
42
43
# File 'lib/ldap/server/operation.rb', line 41

def log msg, severity = Logger::INFO
  @connection.log msg, severity
end

#log_exception(msg) ⇒ Object

Send an exception report to the log



51
52
53
# File 'lib/ldap/server/operation.rb', line 51

def log_exception msg
  @connection.log_exception msg
end

#modify(dn, modification) ⇒ Object

Handle a modify request; override this

dn is the object to modify; modification is a hash of

attr => [:add, val, val...]       -- add operation
attr => [:replace, val, val...]   -- replace operation
attr => [:delete, val, val...]    -- delete these values
attr => [:delete]                 -- delete all values


456
457
458
# File 'lib/ldap/server/operation.rb', line 456

def modify(dn, modification)
  raise LDAP::ResultError::UnwillingToPerform, "modify not implemented"
end

#modifydn(entry, newrdn, deleteoldrdn, newSuperior) ⇒ Object

Handle a modifydn request; override this



479
480
481
# File 'lib/ldap/server/operation.rb', line 479

def modifydn(entry, newrdn, deleteoldrdn, newSuperior)
  raise LDAP::ResultError::UnwillingToPerform, "modifydn not implemented"
end

#search(basedn, scope, deref, filter) ⇒ Object

Handle a search request; override this.

Call send_SearchResultEntry for each result found. Raise an exception if there is a problem. timeLimit, sizeLimit and typesOnly are taken care of, but you need to perform all authorisation checks yourself, using @connection.binddn



443
444
445
446
# File 'lib/ldap/server/operation.rb', line 443

def search(basedn, scope, deref, filter)
  debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
  raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
end

#send_AddResponse(resultCode, opt = {}) ⇒ Object



164
165
166
# File 'lib/ldap/server/operation.rb', line 164

def send_AddResponse(resultCode, opt={})
  send_LDAPResult(9, resultCode, opt)
end

#send_BindResponse(resultCode, opt = {}) ⇒ Object



91
92
93
94
95
96
97
# File 'lib/ldap/server/operation.rb', line 91

def send_BindResponse(resultCode, opt={})
  send_LDAPResult(1, resultCode, opt) do |resp|
    if opt[:serverSaslCreds]
      resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
    end
  end
end

#send_CompareResponse(resultCode, opt = {}) ⇒ Object



176
177
178
# File 'lib/ldap/server/operation.rb', line 176

def send_CompareResponse(resultCode, opt={})
  send_LDAPResult(15, resultCode, opt)
end

#send_DelResponse(resultCode, opt = {}) ⇒ Object



168
169
170
# File 'lib/ldap/server/operation.rb', line 168

def send_DelResponse(resultCode, opt={})
  send_LDAPResult(11, resultCode, opt)
end

#send_ExtendedResponse(resultCode, opt = {}) ⇒ Object



180
181
182
183
184
185
186
187
188
189
# File 'lib/ldap/server/operation.rb', line 180

def send_ExtendedResponse(resultCode, opt={})
  send_LDAPResult(24, resultCode, opt) do |resp|
    if opt[:responseName]
      resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
    end
    if opt[:response]
      resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
    end
  end
end

#send_LDAPMessage(protocolOp, opt = {}) ⇒ Object

Utility methods to send protocol responses ###



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ldap/server/operation.rb', line 59

def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
  @respEnvelope.value[1] = protocolOp
  if opt[:controls]
    @respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
  else
    @respEnvelope.value.delete_at(2)
  end

  if false # $debug
    puts "Response:"
    p @respEnvelope
    p @respEnvelope.to_der.unpack("H*")
  end

  @connection.write(@respEnvelope.to_der)
end

#send_LDAPResult(tag, resultCode, opt = {}) {|seq| ... } ⇒ Object

:nodoc:

Yields:

  • (seq)


76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ldap/server/operation.rb', line 76

def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
  seq = [
    OpenSSL::ASN1::Enumerated(resultCode),
    OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
    OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
  ]
  if opt[:referral]
    rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
    seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
  end
  yield seq if block_given?   # opportunity to add more elements
    
  send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
end

#send_ModifyDNResponse(resultCode, opt = {}) ⇒ Object



172
173
174
# File 'lib/ldap/server/operation.rb', line 172

def send_ModifyDNResponse(resultCode, opt={})
  send_LDAPResult(13, resultCode, opt)
end

#send_ModifyResponse(resultCode, opt = {}) ⇒ Object



160
161
162
# File 'lib/ldap/server/operation.rb', line 160

def send_ModifyResponse(resultCode, opt={})
  send_LDAPResult(7, resultCode, opt)
end

#send_SearchResultDone(resultCode, opt = {}) ⇒ Object



156
157
158
# File 'lib/ldap/server/operation.rb', line 156

def send_SearchResultDone(resultCode, opt={})
  send_LDAPResult(5, resultCode, opt)
end

#send_SearchResultEntry(dn, avs, opt = {}) ⇒ Object

Send a found entry. Avs are attr2=> If schema given, return operational attributes only if explicitly requested



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ldap/server/operation.rb', line 103

def send_SearchResultEntry(dn, avs, opt={})
  @rescount += 1
  if @sizelimit
    raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
  end

  if @schema
    # normalize the attribute names
    @attributes = @attributes.collect { |a| @schema.find_attrtype(a).to_s }
  else
    @attributes = @attributes.map(&:downcase)
  end

  sendall = @attributes == [] || @attributes.include?("*")
  avseq = []

  avs.each do |attr, vals|
    if !@attributes.include?(@schema ? attr : attr.downcase)
      next unless sendall
      if @schema
        a = @schema.find_attrtype(attr)
        next unless a and (a.usage.nil? or a.usage == :userApplications)
      end
    end

    if @typesOnly
      vals = [] 
    else
      vals = [vals] unless vals.kind_of?(Array)
      # FIXME: optionally do a value_to_s conversion here?
      # FIXME: handle attribute;binary
    end

    avseq << OpenSSL::ASN1::Sequence([
      OpenSSL::ASN1::OctetString(attr),
      OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
    ])
  end

  send_LDAPMessage(OpenSSL::ASN1::Sequence([
      OpenSSL::ASN1::OctetString(dn),
      OpenSSL::ASN1::Sequence(avseq),
    ], 4, :IMPLICIT, :APPLICATION), opt)
end

#send_SearchResultReference(urls, opt = {}) ⇒ Object



148
149
150
151
152
153
154
# File 'lib/ldap/server/operation.rb', line 148

def send_SearchResultReference(urls, opt={})
  send_LDAPMessage(OpenSSL::ASN1::Sequence(
      urls.collect { |url| OpenSSL::ASN1::OctetString(url) }
    ),
    opt
  )
end

#server_sizelimitObject

Server-set maximum size limit. Override for more complex behaviour (e.g. limit depends on @connection.binddn). Return nil for unlimited.



414
415
416
# File 'lib/ldap/server/operation.rb', line 414

def server_sizelimit
  @connection.opt[:sizelimit]
end

#server_timelimitObject

Server-set maximum time limit. Override for more complex behaviour (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.



407
408
409
# File 'lib/ldap/server/operation.rb', line 407

def server_timelimit
  @connection.opt[:timelimit]
end

#simple_bind(version, dn, password) ⇒ Object

Handle a simple bind request; raise an exception if the bind is not acceptable, otherwise just return to accept the bind.

Override this method in your own subclass.



427
428
429
430
431
432
433
434
# File 'lib/ldap/server/operation.rb', line 427

def simple_bind(version, dn, password)
  if version != 3
    raise LDAP::ResultError::ProtocolError, "version 3 only"
  end
  if dn
    raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
  end
end