ruby-activeldap 0.7.1 → 0.7.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.
@@ -177,7 +177,8 @@
177
177
  # Below is a much more realistic Group class:
178
178
  #
179
179
  # class Group < ActiveLDAP::Base
180
- # ldap_mapping :dnattr => 'cn', :prefix => 'ou=Groups', :classes => ['top', 'posixGroup']
180
+ # ldap_mapping :dnattr => 'cn', :prefix => 'ou=Groups', :classes => ['top', 'posixGroup']<
181
+ # :scope => LDAP::LDAP_SCOPE_ONELEVEL
181
182
  # end
182
183
  #
183
184
  # As you can see, this method is used for defining how this class maps in to LDAP. Let's say that
@@ -205,6 +206,8 @@
205
206
  # :prefix |
206
207
  # :base from configuration.rb
207
208
  #
209
+ # :scope tells ActiveLDAP to only search under ou=Groups, and not to look deeper
210
+ # for dnattr matches. (e.g. cn=develop,ou=DevGroups,ou=Groups,dc=dataspill,dc=org)
208
211
  #
209
212
  # Something's missing: :classes. :classes is used to tell Ruby/ActiveLDAP what
210
213
  # the minimum requirement is when creating a new object. LDAP uses objectClasses
@@ -222,7 +225,10 @@
222
225
  #
223
226
  # :classes isn't the only optional argument. If :dnattr is left off, it defaults
224
227
  # to 'cn'. If :prefix is left off, it will default to 'ou=CLASSNAME'. In this
225
- # case, it would be 'ou=Group'.
228
+ # case, it would be 'ou=Group'. There is also a :parent_class option which, when
229
+ # specified, adds a method call parent() which will return the
230
+ # parent_class.new(parent_dn). The parent_dn is the objects dn without the dnattr
231
+ # pair.
226
232
  #
227
233
  # :classes should be an Array. :dnattr should be a String and so should :prefix.
228
234
  #
@@ -486,12 +492,20 @@
486
492
  # bind methods fail
487
493
  # * :try_sasl, when true, tells ActiveLDAP to attempt a SASL-GSSAPI bind
488
494
  # * :sasl_quiet, when true, tells the SASL libraries to not spew messages to STDOUT
495
+ # * :method indicates whether to use :ssl, :tls, or :plain
496
+ # * :retries - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs. -1 means infinite.
497
+ # * :retry_wait - seconds to wait before retrying a connection
498
+ # * :ldap_scope - dictates how to find objects. (Default: ONELEVEL)
499
+ # * :return_objects - indicates whether find/find_all will return objects or just the distinguished name attribute value of the matches. Rails users will find this useful.
500
+ # * :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned.
501
+ # * :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true
502
+ # See lib/configuration.rb for defaults for each option
489
503
  #
490
504
  # Base.connect both connects and binds in one step. It follows roughly the following approach:
491
505
  #
492
506
  # * Connect to host:port using :method
493
507
  #
494
- # * If user and password_block, attempt to bind with credentials.
508
+ # * If user and password_block/password, attempt to bind with credentials.
495
509
  # * If that fails or no password_block and anonymous allowed, attempt to bind
496
510
  # anonymously.
497
511
  # * If that fails, error out.
@@ -907,13 +921,18 @@ $VERBOSE, verbose = false, $VERBOSE
907
921
 
908
922
  require 'activeldap/ldap'
909
923
  require 'activeldap/schema2'
924
+ if RUBY_PLATFORM.match('linux')
925
+ require 'activeldap/timeout'
926
+ else
927
+ require 'activeldap/timeout_stub'
928
+ end
910
929
  require 'activeldap/base'
911
930
  require 'activeldap/associations'
912
931
  require 'activeldap/configuration'
913
932
 
914
933
 
915
934
  module ActiveLDAP
916
- VERSION = "0.7.1"
935
+ VERSION = "0.7.2"
917
936
  end
918
937
 
919
938
  ActiveLDAP::Base.class_eval do
@@ -11,28 +11,42 @@ module ActiveLDAP
11
11
  base.extend(ClassMethods)
12
12
  end
13
13
  module ClassMethods
14
+
14
15
  # This class function is used to setup all mappings between the subclass
15
16
  # and ldap for use in activeldap
17
+ #
18
+ # Example:
19
+ # ldap_mapping :dnattr => 'uid', :prefix => 'ou=People',
20
+ # :classes => ['top', 'posixAccount'], scope => LDAP::LDAP_SCOPE_SUBTREE,
21
+ # :parent => String
16
22
  def ldap_mapping(options = {})
17
23
  # The immediate ancestor should be the caller....
18
24
  klass = self.ancestors[0]
19
25
 
20
26
  dnattr = options[:dnattr] || 'cn'
21
27
  prefix = options[:prefix] || "ou=#{klass.to_s.split(':').last}"
22
- classes_array = options[:classes] || ['top']
28
+ classes_array = options[:classes] || nil
29
+ scope = options[:scope] || 'super'
30
+ # When used, instantiates parent objects from the "parent dn". This
31
+ # can be a String or a real ActiveLDAP class. This just adds the helper
32
+ # Base#parent.
33
+ parent = options[:parent_class] || nil
23
34
 
24
- raise TypeError, ":classes must be an array" \
25
- unless classes_array.respond_to? :size
26
- # Build classes array
27
- classes = '['
28
- classes_array.map! {|x| x = "'#{x}'"}
29
- classes << classes_array.join(', ')
30
- classes << ']'
35
+ classes = 'super'
36
+ unless classes_array.nil?
37
+ raise TypeError, ":classes must be an array" \
38
+ unless classes_array.respond_to? :size
39
+ # Build classes array
40
+ classes = '['
41
+ classes_array.map! {|x| x = "'#{x}'"}
42
+ classes << classes_array.join(', ')
43
+ classes << ']'
44
+ end
31
45
 
