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

- (Operation) initialize(connection, messageID)

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

+ (Object) join_dn(elements)

Reverse of split_dn. Join [elements…] where each element can be attr=>val,&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

+ (Object) split_dn(dn)

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] }

  # 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

- (Object) add(dn, av)

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



462
463
464
# File 'lib/ldap/server/operation.rb', line 462

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

- (Boolean) anonymous?

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

- (Object) attributelist(set)

reformat ASN1 into attr=>[vals]

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


223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/ldap/server/operation.rb', line 223

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

- (Object) compare(entry, attr, val)

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



481
482
483
# File 'lib/ldap/server/operation.rb', line 481

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

- (Object) del(dn)

Handle a del request; override this



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

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

- (Object) do_add(protocolOp, controls)

:nodoc:



325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ldap/server/operation.rb', line 325

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

- (Object) do_bind(protocolOp, controls)

######################################## # Methods to parse each request type ### ########################################



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

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

- (Object) do_compare(protocolOp, controls)

:nodoc:



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/ldap/server/operation.rb', line 373

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

- (Object) do_del(protocolOp, controls)

:nodoc:



340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/ldap/server/operation.rb', line 340

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

- (Object) do_modify(protocolOp, controls)

:nodoc:



292
293
294
295
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
# File 'lib/ldap/server/operation.rb', line 292

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

- (Object) do_modifydn(protocolOp, controls)

:nodoc:



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/ldap/server/operation.rb', line 354

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

- (Object) do_search(protocolOp, controls)

:nodoc:



239
240
241
242
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
# File 'lib/ldap/server/operation.rb', line 239

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

- (Object) log(*args)

Send a log message



43
44
45
# File 'lib/ldap/server/operation.rb', line 43

def log(*args)
  @connection.log(*args)
end

- (Object) log_exception(e)

Send an exception report to the log



49
50
51
# File 'lib/ldap/server/operation.rb', line 49

def log_exception(e)
  @connection.log "#{e}: #{e.backtrace.join("\n\tfrom ")}"
end

- (Object) modify(dn, modification)

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


451
452
453
# File 'lib/ldap/server/operation.rb', line 451

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

- (Object) modifydn(entry, newrdn, deleteoldrdn, newSuperior)

Handle a modifydn request; override this



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

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

- (Object) search(basedn, scope, deref, filter, attrs)

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



439
440
441
# File 'lib/ldap/server/operation.rb', line 439

def search(basedn, scope, deref, filter, attrs)
  raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
end

- (Object) send_AddResponse(resultCode, opt = {})



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

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

- (Object) send_BindResponse(resultCode, opt = {})



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

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

- (Object) send_CompareResponse(resultCode, opt = {})



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

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

- (Object) send_DelResponse(resultCode, opt = {})



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

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

- (Object) send_ExtendedResponse(resultCode, opt = {})



176
177
178
179
180
181
182
183
184
185
# File 'lib/ldap/server/operation.rb', line 176

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

- (Object) send_LDAPMessage(protocolOp, opt = {})

################################################ # Utility methods to send protocol responses ### ################################################



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

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

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

:nodoc:

Yields:

  • (seq)


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

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

- (Object) send_ModifyDNResponse(resultCode, opt = {})



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

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

- (Object) send_ModifyResponse(resultCode, opt = {})



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

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

- (Object) send_SearchResultDone(resultCode, opt = {})



152
153
154
# File 'lib/ldap/server/operation.rb', line 152

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

- (Object) send_SearchResultEntry(dn, avs, opt = {})

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



101
102
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
# File 'lib/ldap/server/operation.rb', line 101

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 }
  end

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

  avs.each do |attr, vals|
    if !@attributes.include?(attr)
      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

- (Object) send_SearchResultReference(urls, opt = {})



144
145
146
147
148
149
150
# File 'lib/ldap/server/operation.rb', line 144

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

- (Object) server_sizelimit

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



410
411
412
# File 'lib/ldap/server/operation.rb', line 410

def server_sizelimit
  @connection.opt[:sizelimit]
end

- (Object) server_timelimit

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



403
404
405
# File 'lib/ldap/server/operation.rb', line 403

def server_timelimit
  @connection.opt[:timelimit]
end

- (Object) simple_bind(version, dn, password)

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.



423
424
425
426
427
428
429
430
# File 'lib/ldap/server/operation.rb', line 423

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