treequel 1.2.2 → 1.3.0pre384

Sign up to get free protection for your applications and to get access to all the features.
data/lib/treequel.rb CHANGED
@@ -25,7 +25,7 @@ end
25
25
 
26
26
  # A library for interacting with LDAP modelled after Sequel.
27
27
  #
28
- # @version 1.2.2
28
+ # @version 1.3.0
29
29
  #
30
30
  # @example
31
31
  # # Connect to the directory at the specified URL
@@ -53,10 +53,10 @@ end
53
53
  module Treequel
54
54
 
55
55
  # Library version
56
- VERSION = '1.2.2'
56
+ VERSION = '1.3.0'
57
57
 
58
58
  # VCS revision
59
- REVISION = %q$Revision: 0c2883d2074a $
59
+ REVISION = %q$Revision: 554028334395 $
60
60
 
61
61
  # Common paths for ldap.conf
62
62
  COMMON_LDAP_CONF_PATHS = %w[
@@ -80,6 +80,16 @@ module Treequel
80
80
 
81
81
 
82
82
  ### Logging
83
+ # Log levels
84
+ LOG_LEVELS = {
85
+ 'debug' => Logger::DEBUG,
86
+ 'info' => Logger::INFO,
87
+ 'warn' => Logger::WARN,
88
+ 'error' => Logger::ERROR,
89
+ 'fatal' => Logger::FATAL,
90
+ }.freeze
91
+ LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
92
+
83
93
  @default_logger = Logger.new( $stderr )
84
94
  @default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
85
95
 
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rspec'
4
+
5
+ require 'treequel'
6
+ require 'treequel/control'
7
+
8
+
9
+ # This is a shared behavior for specs which different Treequel::Controls share in
10
+ # common. If you're creating a Treequel::Control implementation, you can test
11
+ # its conformity to the expectations placed on them by adding this to your spec:
12
+ #
13
+ # require 'treequel/behavior/control'
14
+ #
15
+ # describe YourControl do
16
+ #
17
+ # it_should_behave_like "A Treequel::Control"
18
+ #
19
+ # end
20
+
21
+ shared_examples_for "A Treequel::Control" do
22
+
23
+ let( :control ) do
24
+ described_class
25
+ end
26
+
27
+
28
+ it "implements one of either #get_client_controls or #get_server_controls" do
29
+ methods = [
30
+ 'get_client_controls', # 1.8.x
31
+ 'get_server_controls',
32
+ :get_client_controls, # 1.9.x
33
+ :get_server_controls
34
+ ]
35
+ (control.instance_methods( false ) | methods).should_not be_empty()
36
+ end
37
+
38
+ end
39
+
40
+
@@ -38,11 +38,17 @@ class Treequel::Branch
38
38
  # [Boolean] Whether or not to include operational attributes by default.
39
39
  @include_operational_attrs = false
40
40
 
41
+ # [Boolean] Whether or not to freeze values cached in @values. This helps
42
+ # prevent you from accidentally doing branch[:attr] << 'value', which
43
+ # modifies the cached values, but not the entry.
44
+ @freeze_converted_values = true
45
+
41
46
  # Whether or not to include operational attributes when fetching the
42
47
  # entry for branches.
43
48
  class << self
44
49
  extend Treequel::AttributeDeclarations
45
50
  predicate_attr :include_operational_attrs
51
+ predicate_attr :freeze_converted_values
46
52
  end
47
53
 
48
54
 
@@ -54,8 +60,12 @@ class Treequel::Branch
54
60
  ### @return [Treequel::Branch] The new branch object.
55
61
  def self::new_from_entry( entry, directory )
56
62
  entry = Treequel::HashUtilities.stringify_keys( entry )
57
- Treequel.logger.debug "Creating Branch from entry: %p in directory: %p" % [ entry, directory ]
58
- return self.new( directory, entry['dn'].first, entry )
63
+ dnvals = entry.delete( 'dn' ) or
64
+ raise ArgumentError, "no 'dn' attribute for entry"
65
+
66
+ Treequel.logger.debug "Creating Branch from entry: %p in directory: %s" %
67
+ [ dnvals.first, directory ]
68
+ return self.new( directory, dnvals.first, entry )
59
69
  end
60
70
 
61
71
 
@@ -72,6 +82,7 @@ class Treequel::Branch
72
82
  ### @param [String] dn The DN of the entry the Branch is wrapping.
73
83
  ### @param [LDAP::Entry, Hash] entry The entry object if it's already been fetched.
74
84
  def initialize( directory, dn, entry=nil )
85
+ raise ArgumentError, "nil DN" unless dn
75
86
  raise ArgumentError, "invalid DN" unless
76
87
  dn.match( Patterns::DISTINGUISHED_NAME ) || dn.empty?
77
88
  raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
@@ -93,7 +104,7 @@ class Treequel::Branch
93
104
  ######
94
105
 
95
106
  # Delegate some other methods to a new Branchset via the #branchset method
96
- def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order
107
+ def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order, :as
97
108
 
98
109
  # Delegate some methods to the Branch's directory via its accessor
99
110
  def_method_delegators :directory, :controls, :referrals
@@ -204,7 +215,8 @@ class Treequel::Branch
204
215
  ### Return the Branch's immediate parent node.
205
216
  ### @return [Treequel::Branch]
206
217
  def parent
207
- return self.class.new( self.directory, self.parent_dn )
218
+ pardn = self.parent_dn or return nil
219
+ return self.class.new( self.directory, pardn )
208
220
  end
209
221
 
210
222
 
@@ -267,7 +279,7 @@ class Treequel::Branch
267
279
  self.log.debug " making LDIF from an entry: %p" % [ entry ]
268
280
 
269
281
  entry.keys.reject {|k| k == 'dn' }.each do |attribute|
270
- entry[ attribute ].each do |val|
282
+ Array( entry[attribute] ).each do |val|
271
283
  ldif << ldif_for_attr( attribute, val, width )
272
284
  end
273
285
  end
@@ -299,14 +311,16 @@ class Treequel::Branch
299
311
  def []( attrname )
300
312
  attrsym = attrname.to_sym
301
313
 
302
- unless @values.key?( attrsym )
314
+ if @values.key?( attrsym )
315
+ self.log.debug " value for %p is cached (%p)." % [ attrname, @values[attrsym] ]
316
+ else
303
317
  self.log.debug " value for %p is NOT cached." % [ attrsym ]
304
318
  value = self.get_converted_object( attrsym )
305
319
  self.log.debug " converted value is: %p" % [ value ]
306
- value.freeze if value.respond_to?( :freeze )
320
+ value.freeze if
321
+ self.class.freeze_converted_values? &&
322
+ value.respond_to?( :freeze )
307
323
  @values[ attrsym ] = value
308
- else
309
- self.log.debug " value for %p is cached." % [ attrname ]
310
324
  end
311
325
 
312
326
  return @values[ attrsym ]
@@ -366,23 +380,33 @@ class Treequel::Branch
366
380
  ###
367
381
  ### @return [TrueClass] if the delete succeeded
368
382
  def delete( *attributes )
383
+
384
+ # If no attributes are given, delete the whole entry
369
385
  if attributes.empty?
370
386
  self.log.info "No attributes specified; deleting entire entry for %s" % [ self.dn ]
371
387
  self.directory.delete( self )
388
+
389
+ # Otherwise, gather up the LDAP::Mod objects that will delete the given attributes
372
390
  else
373
391
  self.log.debug "Deleting attributes: %p" % [ attributes ]
374
392
  mods = attributes.flatten.collect do |attribute|
393
+
394
+ # Delete particular values of the attribute
375
395
  if attribute.is_a?( Hash )
376
396
  attribute.collect do |key,vals|
377
- vals = Array( vals ).collect {|val| val.to_s }
397
+ vals = [ vals ] unless vals.is_a?( Array )
398
+ vals.collect! {|val| self.get_converted_attribute(key, val) }
378
399
  LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
379
400
  end
401
+
402
+ # Delete all values of the attribute
380
403
  else
381
404
  LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
382
405
  end
383
- end.flatten
384
406
 
385
- self.directory.modify( self, mods )
407
+ end
408
+
409
+ self.directory.modify( self, mods.flatten )
386
410
  end
387
411
 
388
412
  self.clear_caches
@@ -394,9 +418,10 @@ class Treequel::Branch
394
418
  ### Create the entry for this Branch with the specified +attributes+. The +attributes+ should,
395
419
  ### at a minimum, contain the pair `:objectClass => :someStructuralObjectClass`.
396
420
  ###
397
- ### @param [Hash<Symbol,String => Object>] attributes
421
+ ### @see Treequel::Directory#create
398
422
  def create( attributes={} )
399
423
  self.directory.create( self, attributes )
424
+ self.clear_caches
400
425
  return self
401
426
  end
402
427
 
@@ -432,7 +457,10 @@ class Treequel::Branch
432
457
  ### @param [Hash<String, Symbol => Object>] attributes
433
458
  def move( rdn )
434
459
  self.log.debug "Asking the directory to move me to an entry called %p" % [ rdn ]
435
- return self.directory.move( self, rdn )
460
+ self.directory.move( self, rdn )
461
+ self.clear_caches
462
+
463
+ return self
436
464
  end
437
465
 
438
466
 
@@ -493,9 +521,8 @@ class Treequel::Branch
493
521
  schema = self.directory.schema
494
522
 
495
523
  oc_oids = self[:objectClass] || []
496
- self.log.debug " objectClass OIDs are: %p" % [ oc_oids ]
497
524
  oc_oids |= additional_classes.collect {|str| str.to_sym }
498
- oc_oids << :top if oc_oids.empty?
525
+ oc_oids << 'top' if oc_oids.empty?
499
526
 
500
527
  oclasses = []
501
528
  oc_oids.each do |oid|
@@ -504,7 +531,7 @@ class Treequel::Branch
504
531
  oclasses << oc
505
532
  end
506
533
 
507
- self.log.debug " found %d objectClasses: %p" % [ oclasses.length, oclasses ]
534
+ self.log.debug " found %d objectClasses: %p" % [ oclasses.length, oclasses.map(&:name) ]
508
535
  return oclasses.uniq
509
536
  end
510
537
 
@@ -539,7 +566,7 @@ class Treequel::Branch
539
566
  [ oclasses.map(&:name) ]
540
567
 
541
568
  oclasses.each do |oc|
542
- self.log.debug " adding %p from %p" % [ oc.must, oc ]
569
+ self.log.debug " adding %p from %p" % [ oc.must.map(&:name), oc.name ]
543
570
  types |= oc.must
544
571
  end
545
572
 
@@ -571,10 +598,10 @@ class Treequel::Branch
571
598
  attrhash = {}
572
599
 
573
600
  self.must_attribute_types( *additional_object_classes ).each do |attrtype|
574
- self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype ]
601
+ # self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype.name ]
575
602
 
