treequel 1.0.4 → 1.1.0

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.
Files changed (53) hide show
  1. data/ChangeLog +130 -1
  2. data/Rakefile +8 -3
  3. data/Rakefile.local +2 -0
  4. data/bin/treeirb +6 -2
  5. data/bin/treequel +5 -4
  6. data/lib/treequel/branch.rb +133 -72
  7. data/lib/treequel/branchcollection.rb +16 -5
  8. data/lib/treequel/branchset.rb +37 -6
  9. data/lib/treequel/constants.rb +12 -0
  10. data/lib/treequel/directory.rb +96 -41
  11. data/lib/treequel/filter.rb +42 -1
  12. data/lib/treequel/model/objectclass.rb +111 -0
  13. data/lib/treequel/model.rb +363 -0
  14. data/lib/treequel/monkeypatches.rb +65 -0
  15. data/lib/treequel/schema/attributetype.rb +108 -18
  16. data/lib/treequel/schema/ldapsyntax.rb +15 -0
  17. data/lib/treequel/schema/matchingrule.rb +24 -0
  18. data/lib/treequel/schema/matchingruleuse.rb +24 -0
  19. data/lib/treequel/schema/objectclass.rb +70 -5
  20. data/lib/treequel/schema/table.rb +5 -15
  21. data/lib/treequel/schema.rb +64 -1
  22. data/lib/treequel.rb +5 -5
  23. data/rake/documentation.rb +27 -0
  24. data/rake/hg.rb +14 -2
  25. data/rake/manual.rb +1 -1
  26. data/spec/lib/constants.rb +9 -7
  27. data/spec/lib/control_behavior.rb +1 -0
  28. data/spec/lib/matchers.rb +1 -0
  29. data/spec/treequel/branch_spec.rb +229 -20
  30. data/spec/treequel/branchcollection_spec.rb +73 -39
  31. data/spec/treequel/branchset_spec.rb +59 -8
  32. data/spec/treequel/control_spec.rb +1 -0
  33. data/spec/treequel/controls/contentsync_spec.rb +1 -0
  34. data/spec/treequel/controls/pagedresults_spec.rb +1 -0
  35. data/spec/treequel/controls/sortedresults_spec.rb +1 -0
  36. data/spec/treequel/directory_spec.rb +46 -10
  37. data/spec/treequel/filter_spec.rb +28 -5
  38. data/spec/treequel/mixins_spec.rb +7 -14
  39. data/spec/treequel/model/objectclass_spec.rb +330 -0
  40. data/spec/treequel/model_spec.rb +433 -0
  41. data/spec/treequel/monkeypatches_spec.rb +118 -0
  42. data/spec/treequel/schema/attributetype_spec.rb +116 -0
  43. data/spec/treequel/schema/ldapsyntax_spec.rb +8 -0
  44. data/spec/treequel/schema/matchingrule_spec.rb +6 -1
  45. data/spec/treequel/schema/matchingruleuse_spec.rb +5 -0
  46. data/spec/treequel/schema/objectclass_spec.rb +41 -3
  47. data/spec/treequel/schema/table_spec.rb +31 -18
  48. data/spec/treequel/schema_spec.rb +13 -16
  49. data/spec/treequel_spec.rb +22 -0
  50. data.tar.gz.sig +1 -0
  51. metadata +40 -7
  52. metadata.gz.sig +0 -0
  53. data/spec/treequel/utils_spec.rb +0 -49
@@ -9,6 +9,7 @@ require 'treequel'
9
9
  require 'treequel/schema'
10
10
  require 'treequel/mixins'
11
11
  require 'treequel/constants'
12
+ require 'treequel/branch'
12
13
 
13
14
 
14
15
  # The object in Treequel that represents a connection to a directory, the
@@ -27,17 +28,33 @@ class Treequel::Directory
27
28
  :connect_type => :tls,
28
29
  :base_dn => nil,
