ruby-activeldap 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []