treequel 1.0.0

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