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.

Defined Under Namespace

Classes: AttributeRange

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



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

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]
  @attribute_range_limit = @connection.opt[:attribute_range_limit]
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
  dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format "\\%02x", match[1].ord }

  # Now we know that \\ and \, do not exist, it's safe to split
  parts = dn.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



503
504
505
# File 'lib/ldap/server/operation.rb', line 503

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 }


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/ldap/server/operation.rb', line 250

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.



522
523
524
# File 'lib/ldap/server/operation.rb', line 522

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

#debug(msg) ⇒ Object



47
48
49
# File 'lib/ldap/server/operation.rb', line 47

def debug msg
  @connection.debug msg
end

#del(dn) ⇒ Object

Handle a del request; override this



509
510
511
# File 'lib/ldap/server/operation.rb', line 509

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

#do_add(protocolOp, controls) ⇒ Object

:nodoc:



365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/ldap/server/operation.rb', line 365

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



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/ldap/server/operation.rb', line 218

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:



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/ldap/server/operation.rb', line 413

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:



380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/ldap/server/operation.rb', line 380

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:



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/ldap/server/operation.rb', line 332

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:



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/ldap/server/operation.rb', line 394

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:



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
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
324
325
326
327
328
329
330
# File 'lib/ldap/server/operation.rb', line 266

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.to_i
  @typesOnly = protocolOp.value[5].value
  filter = Filter::parse(protocolOp.value[6], @schema)
  attributes = protocolOp.value[7].value.collect {|x| x.value}
  attributes = attributes.map do |attr|
    if attr =~ /(.*);range=(\d+)-(\d+|\*)\z/
      [$1, $2, $3]
    else
      attr
    end
  end
  @attributes = attributes.map do |name, |
    name
  end
  @attribute_ranges = attributes.map do |_, range_start, range_end|
    range_start && AttributeRange.new(range_start, range_end)
  end

  @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

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



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

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

#log_exception(msg) ⇒ Object

Send an exception report to the log



53
54
55
# File 'lib/ldap/server/operation.rb', line 53

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


492
493
494
# File 'lib/ldap/server/operation.rb', line 492

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

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

Handle a modifydn request; override this



515
516
517
# File 'lib/ldap/server/operation.rb', line 515

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



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

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



187
188
189
# File 'lib/ldap/server/operation.rb', line 187

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

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



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

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



199
200
201
# File 'lib/ldap/server/operation.rb', line 199

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

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



191
192
193
# File 'lib/ldap/server/operation.rb', line 191

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

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



203
204
205
206
207
208
209
210
211
212
# File 'lib/ldap/server/operation.rb', line 203

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



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

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)


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

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



195
196
197
# File 'lib/ldap/server/operation.rb', line 195

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

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



183
184
185
# File 'lib/ldap/server/operation.rb', line 183

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

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



179
180
181
# File 'lib/ldap/server/operation.rb', line 179

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



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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/ldap/server/operation.rb', line 108

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.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s }
  end

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

  avs.each_with_index do |(attr, vals), aidx|
    query_attr_idx = @attributes.index(attr)
    if !query_attr_idx
      next unless sendall
      if @schema
        a = @schema.find_attrtype(attr)
        next unless a and (a.usage.nil? or a.usage == :userApplications)
      end
    end
    query_attr = query_attr_idx && @attribute_ranges[query_attr_idx]

    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

    if (@attribute_range_limit && vals.size > @attribute_range_limit) || query_attr&.start
      if query_attr&.start
        range_start = query_attr.start.to_i
        range_end = query_attr.end == "*" ? -1 : query_attr.end.to_i
      else
        range_start = 0
        range_end = @attribute_range_limit ? @attribute_range_limit - 1 : -1
      end
      range_end = range_start + @attribute_range_limit - 1 if @attribute_range_limit && (vals.size - range_start > @attribute_range_limit)
      range_end = -1 if vals.size <= range_end
      rvals = vals[range_start .. range_end]
      vals = []
      avseq << OpenSSL::ASN1::Sequence([
        OpenSSL::ASN1::OctetString("#{attr};range=#{range_start}-#{range_end == -1 ? "*" : range_end}"),
        OpenSSL::ASN1::Set(rvals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
      ])
    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



171
172
173
174
175
176
177
# File 'lib/ldap/server/operation.rb', line 171

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.



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

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.



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

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.



463
464
465
466
467
468
469
470
# File 'lib/ldap/server/operation.rb', line 463

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