29
30
  :bind_dn => nil,
30
- :pass => nil
31
+ :pass => nil,
32
+ :results_class => Treequel::Branch,
31
33
  }
32
34
 
33
- # Default mapping of SYNTAX OIDs to conversions. See #add_syntax_mapping for more
34
- # information on what a valid conversion is.
35
- DEFAULT_SYNTAX_MAPPING = {
36
- OIDS::BIT_STRING_SYNTAX => lambda {|bs| bs[0..-1].to_i(2) },
37
- OIDS::BOOLEAN_SYNTAX => { 'TRUE' => true, 'FALSE' => false },
38
- OIDS::GENERALIZED_TIME_SYNTAX => lambda {|string| Time.parse(string) },
39
- OIDS::UTC_TIME_SYNTAX => lambda {|string| Time.parse(string) },
40
- OIDS::INTEGER_SYNTAX => lambda {|string| Integer(string) },
35
+ # Default mapping of SYNTAX OIDs to conversions from an LDAP string.
36
+ # See #add_attribute_conversions for more information on what a valid conversion is.
37
+ DEFAULT_ATTRIBUTE_CONVERSIONS = {
38
+ OIDS::BIT_STRING_SYNTAX => lambda {|bs, _| bs[0..-1].to_i(2) },
39
+ OIDS::BOOLEAN_SYNTAX => { 'TRUE' => true, 'FALSE' => false },
40
+ OIDS::GENERALIZED_TIME_SYNTAX => lambda {|string, _| Time.parse(string) },
41
+ OIDS::UTC_TIME_SYNTAX => lambda {|string, _| Time.parse(string) },
42
+ OIDS::INTEGER_SYNTAX => lambda {|string, _| Integer(string) },
43
+ OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|dn, directory|
44
+ resclass = directory.results_class
45
+ resclass.new( directory, dn )
46
+ },
47
+ }
48
+
49
+ # Default mapping of SYNTAX OIDs to conversions to an LDAP string from a Ruby object.
50
+ # See #add_object_conversion for more information on what a valid conversion is.
51
+ DEFAULT_OBJECT_CONVERSIONS = {
52
+ OIDS::BIT_STRING_SYNTAX => lambda {|bs, _| bs.to_i.to_s(2) },
53
+ OIDS::BOOLEAN_SYNTAX => lambda {|obj, _| obj ? 'TRUE' : 'FALSE' },
54
+ OIDS::GENERALIZED_TIME_SYNTAX => lambda {|time, _| time.ldap_generalized },
55
+ OIDS::UTC_TIME_SYNTAX => lambda {|time, _| time.ldap_utc },
56
+ OIDS::INTEGER_SYNTAX => lambda {|obj, _| Integer(obj).to_s },
57
+ OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|obj, _| obj.dn },
41
58
  }
42
59
 
43
60
  # :NOTE: the docs for #search_ext2 lie. The method signature is actually:
@@ -102,22 +119,26 @@ class Treequel::Directory
102
119
  ### The DN of the user to bind as; if unset, binds anonymously.
103
120
  ### @option options [String] :pass (nil)
104
121
  ### The password to use when binding.
122
+ ### @option options [CLass] :results_class (Treequel::Branch)
123
+ ### The class to instantiate by default for entries fetched from the Directory.
105
124
  def initialize( options={} )
106
- options = DEFAULT_OPTIONS.merge( options )
125
+ options = DEFAULT_OPTIONS.merge( options )
107
126
 
108
- @host = options[:host]
109
- @port = options[:port]
110
- @connect_type = options[:connect_type]
127
+ @host = options[:host]
128
+ @port = options[:port]
129
+ @connect_type = options[:connect_type]
130
+ @results_class = options[:results_class]
111
131
 
112
- @conn = nil
113
- @bound_user = nil
132
+ @conn = nil
133
+ @bound_user = nil
114
134
 
115
- @base_dn = options[:base_dn] || self.get_default_base_dn
135
+ @base_dn = options[:base_dn] || self.get_default_base_dn
116
136
 
117
- @base = nil
137
+ @base = nil
118
138
 
119
- @syntax_mapping = DEFAULT_SYNTAX_MAPPING.dup
120
- @registered_controls = []
139
+ @object_conversions = DEFAULT_OBJECT_CONVERSIONS.dup
140
+ @attribute_conversions = DEFAULT_ATTRIBUTE_CONVERSIONS.dup
141
+ @registered_controls = []
121
142
 
122
143
  # Immediately bind if credentials are passed to the initializer.
123
144
  if ( options[:bind_dn] && options[:pass] )
@@ -134,7 +155,7 @@ class Treequel::Directory
134
155
  def_method_delegators :base, *DELEGATED_BRANCH_METHODS
135
156
 
136
157
  # Delegate some methods to the connection via the #conn method
137
- def_method_delegators :conn, :controls, :referrals
158
+ def_method_delegators :conn, :controls, :referrals, :root_dse
138
159
 
139
160
 
140
161
  # The host to connect to.
@@ -149,6 +170,10 @@ class Treequel::Directory
149
170
  # @return [Symbol]
150
171
  attr_accessor :connect_type
151
172
 
173
+ # The Class to instantiate when wrapping results fetched from the Directory.
174
+ # @return [Class]
175
+ attr_accessor :results_class
176
+
152
177
  # The base DN of the directory
153
178
  # @return [String]
154
179
  attr_accessor :base_dn
@@ -165,7 +190,7 @@ class Treequel::Directory
165
190
  ### Fetch the Branch for the base node of the directory.
166
191
  ### @return [Treequel::Branch]
167
192
  def base
168
- return @base ||= Treequel::Branch.new( self, self.base_dn )
193
+ return @base ||= self.results_class.new( self, self.base_dn )
169
194
  end
170
195
 
171
196
 
@@ -330,8 +355,8 @@ class Treequel::Directory
330
355
  ###
331
356
  ### @option options [Class] :results_class (Treequel::Branch)
332
357
  ### The Class to use when wrapping results; if not specified, defaults to the class
333
- ### of +base+ if it responds to #new_from_entry, or Treequel::Branch
334
- ### if it does not.
358
+ ### of +base+ if it responds to #new_from_entry, or the directory object's
359
+ ### #results_class if it does not.
335
360
  ### @option options [Array<String, Symbol>] :selectattrs (['*'])
336
361
  ### The attributes to return from the search; defaults to '*', which means to
337
362
  ### return all non-operational attributes. Specifying '+' will cause the search
@@ -372,7 +397,9 @@ class Treequel::Directory
372
397
  if options.key?( :results_class )
373
398
  collectclass = options.delete( :results_class )
374
399
  else
375
- collectclass = base.class.respond_to?( :new_from_entry ) ? base.class : Treequel::Branch
400
+ collectclass = base.class.respond_to?( :new_from_entry ) ?
401
+ base.class :
402
+ self.results_class
376
403
  end
377
404
 
378
405
  # Format the arguments in the way #search_ext2 expects them
@@ -395,7 +422,6 @@ class Treequel::Directory
395
422
  ]]
396
423
 
397
424
  results = []
398
-
399
425
  self.conn.search_ext2( base_dn, scope, filter, *parameters ).each do |entry|
400
426
  branch = collectclass.new_from_entry( entry, self )
401
427
  branch.include_operational_attrs = base.include_operational_attrs? if
@@ -495,25 +521,41 @@ class Treequel::Directory
495
521
  end
496
522
 
497
523
 
