treequel 1.0.4 → 1.1.0

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