treequel 1.2.2 → 1.3.0pre384

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