524
+ ### Add +conversion+ mapping for attributes of specified +oid+ to a Ruby object. A
525
+ ### conversion is any object that responds to #[] with a String
526
+ ### argument(e.g., Proc, Method, Hash); the argument is the raw value String returned
527
+ ### from the LDAP entry, and it should return the converted value. Adding a mapping
528
+ ### with a nil +conversion+ effectively clears it.
529
+ ### @see #convert_to_object
530
+ def add_attribute_conversion( oid, conversion=nil )
531
+ conversion = Proc.new if block_given?
532
+ @attribute_conversions[ oid ] = conversion
533
+ end
534
+
535
+
498
536
  ### Add +conversion+ mapping for the specified +oid+. A conversion is any object that
499
- ### responds to #[] with a String argument(e.g., Proc, Method, Hash); the argument is
500
- ### the raw value String returned from the LDAP entry, and it should return the
501
- ### converted value. Adding a mapping with a nil +conversion+ effectively clears it.
502
- def add_syntax_mapping( oid, conversion=nil )
537
+ ### responds to #[] with an object argument(e.g., Proc, Method, Hash); the argument is
538
+ ### the Ruby object that's being set as a value in an LDAP entry, and it should return the
539
+ ### raw LDAP string. Adding a mapping with a nil +conversion+ effectively clears it.
540
+ ### @see #convert_to_attribute
541
+ def add_object_conversion( oid, conversion=nil )
503
542
  conversion = Proc.new if block_given?
504
- @syntax_mapping[ oid ] = conversion
543
+ @object_conversions[ oid ] = conversion
505
544
  end
506
545
 
507
546
 
508
547
  ### Register the specified +modules+
509
548
  def register_controls( *modules )
510
- dse = self.conn.root_dse.first
511
- supported_controls = dse['supportedControl']
549
+ supported_controls = self.supported_control_oids
550
+ self.log.debug "Got %d supported controls: %p" %
551
+ [ supported_controls.length, supported_controls ]
512
552
 
513
553
  modules.each do |mod|
514
554
  oid = mod.const_get( :OID ) if mod.const_defined?( :OID )
515
555
  raise NotImplementedError, "%s doesn't define an OID" % [ mod.name ] if oid.nil?
516
556
 
557
+ self.log.debug "Checking for directory support for %p (%s)" % [ mod, oid ]
558
+
517
559
  if supported_controls.include?( oid )
518
560
  @registered_controls << mod
519
561
  else
@@ -524,17 +566,30 @@ class Treequel::Directory
524
566
  end
525
567
 
526
568
 
527
- ### Map the specified +value+ to its Ruby datatype if one is registered for the given
569
+ ### Map the specified LDAP +attribute+ to its Ruby datatype if one is registered for the given
528
570
  ### syntax +oid+. If there is no conversion registered, just return the +value+ as-is.
529
- def convert_syntax_value( oid, value )
530
- self.log.debug "Converting value %p using the syntax rule for %p" % [ value, oid ]
531
- unless conversion = @syntax_mapping[ oid ]
532
- self.log.debug " ...no conversion, returning it as-is."
533
- return value
571
+ def convert_to_object( oid, attribute )
572
+ return attribute unless conversion = @attribute_conversions[ oid ]
573
+
574
+ if conversion.respond_to?( :call )
575
+ return conversion.call( attribute, self )
576
+ else
577
+ return conversion[ attribute ]
534
578
  end
579
+ end
535
580
 
536
- self.log.debug " ...found conversion: %p" % [ conversion ]
537
- return conversion[ value ]
581
+
582
+ ### Map the specified Ruby +object+ to its LDAP string equivalent if a conversion is
583
+ ### registered for the given syntax +oid+. If there is no conversion registered, just
584
+ ### returns the +value+ as a String (via #to_s).
585
+ def convert_to_attribute( oid, object )
586
+ return object.to_s unless conversion = @object_conversions[ oid ]
587
+
588
+ if conversion.respond_to?( :call )
589
+ return conversion.call( object, self )
590
+ else
591
+ return conversion[ object ]
592
+ end
538
593
  end
539
594
 
540
595
 
@@ -619,7 +674,7 @@ class Treequel::Directory
619
674
 