32
46
  # This adds the methods to the local
33
47
  # class which can then be inherited, etc
34
48
  # which describe the mapping to LDAP.
35
- klass.class_eval <<-"end_eval"
49
+ klass.class_eval(<<-"end_eval")
36
50
  class << self
37
51
  # Return the list of required object classes
38
52
  def required_classes
@@ -52,8 +66,13 @@ module ActiveLDAP
52
66
  def dnattr
53
67
  '#{dnattr}'
54
68
  end
55
- end
56
69
 
70
+ # Return the expected DN attribute of an object
71
+ def ldap_scope
72
+ #{scope}
73
+ end
74
+ end
75
+
57
76
  # Hide connect
58
77
  private_class_method :connect
59
78
 
@@ -63,6 +82,15 @@ module ActiveLDAP
63
82
  public_class_method :new
64
83
  public_class_method :dnattr
65
84
  end_eval
85
+
86
+ # Add the parent helper if desired
87
+ if parent
88
+ klass.class_eval(<<-"end_eval")
89
+ def parent()
90
+ return #{parent}.new(@dn.split(',')[1..-1].join(','))
91
+ end
92
+ end_eval
93
+ end
66
94
  end
67
95
 
68
96
  # belongs_to
@@ -40,7 +40,7 @@ module ActiveLDAP
40
40
  # If no exact match, raise an error.
41
41
  # If match, change all LDAP attributes in accessor attributes on the object.
42
42
  # -- these are ACTUALLY populated from schema - see subschema.rb example
43
- # -- @conn.schema().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
43
+ # -- @conn.schema2().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
44
44
  # -- extract objectClasses from match and populate
45
45
  # Multiple entries become lists.
46
46
  # If this isn't read-only then lists become multiple entries, etc.
@@ -94,6 +94,11 @@ module ActiveLDAP
94
94
  class AttributeAssignmentError < RuntimeError
95
95
  end
96
96
 
97
+ # TimeoutError
98
+ #
99
+ # An exception raised when a connection action fails due to a timeout
100
+ class TimeoutError < RuntimeError
101
+ end
97
102
 
98
103
  # Base
99
104
  #
@@ -196,6 +201,9 @@ module ActiveLDAP
196
201
  # :method - whether to use :ssl, :tls, or :plain (unencrypted)
197
202
  # :retry_wait - seconds to wait before retrying a connection
198
203
  # :ldap_scope - dictates how to find objects. ONELEVEL by default to avoid dn_attr collisions across OUs. Think before changing.
204
+ # :return_objects - indicates whether find/find_all will return objects or just the distinguished name attribute value of the matches
205
+ # :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned.
206
+ # :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true
199
207
  # See lib/configuration.rb for defaults for each option
200
208
  def Base.connect(config={})
201
209
  # Process config
@@ -203,10 +211,16 @@ module ActiveLDAP
203
211
  ## These will be replace by configuration.rb defaults if defined
204
212
  @@config = DEFAULT_CONFIG.dup
205
213
  config.keys.each do |key|
206
- if key == :base
214
+ case key
215
+ when :base
207
216
  # Scrub before inserting
208
217
  base = config[:base].gsub(/['}{#]/, '')
209
218
  Base.class_eval("def Base.base();'#{base}';end")
219
+ when :ldap_scope
220
+ if config[:ldap_scope].class != Fixnum
221
+ raise ConfigurationError, ':ldap_scope must be a Fixnum'
222
+ end
223
+ Base.class_eval("def Base.ldap_scope();#{config[:ldap_scope]};end")
210
224
  else
211
225
  @@config[key] = config[key]
212
226
  end
@@ -242,11 +256,79 @@ module ActiveLDAP
242
256
  end
243
257
  @@conn = nil
244
258
  # Make sure it is cleaned up
245
- ObjectSpace.garbage_collect
259
+ # This causes Ruby/LDAP memory corruption.
260
+ # ObjectSpace.garbage_collect
246
261
  end
247
262
 
248
263
  # Return the LDAP connection object currently in use
249
- def Base.connection
264
+ # Alternately execute a command against the connection
265
+ # object "safely" using a given block. Use the given
266
+ # "errmsg" for any error conditions.
267
+ def Base.connection(exc=RuntimeError.new('unknown error'), try_reconnect = true)
268
+ # Block was given! Let's safely provide access.
269
+ if block_given?
270
+ begin
271
+ Timeout.alarm(@@config[:timeout]) do
272
+ begin
273
+ yield @@conn
274
+ rescue => e
275
+ # Raise an LDAP error instead of RuntimeError or whatever
276
+
277
+ raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0
278
+ # Else reraise
279
+
280
+ raise e
281
+ end
282
+ end
283
+ rescue Timeout::Error => e
284
+ @@logger.error('Requested action timed out.')
285
+ retry if try_reconnect and @@config[:retry_on_timeout] and Base.reconnect()
286
+ message = e.message
287
+ message = exc.message unless exc.nil?
288
+ @@logger.error(message)
289
+ raise TimeoutError, message
290
+ rescue RuntimeError => e
291
+ @@logger.error("#{e.class} exception occurred in connection block")
292
+ @@logger.error("Exception message: #{e.message}")
293
+ @@logger.error("Exception backtrace: #{e.backtrace}")
294
+ @@logger.error(exc.message) unless exc.nil?
295
+ retry if try_reconnect and Base.reconnect()
296
+ raise exc unless exc.nil?
297
+ return nil
298
+ rescue LDAP::ServerDown => e
299
+ @@logger.error("#{e.class} exception occurred in connection block")
300
+ @@logger.error("Exception message: #{e.message}")
301
+ @@logger.error("Exception backtrace: #{e.backtrace}")
302
+ @@logger.error(exc.message) unless exc.nil?
303
+ retry if try_reconnect and Base.reconnect()
304
+ raise exc unless exc.nil?
305
+ return nil
306
+ rescue LDAP::ResultError => e
307
+ @@logger.error("#{e.class} exception occurred in connection block")
308
+ @@logger.error("Exception message: #{e.message}")
309
+ @@logger.error("Exception backtrace: #{e.backtrace}")
310
+ @@logger.error(exc.message) unless exc.nil?
311
+ retry if try_reconnect and Base.reconnect()
312
+ raise exc unless exc.nil?
313
+ return nil
314
+ rescue LDAP::UndefinedType => e
315
+ @@logger.error("#{e.class} exception occurred in connection block")
316
+ @@logger.error("Exception message: #{e.message}")
317
+ @@logger.error("Exception backtrace: #{e.backtrace}")
318
+ # Do not retry - not a connection error
319
+ raise exc unless exc.nil?
320
+ return nil
321
+ # Catch all - to be remedied later
322
+ rescue => e
323
+ @@logger.error("#{e.class} exception occurred in connection block")
324
+ @@logger.error("Exception message: #{e.message}")
325
+ @@logger.error("Exception backtrace: #{e.backtrace}")
326
+ @@logger.error("Error in catch all: please send debug log to ActiveLDAP author")
327
+ @@logger.error(exc.message) unless exc.nil?
328
+ raise exc unless exc.nil?
329
+ return nil
330
+ end
331
+ end
250
332
  return @@conn
251
333
  end
252
334
 
@@ -255,24 +337,36 @@ module ActiveLDAP
255
337
  @@conn = conn
256
338
  end
257
339
 
340
+ # Determine if we have exceed the retry limit or not.
341
+ # True is reconnecting is allowed - False if not.
342
+ def Base.can_reconnect?
343
+ # Allow connect if we've never connected.
344
+ return true unless @@config
345
+ if @@reconnect_attempts < (@@config[:retries] - 1) or
346
+ @@config[:retries] < 0
347
+ return true
348
+ end
349
+ return false
350
+ end
351
+
258
352
  # Attempts to reconnect up to the number of times allowed
259
353
  # If forced, try once then fail with ConnectionError if not connected.
260
354
  def Base.reconnect(force=false)
355
+ unless @@config
356
+ @@logger.error('Ignoring force: Base.reconnect called before Base.connect') if force
357
+
358
+ Base.connect
359
+ return true
360
+ end
261
361
  not_connected = true
262
362
  while not_connected
263
- # Just to be clean, unbind if possible
264
- begin
265
- @@conn.unbind() if not @@conn.nil? and @@conn.bound?
266
- rescue
267
- # Ignore complaints.
268
- end
269
- @@conn = nil
270
-
271
- if @@config[:retries] == -1 or force == true
363
+ if Base.can_reconnect?
272
364
 
365
+ Base.close()
273
366
 
274
367
  # Reset the attempts if this was forced.
275
- @@reconnect_attempts = 0 if @@reconnect_attempts != 0
368
+ @@reconnect_attempts = 0 if force
369
+ @@reconnect_attempts += 1 if @@config[:retries] >= 0
276
370
  begin
277
371
  do_connect()
278
372
  not_connected = false
@@ -282,18 +376,6 @@ module ActiveLDAP
282
376
  # Do not loop if forced
283
377
  raise ConnectionError, detail.message if force
284
378
  end
285
- elsif @@reconnect_attempts < @@config[:retries]
286
-
287
- @@reconnect_attempts += 1
288
- begin
289
- do_connect()
290
- not_connected = false
291
- # Reset to 0 if a connection was made.
292
- @@reconnect_attempts = 0
293
- rescue => detail
294
- @@logger.error("Reconnect to server failed: #{detail.exception}")
295
- @@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}")
296
- end
297
379
  else
298
380
  # Raise a warning
299
381
  raise ConnectionError, 'Giving up trying to reconnect to LDAP server.'
@@ -315,13 +397,7 @@ module ActiveLDAP
315
397
  # Wraps Ruby/LDAP connection.search to make it easier to search for specific
316
398
  # data without cracking open Base.connection
317
399
  def Base.search(config={})
318
- unless Base.connection
319
- if @@config
320
- ActiveLDAP::Base.connect(@@config)
321
- else
322
- ActiveLDAP::Base.connect
323
- end
324
- end
400
+ Base.reconnect if Base.connection.nil? and Base.can_reconnect?
325
401
 
326
402
  config[:filter] = 'objectClass=*' unless config.has_key? :filter
327
403
  config[:attrs] = [] unless config.has_key? :attrs
@@ -331,8 +407,8 @@ module ActiveLDAP
331
407
  values = []
332
408
  config[:attrs] = config[:attrs].to_a # just in case
333
409
 
334
- begin
335
- @@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
410
+ result = Base.connection() do |conn|
411
+ conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
336
412
  res = {}
337
413
  res['dn'] = [m.dn.dup] # For consistency with the below
338
414
  m.attrs.each do |attr|
@@ -342,18 +418,10 @@ module ActiveLDAP
342
418
  end
343
419
  values.push(res)
344
420
  end
345
- rescue RuntimeError => detail
346
- #TODO# Check for 'No message' when retrying
347
- # The connection may have gone stale. Let's reconnect and retry.
348
- retry if Base.reconnect()
421
+ end
422
+ if result.nil?
349
423
  # Do nothing on failure
350
424
 
351
- rescue => detail
352
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
353
-
354
- retry if Base.reconnect()
355
- end
356
- raise detail
357
425
  end
358
426
  return values
359
427
  end
@@ -365,14 +433,8 @@ module ActiveLDAP
365
433
  # usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
366
434
  # Subclass.find('some*val')
367
435
  #
368
- def Base.find(config = {})
369
- unless Base.connection
370
- if @@config
371
- ActiveLDAP::Base.connect(@@config)
372
- else
373
- ActiveLDAP::Base.connect
374
- end
375
- end
436
+ def Base.find(config='*')
437
+ Base.reconnect if Base.connection.nil? and Base.can_reconnect?
376
438
 
377
439
  if self.class == Class
378
440
  klass = self.ancestors[0].to_s.split(':').last
@@ -384,20 +446,18 @@ module ActiveLDAP
384
446
 
385
447
  # Allow a single string argument
386
448
  attr = dnattr()
387
- objects = false
449
+ objects = @@config[:return_objects]
388
450
  val = config
389
451
  # Or a hash
390
- if config.respond_to?"has_key?"
452
+ if config.respond_to?(:has_key?)
391
453
  attr = config[:attribute] || dnattr()
392
454
  val = config[:value] || '*'
393
- objects = config[:objects]
455
+ objects = config[:objects] || @@config[:return_objects]
394
456
  end
395
457
 
396
- matches = []
397
-
398
- begin
458
+ Base.connection(ConnectionError.new("Failed in #{self.class}#find(#{config.inspect})")) do |conn|
399
459
  # Get some attributes
400
- @@conn.search(base(), @@config[:ldap_scope], "(#{attr}=#{val})") do |m|
460
+ conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m|
401
461
  # Extract the dnattr value
402
462
  dnval = m.dn.split(/,/)[0].split(/=/)[1]
403
463
 
@@ -407,20 +467,8 @@ module ActiveLDAP
407
467
  return dnval
408
468
  end
409
469
  end
410
- rescue RuntimeError => detail
411
- #TODO# Check for 'No message' when retrying
412
- # The connection may have gone stale. Let's reconnect and retry.
413
- retry if Base.reconnect()
414
-
415
- # Do nothing on failure
416
-
417
- rescue => detail
418
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
419
-
420
- retry if Base.reconnect()
421
- end
422
- raise detail
423
470
  end
471
+ # If we're here, there were no results
424
472
  return nil
425
473
  end
426
474
  private_class_method :find
@@ -429,14 +477,8 @@ module ActiveLDAP
429
477
  #
430
478
  # Finds all matches for value where |value| is the value of some
431
479
  # |field|, or the wildcard match. This is only useful for derived classes.
432
- def Base.find_all(config = {})
433
- unless Base.connection
434
- if @@config
435
- ActiveLDAP::Base.connect(@@config)
436
- else
437
- ActiveLDAP::Base.connect
438
- end
439
- end
480
+ def Base.find_all(config='*')
481
+ Base.reconnect if Base.connection.nil? and Base.can_reconnect?
440
482
 
441
483
  if self.class == Class
442
484
  real_klass = self.ancestors[0]
@@ -447,20 +489,18 @@ module ActiveLDAP
447
489
  # Allow a single string argument
448
490
  val = config
449
491
  attr = dnattr()
450
- objects = false
492
+ objects = @@config[:return_objects]
451
493
  # Or a hash
452
- if config.respond_to?"has_key?"
494
+ if config.respond_to?(:has_key?)
453
495
  val = config[:value] || '*'
454
496
  attr = config[:attribute] || dnattr()
455
- objects = config[:objects]
497
+ objects = config[:objects] || @@config[:return_objects]
456
498
  end
457
499
 
458
500
  matches = []
459
-
460
- begin
501
+ Base.connection(ConnectionError.new("Failed in #{self.class}#find_all(#{config.inspect})")) do |conn|
461
502
  # Get some attributes
462
- @@conn.search(base(), @@config[:ldap_scope],
463
- "(#{attr}=#{val})") do |m|
503
+ conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m|
464
504
  # Extract the dnattr value
465
505
  dnval = m.dn.split(/,/)[0].split(/=/)[1]
466
506
 
@@ -470,19 +510,6 @@ module ActiveLDAP
470
510
  matches.push(dnval)
471
511
  end
472
512
  end
473
- rescue RuntimeError => detail
474
- #TODO# Check for 'No message' when retrying
475
- # The connection may have gone stale. Let's reconnect and retry.
476
- retry if Base.reconnect()
477
-
478
- # Do nothing on failure
479
-
480
- rescue => detail
481
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
482
-
483
- retry if Base.reconnect()
484
- end
485
- raise detail
486
513
  end
487
514
  return matches
488
515
  end
@@ -499,7 +526,7 @@ module ActiveLDAP
499
526
  # configuration.rb into this class.
500
527
  # When subclassing, the specified prefix will be concatenated.
501
528
  def Base.base
502
- 'dc=example,dc=com'
529
+ 'dc=localdomain'
503
530
  end
504
531
 
505
532
  # Base.dnattr
@@ -526,6 +553,17 @@ module ActiveLDAP
526
553
  []
527
554
  end
528
555
 
556
+ # Base.ldap_scope
557
+ #
558
+ # This method when included into Base provides
559
+ # an inheritable, overwritable configuration setting
560
+ #
561
+ # This value should be the default LDAP scope behavior
562
+ # desired.
563
+ def Base.ldap_scope
564
+ LDAP::LDAP_SCOPE_ONELEVEL
565
+ end
566
+
529
567
 
530
568
 
531
569
  ### All instance methods, etc
@@ -539,20 +577,12 @@ module ActiveLDAP
539
577
  # val can be a dn attribute value, a full DN, or a LDAP::Entry. The use
540
578
  # with a LDAP::Entry is primarily meant for internal use by find and
541
579
  # find_all.
580
+ #
542
581
  def initialize(val)
543
582
  @exists = false
544
- # Try a default connection if none made explicitly
545
- if Base.connection.nil? and @@reconnect_attempts < @@config[:retries]
546
- # Use @@config if it has been prepopulated and the conn is down.
547
- if @@config
548
- ActiveLDAP::Base.reconnect
549
- else
550
- ActiveLDAP::Base.connect
551
- end
552
- elsif Base.connection.nil?
553
- @@logger.error('Attempted to initialize a new object with no connection')
554
- raise ConnectionError, 'Number of reconnect attempts exceeded.'
555
- end
583
+ # Make sure we're connected
584
+ Base.reconnect if Base.connection.nil? and Base.can_reconnect?
585
+
556
586
  if val.class == LDAP::Entry
557
587
  # Call import, which is basically initialize
558
588
  # without accessing LDAP.
@@ -569,11 +599,11 @@ module ActiveLDAP
569
599
  @attr_methods = {} # list of valid method calls for attributes used for dereferencing
570
600
  @last_oc = false # for use in other methods for "caching"
571
601
  if dnattr().empty?
572
- raise RuntimeError, "dnattr() not set for this class."
602
+ raise ConfigurationError, "dnattr() not set for this class: #{self.class}"
573
603
  end
574
604
 
575
- # Break val apart if it is a dn
576
- if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i)
605
+ # Extract dnattr if val looks like a dn
606
+ if val.match(/^#{dnattr()}=([^,=]+),/i)
577
607
  val = $1
578
608
  elsif val.match(/[=,]/)
579
609
  @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN."
@@ -589,9 +619,9 @@ module ActiveLDAP
589
619
  @dn = "#{dnattr()}=#{val},#{base()}"
590
620
 
591
621
  # Search for the existing entry
592
- begin
622
+ Base.connection(ConnectionError.new("Failed in #{self.class}#new(#{val.inspect})")) do |conn|
593
623
  # Get some attributes
594
- Base.connection.search(base(), @@config[:ldap_scope], "(#{dnattr()}=#{val})") do |m|
624
+ conn.search(base(), ldap_scope(), "(#{dnattr()}=#{val})") do |m|
595
625
  @exists = true
596
626
  # Save DN
597
627
  @dn = m.dn
@@ -612,20 +642,22 @@ module ActiveLDAP
612
642
  end
613
643
  end
614
644
  end
615
- rescue RuntimeError => detail
616
- #TODO# Check for 'No message' when retrying
617
- # The connection may have gone stale. Let's reconnect and retry.
618
- retry if Base.reconnect()
619
-
620
- # Do nothing on failure
621
- @@logger.error('new: unable to search for entry')
622
- raise detail
623
- rescue LDAP::ResultError
624
645
  end
625
646
  end
626
647
 
627
648
  # Do the actual object setup work.
628
649
  if @exists
650
+ # Make sure the server uses objectClass and not objectclass
651
+ unless @ldap_data.has_key?('objectClass')
652
+ real_objc = @ldap_data.grep(/^objectclass$/i)
653
+ if real_objc.size == 1
654
+ @ldap_data['objectClass'] = @ldap_data[real_objc]
655
+ @ldap_data.delete(real_objc)
656
+ else
657
+ raise AttributeEmpty, 'objectClass was not sent by LDAP server!'
658
+ end
659
+ end
660
+
629
661
  # Populate schema data
630
662
  send(:apply_objectclass, @ldap_data['objectClass'])
631
663
 
@@ -633,6 +665,10 @@ module ActiveLDAP
633
665
  @ldap_data.each do |pair|
634
666
  real_attr = @attr_methods[pair[0]]
635
667
 
668
+ if real_attr.nil?
669
+ @@logger.error("Unable to resolve attribute value #{pair[0].inspect}. " +
670
+ "Unpredictable behavior likely!")
671
+ end
636
672
  @data[real_attr] = pair[1].dup
637
673
 
638
674
  end
@@ -656,7 +692,7 @@ module ActiveLDAP
656
692
  def attributes
657
693
 
658
694
  send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
659
- return @attr_methods.keys
695
+ return @attr_methods.keys.map {|x|x.downcase}.uniq
660
696
  end
661
697
 
662
698
  # exists?
@@ -706,7 +742,8 @@ module ActiveLDAP
706
742
  # Make sure all MUST attributes have a value
707
743
  @data['objectClass'].each do |objc|
708
744
  @must.each do |req_attr|
709
- deref = @attr_methods[req_attr]
745
+ # Downcase to ensure we catch schema problems
746
+ deref = @attr_methods[req_attr.downcase]
710
747
  # Set default if it wasn't yet set.
711
748
  @data[deref] = [] if @data[deref].nil?
712
749
  # Check for missing requirements.
@@ -724,20 +761,10 @@ module ActiveLDAP
724
761
  # Delete this entry from LDAP
725
762
  def delete
726
763
 
727
- begin
728
- @@conn.delete(@dn)
764
+ Base.connection(DeleteError.new(
765
+ "Failed to delete LDAP entry: '#{@dn}'")) do |conn|
766
+ conn.delete(@dn)
729
767
  @exists = false
730
- rescue RuntimeError => detail
731
- #todo# check for 'no message' when retrying
732
- # the connection may have gone stale. let's reconnect and retry.
733
- retry if Base.reconnect()
734
- raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
735
- rescue LDAP::ResultError => detail
736
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
737
-
738
- retry if Base.reconnect()
739
- end
740
- raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
741
768
  end
742
769
  end
743
770
 
@@ -784,6 +811,15 @@ module ActiveLDAP
784
811
 
785
812
  data = Marshal.load(Marshal.dump(@data))
786
813
 
814
+
815
+
816
+ bad_attrs = @data.keys - (@must+@may)
817
+ bad_attrs.each do |removeme|
818
+ data.delete(removeme)
819
+ end
820
+
821
+
822
+
787
823
 
788
824
  data.keys.each do |key|
789
825
  data[key].each do |value|
@@ -800,7 +836,6 @@ module ActiveLDAP
800
836
  end
801
837
 
802
838
 
803
-
804
839
  if @exists
805
840
  # Cycle through all attrs to determine action
806
841
  action = {}
@@ -865,23 +900,11 @@ module ActiveLDAP
865
900
  end
866
901
  end
867
902
 
868
- begin
903
+ Base.connection(WriteError.new(
904
+ "Failed to modify: '#{entry}'")) do |conn|
869
905
 
870
- @@conn.modify(@dn, entry)
906
+ conn.modify(@dn, entry)
871
907
 
872
- rescue RuntimeError => detail
873
- #todo# check for SERVER_DOWN
874
- # the connection may have gone stale. let's reconnect and retry.
875
- retry if Base.reconnect()
876
- raise WriteError, "Could not update LDAP entry: #{detail}"
877
- rescue => detail
878
-
879
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
880
-
881
- retry if Base.reconnect()
882
- end
883
-
884
- raise WriteError, "Could not update LDAP entry: #{detail}"
885
908
  end
886
909
  else # add everything!
887
910
 
@@ -903,30 +926,26 @@ module ActiveLDAP
903
926
  entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
904
927
  end
905
928
  end
906
- begin
929
+ Base.connection(WriteError.new(
930
+ "Failed to add: '#{entry}'")) do |conn|
907
931
 
908
- @@conn.add(@dn, entry)
932
+ conn.add(@dn, entry)
909
933
 
910
934
  @exists = true
911
- rescue RuntimeError => detail
912
- # The connection may have gone stale. Let's reconnect and retry.
913
- retry if Base.reconnect()
914
- raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
915
- rescue LDAP::ResultError => detail
916
- if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown
917
-
918
- retry if Base.reconnect()
919
- end
920
- raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
921
935
  end
922
936
  end
923
937
 
924
- @ldap_data = Marshal.load(Marshal.dump(@data))
938
+ @ldap_data = Marshal.load(Marshal.dump(data))
939
+ # Delete items disallowed by objectclasses.
940
+ # They should have been removed from ldap.
941
+
942
+ bad_attrs.each do |removeme|
943
+ @ldap_data.delete(removeme)
944
+ end
925
945
 
926
946
 
927
947
  end
928
948
 
929
-
930
949
  # method_missing
931
950
  #
932
951
  # If a given method matches an attribute or an attribute alias
@@ -970,7 +989,7 @@ module ActiveLDAP
970
989
  end
971
990
 
972
991
 
973
- private
992
+ private
974
993
 
975
994
  # import(LDAP::Entry)
976
995
  #
@@ -1072,14 +1091,6 @@ module ActiveLDAP
1072
1091
  # Update attr_method with appropriate
1073
1092
  define_attribute_methods(attr)
1074
1093
  end
1075
-
1076
- # Delete all now innew_ocid attributes given the new objectClasses
1077
- @data.keys.each do |key|
1078
- # If it's not a proper aliased attribute, drop it
1079
- unless @attr_methods.has_key? key
1080
- @data.delete(key)
1081
- end
1082
- end
1083
1094
  end
1084
1095
 
1085
1096
 
@@ -1198,21 +1209,18 @@ module ActiveLDAP
1198
1209
  end
1199
1210
 
1200
1211
  # Enforce LDAPv3
1201
- @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
1212
+ Base.connection(nil, false) do |conn|
1213
+ conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
1214
+ end
1202
1215
 
1203
1216
  # Authenticate
1204
1217
  do_bind
1205
1218
 
1206
1219
  # Retrieve the schema. We need this to automagically determine attributes
1207
- begin
1208
- @@schema = @@conn.schema() if @@schema.nil?
1209
- rescue => e
1210
- @@logger.error("Failed to retrieve the schema (#{@@config[:method]})")
1211
- @@logger.error("Schema failure exception: #{e.exception}")
1212
- @@logger.error("Schema failure backtrace: #{e.backtrace}")
1213
- raise ConnectionError, "#{e.exception} - LDAP connection failure, or server does not support schema queries."
1220
+ exc = ConnectionError.new("Unable to retrieve schema from server (#{@@config[:method]})")
1221
+ Base.connection(exc, false) do |conn|
1222
+ @@schema = @@conn.schema2() if @@schema.nil?
1214
1223
  end
1215
-
1216
1224
 
1217
1225
  end
1218
1226
 
@@ -1228,39 +1236,35 @@ module ActiveLDAP
1228
1236
  elsif do_simple_bind(bind_dn)
1229
1237
  @@logger.info('Bound simple')
1230
1238
  elsif @@config[:allow_anonymous] and do_anonymous_bind(bind_dn)
1231
- @@logger.info('Bound simple')
1239
+ @@logger.info('Bound anonymous')
1232
1240
  else
1233
- @@logger.error('Failed to bind using any available method')
1234
1241
  raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0
1242
+ raise AuthenticationError, 'All authentication methods exhausted.'
1235
1243
  end
1236
1244
 
1237
1245
  return @@conn.bound?
1238
1246
  end
1247
+ private_class_method :do_bind
1239
1248
 
1240
1249
  # Base.do_anonymous_bind
1241
1250
  #
1242
1251
  # Bind to LDAP with the given DN, but with no password. (anonymous!)
1243
1252
  def Base.do_anonymous_bind(bind_dn)
1244
1253
  @@logger.info "Attempting anonymous authentication"
1245
- begin
1246
- @@conn.bind()
1254
+ Base.connection(nil, false) do |conn|
1255
+ conn.bind()
1247
1256
  return true
1248
- rescue => e
1249
-
1250
-
1251
-
1252
- @@logger.warn "Warning: Anonymous authentication failed."
1253
- @@logger.warn "message: #{e.message}"
1254
- return false
1255
1257
  end
1258
+ return false
1256
1259
  end
1260
+ private_class_method :do_anonymous_bind
1257
1261
 
1258
1262
  # Base.do_simple_bind
1259
1263
  #
1260
1264
  # Bind to LDAP with the given DN and password
1261
1265
  def Base.do_simple_bind(bind_dn)
1262
1266
  # Bail if we have no password or password block
1263
- if not @@config[:password_block].nil? and not @@config[:password].nil?
1267
+ if @@config[:password_block].nil? and @@config[:password].nil?
1264
1268
  return false
1265
1269
  end
1266
1270
 
@@ -1282,50 +1286,46 @@ module ActiveLDAP
1282
1286
  return false
1283
1287
  end
1284
1288
 
1285
- begin
1286
- @@conn.bind(bind_dn, password)
1287
- rescue => e
1288
-
1289
- # TODO: replace this with LDAP::err2exception()
1290
- if @@conn.err == LDAP::LDAP_SERVER_DOWN
1291
- @@logger.error "Warning: " + e.message
1292
- else
1293
- @@logger.warn "Warning: SIMPLE authentication failed."
1294
- end
1295
- return false
1296
- end
1297
1289
  # Store the password for quick reference later
1298
1290
  if @@config[:store_password]
1299
1291
  @@config[:password] = password
1300
1292
  elsif @@config[:store_password] == false
1301
1293
  @@config[:password] = nil
1302
1294
  end
1303
- return true
1295
+
1296
+ Base.connection(nil, false) do |conn|
1297
+ conn.bind(bind_dn, password)
1298
+ return true
1299
+ end
1300
+ return false
1304
1301
  end
1302
+ private_class_method :do_simple_bind
1305
1303
 
1306
1304
  # Base.do_sasl_bind
1307
1305
  #
1308
1306
  # Bind to LDAP with the given DN using any available SASL methods
1309
1307
  def Base.do_sasl_bind(bind_dn)
1310
1308
  # Get all SASL mechanisms
1311
- mechanisms = @@conn.root_dse[0]['supportedSASLMechanisms']
1309
+ #
1310
+ mechanisms = []
1311
+ exc = ConnectionError.new('Root DSE query failed')
1312
+ Base.connection(exc, false) do |conn|
1313
+ mechanisms = conn.root_dse[0]['supportedSASLMechanisms']
1314
+ end
1312
1315
  # Use GSSAPI if available
1313
1316
  # Currently only GSSAPI is supported with Ruby/LDAP from
1314
1317
  # http://caliban.org/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
1315
1318
  # TODO: Investigate further SASL support
1316
1319
  if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
1317
- begin
1318
- @@conn.sasl_quiet = @@config[:sasl_quiet] if @@config.has_key?(:sasl_quiet)
1319
- @@conn.sasl_bind(bind_dn, 'GSSAPI')
1320
+ Base.connection(nil, false) do |conn|
1321
+ conn.sasl_quiet = @@config[:sasl_quiet] if @@config.has_key?(:sasl_quiet)
1322
+ conn.sasl_bind(bind_dn, 'GSSAPI')
1320
1323
  return true
1321
- rescue
1322
-
1323
- @@logger.warn "Warning: SASL GSSAPI authentication failed."
1324
- return false
1325
1324
  end
1326
1325
  end
1327
1326
  return false
1328
1327
  end
1328
+ private_class_method :do_sasl_bind
1329
1329
 
1330
1330
  # base
1331
1331
  #
@@ -1336,6 +1336,15 @@ module ActiveLDAP
1336
1336
  self.class.base
1337
1337
  end
1338
1338
 
1339
+ # ldap_scope
1340
+ #
1341
+ # Returns the value of self.class.ldap_scope
1342
+ # This is just syntactic sugar
1343
+ def ldap_scope
1344
+
1345
+ self.class.ldap_scope
1346
+ end
1347
+
1339
1348
  # required_classes
1340
1349
  #
1341
1350
  # Returns the value of self.class.required_classes
@@ -1419,6 +1428,8 @@ module ActiveLDAP
1419
1428
  aliases.each do |ali|
1420
1429
 
1421
1430
  @attr_methods[ali] = attr
1431
+
1432
+ @attr_methods[ali.downcase] = attr
1422
1433
  end
1423
1434
 
1424
1435
  end
@@ -22,10 +22,28 @@ module ActiveLDAP
22
22
 
23
23
  DEFAULT_CONFIG[:retries] = 3
24
24
  DEFAULT_CONFIG[:retry_wait] = 3
25
+ DEFAULT_CONFIG[:timeout] = 0 # in seconds; 0 <= Never timeout
26
+ # Whether or not to retry on timeouts
27
+ DEFAULT_CONFIG[:retry_on_timeout] = true
28
+
29
+ # Whether to return objects by default from find/find_all
30
+ DEFAULT_CONFIG[:return_objects] = false
25
31
 
26
32
  DEFAULT_CONFIG[:logger] = nil
27
- DEFAULT_CONFIG[:ldap_scope] = LDAP::LDAP_SCOPE_ONELEVEL
28
33
 
34
+ # On connect, this is overriden by the :base argument
35
+ #
36
+ # Set this to LDAP_SCOPE_SUBTREE if you have a LDAP tree where all
37
+ # objects of the same class living in different parts of the same subtree, but
38
+ # not. LDAP_SCOPE_ONELEVEL is for use when all the objects in your classes live
39
+ # under one shared level (e.g. ou=People,dc=localdomain)
40
+ #
41
+ # This can be overriden on a per class basis in ldap_mapping :scope
42
+ def Base.ldap_scope
43
+ LDAP::LDAP_SCOPE_ONELEVEL
44
+ end
45
+
46
+ # On connect, this is overriden by the :base argument
29
47
  # Make the return value the string that is your LDAP base
30
48
  def Base.base
31
49
  'dc=localdomain'
@@ -6,6 +6,9 @@
6
6
 
7
7
 
8
8
  module LDAP
9
+ class PrettyError < RuntimeError
10
+ end
11
+
9
12
  ERRORS = [
10
13
  "LDAP_SUCCESS",
11
14
  "LDAP_OPERATIONS_ERROR",
@@ -75,7 +78,7 @@ module LDAP
75
78
  exc = exc.split('_').collect {|w| w.capitalize }.join('')
76
79
  # Doesn't exist :-)
77
80
  LDAP.module_eval(<<-end_module_eval)
78
- class #{exc} < LDAP::ResultError
81
+ class #{exc} < LDAP::PrettyError
79
82
  end
80
83
  end_module_eval
81
84
  hash[val] = exc
@@ -103,3 +106,6 @@ module LDAP
103
106
  return [exc, err2string(errno)]
104
107
  end
105
108
  end
109
+
110
+ # Generate!
111
+ LDAP::generate_err2exceptions()
@@ -18,6 +18,8 @@ module LDAP
18
18
  return [] if type.empty?
19
19
  return [] if at.empty?
20
20
 
21
+ type = type.downcase # We're going case insensitive.
22
+
21
23
  # Check already parsed options first
22
24
  if @@attr_cache.has_key? sub \
23
25
  and @@attr_cache[sub].has_key? type \
@@ -38,9 +40,13 @@ module LDAP
38
40
  self[sub].each do |s|
39
41
  line = ''
40
42
  if type[0..0] =~ /[0-9]/
41
- line = s if s =~ /\(\s+#{type}\s+(?:[A-Z]|\))/
43
+ if s =~ /\(\s+(?i:#{type})\s+(?:[A-Z]|\))/
44
+ line = s
45
+ end
42
46
  else
43
- line = s if s =~ /NAME\s+\(?.*'#{type}'.*\)?\s+(?:[A-Z]|\))/
47
+ if s =~ /NAME\s+\(?.*'(?i:#{type})'.*\)?\s+(?:[A-Z]|\))/
48
+ line = s
49
+ end
44
50
  end
45
51
 
46
52
  # I need to check, but I think some of these matchs
@@ -190,7 +196,7 @@ module LDAP
190
196
  end # Schema2
191
197
 
192
198
  class Conn
193
- def schema(base = nil, attrs = nil, sec = 0, usec = 0)
199
+ def schema2(base = nil, attrs = nil, sec = 0, usec = 0)
194
200
  attrs ||= [
195
201
  'objectClasses',
196
202
  'attributeTypes',
@@ -0,0 +1,75 @@
1
+ require 'timeout'
2
+
3
+ module Timeout
4
+
5
+ # A forking timeout implementation that relies on
6
+ # signals to interrupt blocking I/O instead of passing
7
+ # that code to run in a separate process.
8
+ #
9
+ # A process is fork()ed, sleeps for _sec_,
10
+ # then sends a ALRM signal to the Process.ppid
11
+ # process. ALRM is used to avoid conflicts with sleep()
12
+ #
13
+ # This overwrites any signal
14
+ def Timeout.alarm(sec, exception=Timeout::Error, &block)
15
+ return block.call if sec == nil or sec.zero?
16
+
17
+
18
+ # Trap an alarm in case it comes before we're ready
19
+ orig_alrm = trap(:ALRM, 'IGNORE')
20
+
21
+ # Setup a fallback in case of a race condition of an
22
+ # alarm before we set the other trap
23
+ trap(:ALRM) do
24
+ # Don't leave zombies
25
+ Process.wait2()
26
+ # Restore the original handler
27
+ trap('ALRM', orig_alrm)
28
+ # Now raise an exception!
29
+ raise exception, 'execution expired'
30
+ end
31
+
32
+ # Spawn the sleeper
33
+ pid = Process.fork {
34
+ begin
35
+ # Sleep x seconds then send SIGALRM
36
+ sleep(sec)
37
+ # Send alarm!
38
+ Process.kill(:ALRM, Process.ppid)
39
+ end
40
+ exit! 0
41
+ }
42
+
43
+ # Setup the real handler
44
+ trap(:ALRM) do
45
+ # Make sure we clean up any zombies
46
+ Process.waitpid(pid)
47
+ # Restore the original handler
48
+ trap(:ALRM, orig_alrm)
49
+ # Now raise an exception!
50
+ raise exception, 'execution expired'
51
+ end
52
+
53
+ begin
54
+ # Run the code!
55
+ return block.call
56
+ ensure
57
+ # Restore old alarm handler since we're done
58
+ trap(:ALRM, orig_alrm)
59
+ # Make sure the process is dead
60
+ # This may be run twice (trap occurs during execution) so ignore ESRCH
61
+ Process.kill(:TERM, pid) rescue Errno::ESRCH
62
+ # Don't leave zombies
63
+ Process.waitpid(pid) rescue Errno::ECHILD
64
+ end
65
+ end
66
+ end # Timeout
67
+
68
+ if __FILE__ == $0
69
+ require 'time'
70
+ Timeout.alarm(2) do
71
+ loop do
72
+ p Time.now
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,17 @@
1
+ require 'timeout'
2
+
3
+ module Timeout
4
+ # STUB
5
+ def Timeout.alarm(sec, exception=Timeout::Error, &block)
6
+ return block.call
7
+ end
8
+ end # Timeout
9
+
10
+ if __FILE__ == $0
11
+ require 'time'
12
+ Timeout.alarm(2) do
13
+ loop do
14
+ p Time.now
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: ruby-activeldap
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.7.1
7
- date: 2006-05-04 00:00:00 +01:00
6
+ version: 0.7.2
7
+ date: 2006-05-22 00:00:00 +01:00
8
8
  summary: Ruby/ActiveLDAP is a object-oriented API to LDAP
9
9
  require_paths:
10
10
  - lib
@@ -35,6 +35,8 @@ files:
35
35
  - lib/activeldap/configuration.rb
36
36
  - lib/activeldap/ldap.rb
37
37
  - lib/activeldap/schema2.rb
38
+ - lib/activeldap/timeout.rb
39
+ - lib/activeldap/timeout_stub.rb
38
40
  test_files: []
39
41
 
40
42
  rdoc_options: []