576
603
  if attrtype.name == :objectClass
577
- attrhash[ :objectClass ] = [:top] | additional_object_classes
604
+ attrhash[ :objectClass ] = ['top'] | additional_object_classes
578
605
  elsif attrtype.single?
579
606
  attrhash[ attrtype.name ] = ''
580
607
  else
@@ -625,7 +652,7 @@ class Treequel::Branch
625
652
  attrhash = {}
626
653
 
627
654
  self.may_attribute_types( *additional_object_classes ).each do |attrtype|
628
- self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype ]
655
+ # self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype.named ]
629
656
 
630
657
  if attrtype.single?
631
658
  attrhash[ attrtype.name ] = nil
@@ -772,15 +799,16 @@ class Treequel::Branch
772
799
  ### Get the value associated with +attrsym+, convert it to a Ruby object if the Branch's
773
800
  ### directory has a conversion rule, and return it.
774
801
  def get_converted_object( attrsym )
775
- return nil unless self.entry
776
- value = self.entry[ attrsym.to_s ] or return nil
802
+ value = self.entry ? self.entry[ attrsym.to_s ] : nil
777
803
 
778
804
  if attribute = self.directory.schema.attribute_types[ attrsym ]
805
+ syntax_oid = attribute.syntax.oid
806
+
779
807
  if attribute.single?