620
675
  ### Fetch the default base dn for the server from the server's Root DSE.
621
676
  def get_default_base_dn
622
- dse = self.conn.root_dse
677
+ dse = self.root_dse
623
678
  return '' if dse.nil? || dse.empty?
624
679
  return dse.first['namingContexts'].first
625
680
  end
@@ -62,6 +62,15 @@ class Treequel::Filter
62
62
  return self.filters.collect {|f| f.to_s }.join
63
63
  end
64
64
 
65
+
66
+ ### Append operator: add the +other+ filter to the list.
67
+ ### @param [Treequel::Filter] other the new filter to add
68
+ ### @return [Treequel::Filter::FilterList] self (for chaining)
69
+ def <<( other )
70
+ @filters << other
71
+ return self
72
+ end
73
+
65
74
  end # class FilterList
66
75
 
67
76
 
@@ -140,6 +149,12 @@ class Treequel::Filter
140
149
  return '&' + @filterlist.to_s
141
150
  end
142
151
 
152
+ ### Add an additional filter to the list of requirements
153
+ ### @param [Treequel::Filter] filter the new requirement
154
+ def add_requirement( filter )
155
+ @filterlist << filter
156
+ end
157
+
143
158
  end # AndComponent
144
159
 
145
160
 
@@ -160,6 +175,12 @@ class Treequel::Filter
160
175
  return '|' + @filterlist.to_s
161
176
  end
162
177
 
178
+ ### Add an additional filter to the list of alternatives
179
+ ### @param [Treequel::Filter] filter the new alternative
180
+ def add_alternation( filter )
181
+ @filterlist << filter
182
+ end
183
+
163
184
  end # class OrComponent
164
185
 
165
186
 
@@ -215,8 +236,10 @@ class Treequel::Filter
215
236
  self.log.debug "creating a new %s %s for %p and %p" %
216
237
  [ filtertype, self.class.name, attribute, value ]
217
238
 
218
- filtertype = filtertype.to_s.downcase.to_sym
239
+ # Handle Sequel :attribute.identifier
240
+ attribute = attribute.value if attribute.respond_to?( :value )
219
241
 
242
+ filtertype = filtertype.to_s.downcase.to_sym
220
243
  if FILTERTYPE_OP.key?( filtertype )
221
244
  # no-op
222
245
  elsif FILTEROP_NAMES.key?( filtertype.to_s )
@@ -688,6 +711,24 @@ class Treequel::Filter
688
711
  alias_method :+, :&
689
712
 
690
713
 
714
+ ### OR two filters together
715
+ ### @param [Treequel::Filter] other_filter
716
+ def |( other_filter )
717
+ return other_filter if self.promiscuous?
718
+ return self.dup if other_filter.promiscuous?
719
+
720
+ # Collapse nested ORs into a single one with an additional alternation
721
+ # if possible.
722
+ if self.component.respond_to?( :add_alternation )
723
+ self.log.debug "collapsing nested ORs..."
724
+ newcomp = self.component.dup
725
+ newcomp.add_alternation( other_filter )
726
+ return self.class.new( newcomp )
727
+ else
728
+ return self.class.new( :or, [self, other_filter] )
729
+ end
730
+ end
731
+
691
732
 
692
733
  end # class Treequel::Filter
