treequel 1.0.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 (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'ldap'
5
+
6
+ require 'treequel'
7
+ require 'treequel/mixins'
8
+ require 'treequel/constants'
9
+ require 'treequel/branch'
10
+
11
+
12
+ # A Treequel::BranchCollection is a union of Treequel::Branchset
13
+ # objects, suitable for performing operations on multiple branches
14
+ # of the directory at once.
15
+ #
16
+ # For example, if you have hosts under ou=Hosts in two different
17
+ # subdomains (e.g., acme.com, seattle.acme.com, and newyork.acme.com),
18
+ # and you want to search for a host by its CN, you could do so like
19
+ # this:
20
+ #
21
+ # # Top-level hosts, and those in the 'seattle' subdomain, but not
22
+ # # those in the 'newyork' subdomain:
23
+ # west_coast_hosts = dir.ou( :hosts ) + dir.dc( :seattle ).ou( :hosts )
24
+ # west_coast_www_hosts = west_coast_hosts.filter( :cn => 'www' )
25
+ #
26
+ # # And one that includes hosts in all three DCs:
27
+ # all_hosts = west_coast_hosts + dir.dc( :newyork ).ou( :hosts )
28
+ # all_ns_hosts = all_hosts.filter( :cn => 'ns*' )
29
+ #
30
+ # Note that you could accomplish most of what BranchCollection does
31
+ # using filters, but some people might find this a bit more readable.
32
+ #
33
+ # == Authors
34
+ #
35
+ # * Michael Granger <ged@FaerieMUD.org>
36
+ #
37
+ # :include: LICENSE
38
+ #
39
+ #--
40
+ #
41
+ # Please see the file LICENSE in the base directory for licensing details.
42
+ #
43
+ class Treequel::BranchCollection
44
+ include Enumerable,
45
+ Treequel::Loggable,
46
+ Treequel::Constants
47
+
48
+ extend Treequel::Delegation
49
+
50
+
51
+ #################################################################
52
+ ### C L A S S M E T H O D S
53
+ #################################################################
54
+
55
+ ### Create a delegator that will return an instance of the receiver created with the results of
56
+ ### iterating over the branchsets and calling the delegated method.
57
+ def self::def_cloning_delegators( *symbols )
58
+ symbols.each do |methname|
59
+ # Create the method body
60
+ methodbody = Proc.new {|*args|
61
+ mutated_branchsets = self.branchsets.
62
+ collect {|bs| bs.send(methname, *args) }.flatten
63
+ self.class.new( *mutated_branchsets )
64
+ }
65
+
66
+ # ...and install it
67
+ self.send( :define_method, methname, &methodbody )
68
+ end
69
+ end
70
+ private_class_method :def_cloning_delegators
71
+
72
+
73
+ #################################################################
74
+ ### I N S T A N C E M E T H O D S
75
+ #################################################################
76
+
77
+ ### Create a new Treequel::BranchCollection that will operate on the given +branchsets+.
78
+ def initialize( *branchsets )
79
+ @branchsets = branchsets.flatten.collect do |obj|
80
+ if obj.respond_to?( :each )
81
+ obj
82
+ else
83
+ Treequel::Branchset.new( obj )
84
+ end
85
+ end
86
+ end
87
+
88
+
89
+ ### Declare some delegator methods that clone the receiver with the results of mapping
90
+ ### the branchsets with the delegated method.
91
+ def_cloning_delegators :filter, :scope, :select, :select_all, :select_more, :timeout,
92
+ :without_timeout
93
+
94
+ ### Delegate some methods through the collection directly
95
+ def_method_delegators :branchsets, :include?
96
+
97
+
98
+ ######
99
+ public
100
+ ######
101
+
102
+ alias_method :all, :entries
103
+
104
+
105
+ # The collection's branchsets
106
+ attr_reader :branchsets
107
+
108
+
109
+ ### Return a human-readable string representation of the object suitable for debugging.
110
+ def inspect
111
+ "#<%s:0x%0x %d branchsets: %p>" % [
112
+ self.class.name,
113
+ self.object_id * 2,
114
+ self.branchsets.length,
115
+ self.branchsets.collect {|bs| bs.to_s },
116
+ ]
117
+ end
118
+
119
+
120
+ ### Iterate over the Treequel::Branches found by each member branchset, yielding each
121
+ ### one in turn.
122
+ def each( &block )
123
+ raise LocalJumpError, "no block given" unless block
124
+ self.branchsets.each do |bs|
125
+ bs.each( &block )
126
+ end
127
+ end
128
+
129
+
130
+ ### Return the first Treequel::Branch that is returned from the collection's branchsets.
131
+ def first
132
+ branch = nil
133
+
134
+ self.branchsets.each do |bs|
135
+ break if branch = bs.first
136
+ end
137
+
138
+ return branch
139
+ end
140
+
141
+ ### Overridden to support Branchset#map
142
+ def map( attribute=nil, &block )
143
+ if attribute
144
+ if block
145
+ super() {|branch| block.call(branch[attribute]) }
146
+ else
147
+ super() {|branch| branch[attribute] }
148
+ end
149
+ else
150
+ super( &block )
151
+ end
152
+ end
153
+
154
+
155
+ ### Append operator: add the specified +object+ (either a Treequel::Branchset or an object
156
+ ### that responds to #branchset and returns a Treequel::Branchset) to the collection
157
+ ### and return the receiver.
158
+ def <<( object )
159
+ if object.respond_to?( :branchset )
160
+ self.branchsets << object.branchset
161
+ else
162
+ self.branchsets << object
163
+ end
164
+
165
+ return self
166
+ end
167
+
168
+
169
+ ### Return a new Treequel::BranchCollection that includes both the receiver's Branchsets and
170
+ ### those in +other_object+ (or +other_object+ itself if it's a Branchset).
171
+ def +( other_object )
172
+ if other_object.respond_to?( :branchsets )
173
+ return self.class.new( self.branchsets + other_object.branchsets )
174
+ elsif other_object.respond_to?( :branchset )
175
+ return self.class.new( self.branchsets + [other_object.branchset] )
176
+ else
177
+ return self.class.new( self.branchsets + [other_object] )
178
+ end
179
+ end
180
+
181
+
182
+ ### Return a new Treequel::BranchCollection that contains the union of the branchsets from both
183
+ ### collections.
184
+ def &( other_collection )
185
+ return self.class.new( self.branchsets & other_collection.branchsets )
186
+ end
187
+
188
+
189
+ ### Return a new Treequel::BranchCollection that contains the intersection of the branchsets
190
+ ### from both collections.
191
+ def |( other_collection )
192
+ return self.class.new( self.branchsets | other_collection.branchsets )
193
+ end
194
+
195
+
196
+ ### Return the base DN of all of the collection's Branchsets.
197
+ def base_dns
198
+ return self.branchsets.collect {|bs| bs.base_dn }
199
+ end
200
+
201
+
202
+ end # class Treequel::BranchCollection
203
+
204
+
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'forwardable'
5
+ require 'ldap'
6
+
7
+ require 'treequel'
8
+ require 'treequel/mixins'
9
+ require 'treequel/constants'
10
+ require 'treequel/branch'
11
+ require 'treequel/filter'
12
+ require 'treequel/sequel_integration'
13
+
14
+
15
+ # A branchset represents an abstract set of LDAP records returned by
16
+ # a search in a directory. It can be used to create, retrieve, update,
17
+ # and delete records.
18
+ #
19
+ # Search results are fetched on demand, so a branchset can be kept
20
+ # around and reused indefinitely (branchsets never cache results):
21
+ #
22
+ # people = directory.ou( :people )
23
+ # davids = people.filter(:firstName => 'david') # no records are retrieved
24
+ # davids.all # records are retrieved
25
+ # davids.all # records are retrieved again
26
+ #
27
+ # Most branchset methods return modified copies of the branchset
28
+ # (functional style), so you can reuse different branchsets to access
29
+ # data:
30
+ #
31
+ # # (employeeId < 2000)
32
+ # veteran_davids = davids.filter( :employeeId < 2000 )
33
+ #
34
+ # # (&(employeeId < 2000)(|(deactivated >= '2008-12-22')(!(deactivated=*))))
35
+ # active_veteran_davids =
36
+ # veteran_davids.filter([:or, ['deactivated >= ?', Date.today], [:not, [:deactivated]] ])
37
+ #
38
+ # # (&(employeeId < 2000)(|(deactivated >= '2008-12-22')(!(deactivated=*)))(mobileNumber=*))
39
+ # active_veteran_davids_with_cellphones =
40
+ # active_veteran_davids.filter( [:mobileNumber] )
41
+ #
42
+ # Branchsets are Enumerable objects, so they can be manipulated using any of the
43
+ # Enumerable methods, such as map, inject, etc.
44
+ #
45
+ # == Authors
46
+ #
47
+ # * Michael Granger <ged@FaerieMUD.org>
48
+ #
49
+ # :include: LICENSE
50
+ #
51
+ #--
52
+ #
53
+ # Please see the file LICENSE in the base directory for licensing details.
54
+ #
55
+ class Treequel::Branchset
56
+ include Enumerable,
57
+ Treequel::Loggable,
58
+ Treequel::Constants
59
+
60
+ # The default scope to use when searching if none is specified
61
+ DEFAULT_SCOPE = :subtree
62
+ DEFAULT_SCOPE.freeze
63
+
64
+ # The default filter to use when searching if non is specified
65
+ DEFAULT_FILTER = :objectClass
66
+ DEFAULT_FILTER.freeze
67
+
68
+
69
+ # The default options hash for new Branchsets
70
+ DEFAULT_OPTIONS = {
71
+ :filter => DEFAULT_FILTER,
72
+ :scope => DEFAULT_SCOPE,
73
+ :timeout => 0, # Floating-point timeout -> sec, usec
74
+ :select => [], # Attributes to return -> attrs
75
+ :order => '', # Sorting criteria -> s_attr/s_proc
76
+ :limit => 0, # Limit -> number of results
77
+ }.freeze
78
+
79
+
80
+ #################################################################
81
+ ### I N S T A N C E M E T H O D S
82
+ #################################################################
83
+
84
+ ### Create a new Branchset for a search from the DN of the specified +branch+ (a
85
+ ### Treequel::Branch), with the given +options+.
86
+ def initialize( branch, options={} )
87
+ super()
88
+ @branch = branch
89
+ @options = DEFAULT_OPTIONS.merge( options )
90
+ end
91
+
92
+
93
+ ######
94
+ public
95
+ ######
96
+
97
+ alias_method :all, :entries
98
+
99
+ # The branchset's search options hash
100
+ attr_accessor :options
101
+
102
+ # The branchset's base branch that will be used when searching as the basedn
103
+ attr_accessor :branch
104
+
105
+
106
+ ### Returns the DN of the Branchset's branch.
107
+ def base_dn
108
+ return self.branch.dn
109
+ end
110
+
111
+
112
+ ### Override the default clone method to support cloning with different options.
113
+ def clone( options={} )
114
+ self.log.debug "cloning %p with options = %p" % [ self, options ]
115
+ newset = super()
116
+ newset.options = @options.merge( options )
117
+ return newset
118
+ end
119
+
120
+
121
+ ### Return a string representation of the Branchset's filter
122
+ def uri
123
+ # :scheme,
124
+ # :host, :port,
125
+ # :dn,
126
+ # :attributes,
127
+ # :scope,
128
+ # :filter,
129
+ # :extensions,
130
+ uri = self.branch.uri
131
+ uri.attributes = self.select.join(',')
132
+ uri.scope = SCOPE_NAME[ self.scope ]
133
+ uri.filter = self.filter_string
134
+ # :TODO: Add extensions? Support extensions in Branchset?
135
+
136
+ return uri
137
+ end
138
+
139
+
140
+ ### Return the Branchset as a stringified URI.
141
+ def to_s
142
+ return "%s/%s" % [ self.branch.dn, self.filter_string ]
143
+ end
144
+
145
+
146
+ ### Return a human-readable string representation of the object suitable for debugging.
147
+ def inspect
148
+ "#<%s:0x%0x base_dn='%s', filter=%s, scope=%s, select=%s, limit=%d, timeout=%0.3f>" % [
149
+ self.class.name,
150
+ self.object_id * 2,
151
+ self.base_dn,
152
+ self.filter_string,
153
+ self.scope,
154
+ self.select.empty? ? '*' : self.select.join(','),
155
+ self.limit,
156
+ self.timeout,
157
+ ]
158
+ end
159
+
160
+
161
+ ### Return an LDAP filter string made up of the current filter components.
162
+ def filter_string
163
+ return self.filter.to_s
164
+ end
165
+
166
+
167
+ ### Create a BranchCollection from the results of the Branchset and return it.
168
+ def collection
169
+ Treequel::BranchCollection.new( self.all )
170
+ end
171
+
172
+
173
+ ### Iterate over the entries which match the current criteria and yield each of them
174
+ ### as Treequel::Branch objects to the supplied block.
175
+ def each( &block )
176
+ raise LocalJumpError, "no block given" unless block
177
+ directory = self.branch.directory
178
+
179
+ directory.search( self.branch, self.scope, self.filter,
180
+ :selectattrs => self.select,
181
+ :timeout => self.timeout,
182
+ # :sortby => self.order,
183
+ :limit => self.limit,
184
+ &block
185
+ )
186
+ end
187
+
188
+ ### Fetch the first entry which matches the current criteria and return it as an instance of
189
+ ### the object that is set as the +branch+ (e.g., Treequel::Branch).
190
+ def first
191
+ self.branch.directory.search( self.branch, self.scope, self.filter,
192
+ :selectattrs => self.select,
193
+ :timeout => self.timeout,
194
+ # :sortby => self.order,
195
+ :limit => 1
196
+ ).first
197
+ end
198
+
199
+
200
+ ### Either maps entries which match the current criteria into an Array of the given
201
+ ### +attribute+, or falls back to the block form if no +attribute+ is specified. If both an
202
+ ### +attribute+ and a +block+ are given, the +block+ is called once for each +attribute+ value
203
+ ### instead of with each Branch.
204
+ def map( attribute=nil, &block ) # :yields: branch or attribute
205
+ if attribute
206
+ if block
207
+ super() {|branch| block.call(branch[attribute]) }
208
+ else
209
+ super() {|branch| branch[attribute] }
210
+ end
211
+ else
212
+ super( &block )
213
+ end
214
+ end
215
+
216
+
217
+ ### Map the results returned by the search into a hash keyed by the first value of +keyattr+
218
+ ### in the entry. If the optional +valueattr+ argument is given, the values will be the
219
+ ### first corresponding attribute, else the value will be the whole entry.
220
+ def to_hash( keyattr, valueattr=nil )
221
+ return self.inject({}) do |hash, branch|
222
+ key = branch[ keyattr ]
223
+ key = key.first if key.respond_to?( :first )
224
+
225
+ value = valueattr ? branch[ valueattr ] : branch.entry
226
+ value = value.first if value.respond_to?( :first )
227
+
228
+ hash[ key ] = value
229
+ hash
230
+ end
231
+ end
232
+
233
+
234
+ ###
235
+ ### Mutators
236
+ ###
237
+
238
+ ### Returns a clone of the receiving Branchset with the given +filterspec+ added
239
+ ### to it.
240
+ def filter( *filterspec )
241
+ if filterspec.empty?
242
+ opts = self.options
243
+ opts[:filter] = Treequel::Filter.new(opts[:filter]) unless
244
+ opts[:filter].is_a?( Treequel::Filter )
245
+ return opts[:filter]
246
+ else
247
+ self.log.debug "cloning %p with filterspec: %p" % [ self, filterspec ]
248
+ newfilter = Treequel::Filter.new( *filterspec )
249
+ return self.clone( :filter => self.filter + newfilter )
250
+ end
251
+ end
252
+
253
+
254
+ ### If called with no argument, returns the current scope of the Branchset. If
255
+ ### called with an argument (which should be one of the keys of
256
+ ### Treequel::Constants::SCOPE), returns a clone of the receiving Branchset
257
+ ### with the +new_scope+.
258
+ def scope( new_scope=nil )
259
+ if new_scope
260
+ self.log.debug "cloning %p with new scope: %p" % [ self, new_scope ]
261
+ return self.clone( :scope => new_scope.to_sym )
262
+ else
263
+ return @options[:scope]
264
+ end
265
+ end
266
+
267
+
268
+ ### If called with one or more +attributes+, returns a clone of the receiving
269
+ ### Branchset that will only fetch the +attributes+ specified. If no +attributes+
270
+ ### are specified, return the list of attributes that will be fetched by the
271
+ ### receiving Branchset. An empty Array means that it should fetch all
272
+ ### attributes, which is the default.
273
+ def select( *attributes )
274
+ if attributes.empty?
275
+ return self.options[:select].collect {|attribute| attribute.to_s }
276
+ else
277
+ self.log.debug "cloning %p with new selection: %p" % [ self, attributes ]
278
+ return self.clone( :select => attributes )
279
+ end
280
+ end
281
+
282
+
283
+ ### Returns a clone of the receiving Branchset that will fetch all attributes.
284
+ def select_all
285
+ return self.clone( :select => [] )
286
+ end
287
+
288
+
289
+ ### Return a clone of the receiving Branchset that will fetch the specified
290
+ ### +attributes+ in addition to its own.
291
+ def select_more( *attributes )
292
+ return self.select( *(Array(@options[:select]) | attributes) )
293
+ end
294
+
295
+
296
+ ### If called with a +new_limit+, returns a clone of the receiving Branchset that will
297
+ ### fetch (at most) +new_limit+ Branches. If no +new_limit+ argument is specified,
298
+ ### returns the Branchset's current limit. A limit of '0' means that all Branches
299
+ ### will be fetched.
300
+ def limit( new_limit=nil )
301
+ if new_limit.nil?
302
+ return self.options[:limit]
303
+ else
304
+ self.log.debug "cloning %p with new limit: %p" % [ self, new_limit ]
305
+ return self.clone( :limit => Integer(new_limit) )
306
+ end
307
+ end
308
+
309
+
310
+ ### Return a clone of the receiving Branchset that has no restriction on the number
311
+ ### of Branches that will be fetched.
312
+ def without_limit
313
+ return self.clone( :limit => 0 )
314
+ end
315
+
316
+
317
+ ### Return a clone of the receiving Branchset that will search with its timeout
318
+ ### set to +seconds+, which is in floating-point seconds.
319
+ def timeout( seconds=nil )
320
+ if seconds
321
+ return self.clone( :timeout => seconds )
322
+ else
323
+ return @options[:timeout]
324
+ end
325
+ end
326
+
327
+
328
+ ### Return a clone of the receiving Branchset that will not use a timeout when
329
+ ### searching.
330
+ def without_timeout
331
+ return self.clone( :timeout => 0 )
332
+ end
333
+
334
+
335
+ # Hiding this until we figure out how to do server-side ordering (i.e.,
336
+ # http://tools.ietf.org/html/rfc2891)
337
+
338
+ ### Return a clone of the receiving Branchsest that will order its results by the
339
+ ### +attributes+ specified.
340
+ def __order( attribute=:__default__ ) # :nodoc:
341
+ if attribute == :__default__
342
+ if block_given?
343
+ sort_func = Proc.new
344
+ return self.clone( :order => sort_func )
345
+ else
346
+ return self.options[:order]
347
+ end
348
+ elsif attribute.nil?
349
+ self.log.debug "cloning %p with no order" % [ self ]
350
+ return self.clone( :order => nil )
351
+ else
352
+ self.log.debug "cloning %p with new order: %p" % [ self, attribute ]
353
+ return self.clone( :order => attribute.to_sym )
354
+ end
355
+ end
356
+
357
+
358
+ end # class Treequel::Branchset
359
+
360
+