780
- value = self.directory.convert_to_object( attribute.syntax.oid, value.first )
808
+ value = self.directory.convert_to_object( syntax_oid, value.first ) if value
781
809
  else
782
- value = value.collect do |raw|
783
- self.directory.convert_to_object( attribute.syntax.oid, raw )
810
+ value = Array( value ).collect do |raw|
811
+ self.directory.convert_to_object( syntax_oid, raw )
784
812
  end
785
813
  end
786
814
  else
@@ -795,8 +823,8 @@ class Treequel::Branch
795
823
  ### and return it.
796
824
  def get_converted_attribute( attrsym, object )
797
825
  if attribute = self.directory.schema.attribute_types[ attrsym ]
798
- self.log.debug "converting %p object to a %p attribute" %
799
- [ attrsym, attribute.syntax.desc ]
826
+ self.log.debug "converting %p object (a %p) to a %p attribute" %
827
+ [ attrsym, object.class, attribute.syntax.desc ]
800
828
  return self.directory.convert_to_attribute( attribute.syntax.oid, object )
801
829
  else
802
830
  self.log.info "no attributeType for %p" % [ attrsym ]
@@ -77,9 +77,17 @@ class Treequel::Branchset
77
77
  def initialize( branch, options={} )
78
78
  @branch = branch