693
734
 
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'treequel'
5
+ require 'treequel/model'
6
+ require 'treequel/mixins'
7
+ require 'treequel/constants'
8
+
9
+
10
+ # Mixin that provides Treequel::Model characteristics to a mixin module.
11
+ module Treequel::Model::ObjectClass
12
+
13
+ ### Extension callback -- add data structures to the extending +mod+.
14
+ ### @param [Module] mod the mixin module to be extended
15
+ def self::extended( mod )
16
+ # mod.instance_variable_set( :@model_directory, nil )
17
+ # mod.instance_variable_set( :@model_bases, [] )
18
+ mod.instance_variable_set( :@model_class, Treequel::Model )
19
+ mod.instance_variable_set( :@model_objectclasses, [] )
20
+ mod.instance_variable_set( :@model_bases, [] )
21
+ super
22
+ end
23
+
24
+
25
+ ### Inclusion callback -- Methods should be applied to the module rather than an instance.
26
+ ### Warn the user if they use include() and extend() instead.
27
+ def self::included( mod )
28
+ warn "extending %p rather than appending features to it" % [ mod ]
29
+ mod.extend( self )
30
+ end
31
+
32
+
33
+ ### Declare which Treequel::Model subclasses the mixin will register itself with. If this is
34
+ ### used, it should be declared *before* declaring the mixin's bases and/or objectClasses.
35
+ def model_class( mclass=nil )
36
+ if mclass
37
+
38
+ # If there were already registered objectclasses, remove them from the previous
39
+ # model class
40
+ unless @model_objectclasses.empty? && @model_bases.empty?
41
+ Treequel.log.warn "%p: model_class should come before model_objectclasses" % [ self ]
42
+ @model_class.unregister_mixin( self )
43
+ mclass.register_mixin( self )
44
+ end
45
+ @model_class = mclass
46
+ end
47
+
48
+ return @model_class
49
+ end
50
+
51
+
52
+ ### Set or get objectClasses that the mixin requires. Also registers the mixin with
53
+ ### Treequel::Model.
54
+ ###
55
+ ### @param [Array<Symbol>] objectclasses the objectClasses the mixin will apply to, as an
56
+ ### array of Symbols
57
+ ### @return [Array<Symbol>] the objectClasses that the module requires
58
+ def model_objectclasses( *objectclasses )
59
+ unless objectclasses.empty?
60
+ @model_objectclasses = objectclasses
61
+ @model_class.register_mixin( self )
62
+ end
63
+ return @model_objectclasses.dup
64
+ end
65
+
66
+
67
+ ### Set or get base DNs that the mixin applies to.
68
+ def model_bases( *base_dns )
69
+ unless base_dns.empty?
70
+ @model_bases = base_dns.collect {|dn| dn.gsub(/\s+/, '') }
71
+ @model_class.register_mixin( self )
72
+ end
73
+
74
+ return @model_bases.dup
75
+ end
76
+
77
+
78
+ ### Return a Branchset (or BranchCollection if the receiver has more than one
79
+ ### base) that can be used to search the given +directory+ for entries to which
80
+ ### the receiver applies.
81
+ ###
82
+ ### @param [Treequel::Directory] directory the directory to search
83
+ ### @return [Treequel::Branchset, Treequel::BranchCollection] the encapsulated search
84
+ def search( directory )
85
+ bases = self.model_bases
86
+ objectclasses = self.model_objectclasses
87
+
88
+ raise Treequel::ModelError, "%p has no search criteria defined" % [ self ] if
89
+ bases.empty? && objectclasses.empty?
90
+
91
+ Treequel.log.debug "Creating search for %s using model class %p" %
92
+ [ self.name, self.model_class ]
93
+
94
+ # Start by making a Branchset or BranchCollection for the mixin's bases. If
95
+ # the mixin doesn't have any bases, just use the base DN of the directory
96
+ # to be searched
97
+ bases = [directory.base_dn] if bases.empty?
98
+ search = bases.
99
+ map {|base| self.model_class.new(directory, base).branchset }.
100
+ inject {|branch1,branch2| branch1 + branch2 }
101
+
102
+ Treequel.log.debug "Search branch after applying bases is: %p" % [ search ]
103
+
104
+ return self.model_objectclasses.inject( search ) do |branchset, oid|
105
+ Treequel.log.debug " adding filter for objectClass=%s to %p" % [ oid, branchset ]
106
+ branchset.filter( :objectClass => oid )
107
+ end
108
+ end
109
+
110
+ end # Treequel::Model::ObjectClass
111
+