79
79
  @options = DEFAULT_OPTIONS.merge( options )
80
+ self.log.debug "Setting up %p for branch %p with options: %p" %
81
+ [ self.class, @branch, @options ]
80
82
 
81
- self.extend( *@branch.directory.registered_controls ) unless
82
- @branch.directory.registered_controls.empty?
83
+ if @branch.directory.registered_controls.empty?
84
+ self.log.debug " no registered controls."
85
+ else
86
+ @branch.directory.registered_controls.each do |control|
87
+ self.log.debug " extending with %p" % [ control ]
88
+ self.extend( control )
89
+ end
90
+ end
83
91
 
84
92
  super()
85
93
  end
@@ -47,7 +47,7 @@ module Treequel::PagedResultsControl
47
47
 
48
48
  ### Add the control's instance variables to including Branchsets.
49
49
  def initialize
50
- @paged_results_cookie = ''
50
+ @paged_results_cookie = nil
51
51
  @paged_results_setsize = nil
52
52
  end
53
53
 
@@ -75,8 +75,10 @@ module Treequel::PagedResultsControl
75
75
  newset = self.clone
76
76
 
77
77
  if setsize.nil? || setsize.zero?
78
+ self.log.debug "Removing paged results control."
78
79
  newset.paged_results_setsize = nil
79
80
  else
81
+ self.log.debug "Adding paged results control with page size = %d." % [ setsize ]
80
82
  newset.paged_results_setsize = setsize
81
83
  end
82
84
 
@@ -96,7 +98,7 @@ module Treequel::PagedResultsControl
96
98
  ### Remove any paging control associated with the receiving Branchset.
97
99
  ### @return [void]
98
100
  def without_paging!
99
- self.paged_results_cookie = ''
101
+ self.paged_results_cookie = nil
100
102
  self.paged_results_setsize = nil
101
103
  end
102
104
 
@@ -99,7 +99,7 @@ class Treequel::Directory
99
99
 
100
100
 
101
101
  #################################################################
102
- ### C L A S S M E T H O D S
102
+ ### I N S T A N C E M E T H O D S
103
103
  #################################################################
104
104
 
105
105
  ### Create a new Treequel::Directory with the given +options+. Options is a hash with one
@@ -132,14 +132,14 @@ class Treequel::Directory
132
132
  @conn = nil
133
133
  @bound_user = nil
134
134
 
135
- @base_dn = options[:base_dn] || self.get_default_base_dn
136
-
137
- @base = nil
138
135
 
139
136
  @object_conversions = DEFAULT_OBJECT_CONVERSIONS.dup
140
137
  @attribute_conversions = DEFAULT_ATTRIBUTE_CONVERSIONS.dup
141
138
  @registered_controls = []
142
139
 
140
+ @base_dn = options[:base_dn] || self.get_default_base_dn
141
+ @base = nil
142
+
143
143
  # Immediately bind if credentials are passed to the initializer.
144
144
  if ( options[:bind_dn] && options[:pass] )
145
145
  self.bind( options[:bind_dn], options[:pass] )
@@ -155,7 +155,7 @@ class Treequel::Directory
155
155
  def_method_delegators :base, *DELEGATED_BRANCH_METHODS
156
156
 
157
157
  # Delegate some methods to the connection via the #conn method
158
- def_method_delegators :conn, :controls, :referrals, :root_dse
158
+ def_method_delegators :conn, :controls, :referrals
159
159
 
160
160
 
161
161
  # The host to connect to.
@@ -187,6 +187,12 @@ class Treequel::Directory
187
187
  attr_reader :bound_user
188
188
 
189
189
 
190
+ ### Fetch the root DSE as a Treequel::Branch.
191
+ def root_dse
192
+ return self.search( '', :base, '(objectClass=*)', :selectattrs => ['+'] ).first
193
+ end
194
+
195
+
190
196
  ### Fetch the Branch for the base node of the directory.
191
197
  ### @return [Treequel::Branch]
192
198
  def base
@@ -231,6 +237,23 @@ class Treequel::Directory
231
237
  end
232
238
 
233
239
 
240
+ ### Drop the existing connection and establish a new one.
241
+ ### @return [Boolean] +true+ if the connection was re-established
242
+ ### @raise [RuntimeError] if the re-connection failed
243
+ def reconnect
244
+ self.log.info "Reconnecting to %s..." % [ self.uri ]
245
+ @conn = self.connect
246
+ self.log.info "...reconnected."
247
+
248
+ return true
249
+ rescue LDAP::ResultError => err
250
+ self.log.error "%s while attempting to reconnect to %s: %s" %
251
+ [ err.class.name, self.uri, err.message ]
252
+ raise "Couldn't reconnect to %s: %s: %s" %
253
+ [ self.uri, err.class.name, err.message ]
254
+ end
255
+
256
+
234
257
  ### Return the URI object that corresponds to the directory.
235
258
  ### @return [URI::LDAP]
236
259
  def uri
@@ -457,7 +480,7 @@ class Treequel::Directory
457
480
  self.log.debug "Modifying %s with LDAP mod objects: %p" % [ branch.dn, mods ]
458
481
  self.conn.modify( branch.dn, mods )
459
482
  else
460
- normattrs = self.normalize_attributes( mods )
483
+ normattrs = normalize_attributes( mods )
461
484
  self.log.debug "Modifying %s with: %p" % [ branch.dn, normattrs ]
462
485
  self.conn.modify( branch.dn, normattrs )
463
486
  end
@@ -472,28 +495,13 @@ class Treequel::Directory
472
495
 
473
496
 
474
497
  ### Create the entry for the given +branch+, setting its attributes to +newattrs+.
498
+ ### @param [Treequel::Branch, #to_s] branch the branch to create (or a DN string)
499
+ ### @param [Hash, Array<LDAP::Mod>] newattrs the attributes to create the entry with. This
500
+ ### can be either a Hash of attributes, or an Array of
501
+ ### LDAP::Mod objects.
475
502
  def create( branch, newattrs={} )
476
- newdn = branch.dn
477
- schema = self.schema
478
-
479
- # Merge RDN attributes with existing ones, combining any that exist in both
480
- self.log.debug "Smushing rdn attributes %p into %p" % [ branch.rdn_attributes, newdn ]
481
- newattrs.merge!( branch.rdn_attributes ) do |key, *values|
482
- values.flatten.uniq
483
- end
484
-
485
- normattrs = self.normalize_attributes( newattrs )
486
- raise ArgumentError, "Can't create an entry with no objectClasses" unless
487
- normattrs.key?( 'objectClass' )
488
- normattrs['objectClass'].each do |oc|
489
- raise ArgumentError, "No such objectClass #{oc.inspect}" unless
490
- schema.object_classes.key?(oc.to_sym)
491
- end
492
- raise ArgumentError, "Can't create an entry with no structural objectClass" unless
493
- normattrs['objectClass'].any? {|oc| schema.object_classes[oc.to_sym].structural? }
494
-
495
- self.log.debug "Creating an entry at %s with the attributes: %p" % [ newdn, normattrs ]
496
- self.conn.add( newdn, normattrs )
503
+ newattrs = normalize_attributes( newattrs ) if newattrs.is_a?( Hash )
504
+ self.conn.add( branch.to_s, newattrs )
497
505
 
498
506
  return true
499
507
  end
@@ -565,6 +573,7 @@ class Treequel::Directory
565
573
  end
566
574
  end
567
575
  end
576
+ alias_method :register_control, :register_controls
568
577
 
569
578
 
570
579
  ### Map the specified LDAP +attribute+ to its Ruby datatype if one is registered for the given
@@ -605,7 +614,7 @@ class Treequel::Directory
605
614
  ### Return an Array of OID strings representing the controls supported by the Directory,
606
615
  ### as listed in the directory's root DSE.
607
616
  def supported_control_oids
608
- return self.conn.root_dse.first['supportedControl']
617
+ return self.root_dse[:supportedControl]
609
618
  end
610
619
 
611
620
 
@@ -620,7 +629,7 @@ class Treequel::Directory
620
629
  ### Return an Array of OID strings representing the extensions supported by the Directory,
621
630
  ### as listed in the directory's root DSE.
622
631
  def supported_extension_oids
623
- return self.conn.root_dse.first['supportedExtension']
632
+ return self.root_dse[:supportedExtension]
624
633
  end
625
634
 
626
635
 
@@ -635,7 +644,7 @@ class Treequel::Directory
635
644
  ### Return an Array of OID strings representing the features supported by the Directory,
636
645
  ### as listed in the directory's root DSE.
637
646
  def supported_feature_oids
638
- return self.conn.root_dse.first['supportedFeatures']
647
+ return self.root_dse[:supportedFeatures]
639
648
  end
640
649
 
641
650
 
@@ -675,9 +684,7 @@ class Treequel::Directory
675
684
 
676
685
  ### Fetch the default base dn for the server from the server's Root DSE.
677
686
  def get_default_base_dn
678
- dse = self.root_dse
679
- return '' if dse.nil? || dse.empty?
680
- return dse.first['namingContexts'].first
687
+ return self.root_dse[:namingContexts].first.dn
681
688
  end
682
689
 
683
690
 
@@ -694,23 +701,6 @@ class Treequel::Directory
694
701
  end
695
702
 
696
703
 
697
- ### Normalize the attributes in +hash+ to be of the form expected by the
698
- ### LDAP library (i.e., keys as Strings, values as Arrays of Strings)
699
- def normalize_attributes( hash )
700
- normhash = {}
701
- hash.each do |key,val|
702
- val = [ val ] unless val.is_a?( Array )
703
- val.collect! {|obj| obj.to_s }
704
-
705
- normhash[ key.to_s ] = val
706
- end
707
-
708
- normhash.delete( 'dn' )
709
-
710
- return normhash
711
- end
712
-
713
-
714
704
  ### Normalize the parameters to the #search method into the format expected by
715
705
  ### the LDAP::Conn#Search_ext2 method and return them as a Hash.
716
706
  def normalize_search_parameters( base, scope, filter, parameters )