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,541 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'time'
4
+
5
+ require 'ldap'
6
+ require 'ldap/schema'
7
+
8
+ require 'treequel'
9
+ require 'treequel/schema'
10
+ require 'treequel/mixins'
11
+ require 'treequel/constants'
12
+
13
+
14
+ # The object in Treequel that represents a connection to a directory, the
15
+ # binding to that directory, and the base from which all DNs start.
16
+ #
17
+ # == Authors
18
+ #
19
+ # * Michael Granger <ged@FaerieMUD.org>
20
+ # * Mahlon E. Smith <mahlon@martini.nu>
21
+ #
22
+ # :include: LICENSE
23
+ #
24
+ #--
25
+ #
26
+ # Please see the file LICENSE in the base directory for licensing details.
27
+ #
28
+ class Treequel::Directory
29
+ include Treequel::Loggable,
30
+ Treequel::Constants,
31
+ Treequel::HashUtilities
32
+
33
+ extend Treequel::Delegation
34
+
35
+ # The default directory options
36
+ DEFAULT_OPTIONS = {
37
+ :host => 'localhost',
38
+ :port => LDAP::LDAP_PORT,
39
+ :connect_type => :tls,
40
+ :base_dn => nil,
41
+ :bind_dn => nil,
42
+ :pass => nil
43
+ }
44
+
45
+ # Default mapping of SYNTAX OIDs to conversions. See #add_syntax_mapping for more
46
+ # information on what a valid conversion is.
47
+ DEFAULT_SYNTAX_MAPPING = {
48
+ OIDS::BIT_STRING_SYNTAX => lambda {|bs| bs[0..-1].to_i(2) },
49
+ OIDS::BOOLEAN_SYNTAX => { 'TRUE' => true, 'FALSE' => false },
50
+ OIDS::GENERALIZED_TIME_SYNTAX => lambda {|string| Time.parse(string) },
51
+ OIDS::UTC_TIME_SYNTAX => lambda {|string| Time.parse(string) },
52
+ OIDS::INTEGER_SYNTAX => lambda {|string| Integer(string) },
53
+ }
54
+
55
+ # :NOTE: the docs for #search_ext2 lie. The method signature is actually:
56
+ # rb_scan_args (argc, argv, "39",
57
+ # &base, &scope, &filter, &attrs, &attrsonly,
58
+ # &serverctrls, &clientctrls, &sec, &usec, &limit,
59
+ # &s_attr, &s_proc)
60
+
61
+ # The order in which hash arguments should be extracted from Hash parameters to
62
+ # #search
63
+ SEARCH_PARAMETER_ORDER = [
64
+ :selectattrs,
65
+ :attrsonly,
66
+ :server_controls,
67
+ :client_controls,
68
+ :timeout_s,
69
+ :timeout_us,
70
+ :limit,
71
+ :sort_attribute,
72
+ :sort_func,
73
+ ].freeze
74
+
75
+ # Default values to pass to LDAP::Conn#search_ext2; they'll be passed in the order
76
+ # specified by SEARCH_PARAMETER_ORDER.
77
+ SEARCH_DEFAULTS = {
78
+ :selectattrs => ['*'],
79
+ :attrsonly => false,
80
+ :server_controls => nil,
81
+ :client_controls => nil,
82
+ :timeout => 0,
83
+ :limit => 0,
84
+ :sortby => nil,
85
+ }.freeze
86
+
87
+
88
+ require 'treequel/branch'
89
+
90
+ # The methods that get delegated to the directory's #base branch.
91
+ DELEGATED_BRANCH_METHODS =
92
+ Treequel::Branch.instance_methods(false).collect {|m| m.to_sym }
93
+
94
+
95
+
96
+ #################################################################
97
+ ### C L A S S M E T H O D S
98
+ #################################################################
99
+
100
+ ### Create a new Treequel::Directory with the given +options+. Options is a hash with one
101
+ ### or more of the following key-value pairs:
102
+ ###
103
+ ### [:host]
104
+ ### The LDAP host to connect to.
105
+ ### [:port]
106
+ ### The port to connect to.
107
+ ### [:connect_type]
108
+ ### The type of connection to establish. Must be one of +:plain+, +:tls+, or +:ssl+.
109
+ ### [:base_dn]
110
+ ### The base DN of the directory.
111
+ ### [:bind_dn]
112
+ ### The DN of the user to bind as.
113
+ ### [:pass]
114
+ ### The password to use when binding.
115
+ def initialize( options={} )
116
+ options = DEFAULT_OPTIONS.merge( options )
117
+
118
+ @host = options[:host]
119
+ @port = options[:port]
120
+ @connect_type = options[:connect_type]
121
+
122
+ @conn = nil
123
+ @bound_as = nil
124
+
125
+ @base_dn = options[:base_dn] || self.get_default_base_dn
126
+ @syntax_mapping = DEFAULT_SYNTAX_MAPPING.dup
127
+
128
+ @base = nil
129
+
130
+ # Immediately bind if credentials are passed to the initializer.
131
+ if ( options[:bind_dn] && options[:pass] )
132
+ self.bind( options[:bind_dn], options[:pass] )
133
+ end
134
+ end
135
+
136
+
137
+ ######
138
+ public
139
+ ######
140
+
141
+ # Delegate some methods to the #base Branch.
142
+ def_method_delegators :base, *DELEGATED_BRANCH_METHODS
143
+
144
+ # The host to connect to.
145
+ attr_accessor :host
146
+
147
+ # The port to connect to.
148
+ attr_accessor :port
149
+
150
+ # The type of connection to establish
151
+ attr_accessor :connect_type
152
+
153
+ # The base DN of the directory
154
+ attr_accessor :base_dn
155
+
156
+
157
+ ### Fetch the Branch for the base node of the directory.
158
+ def base
159
+ return @base ||= Treequel::Branch.new( self, self.base_dn )
160
+ end
161
+
162
+
163
+ ### Returns a string that describes the directory
164
+ def to_s
165
+ return "%s:%d (%s, %s, %s)" % [
166
+ self.host,
167
+ self.port,
168
+ self.base_dn,
169
+ self.connect_type,
170
+ self.bound? ? @bound_as : 'anonymous'
171
+ ]
172
+ end
173
+
174
+
175
+ ### Return a human-readable representation of the object suitable for debugging
176
+ def inspect
177
+ return %{#<%s:0x%0x %s:%d (%s) base_dn=%p, bound as=%s, schema=%s>} % [
178
+ self.class.name,
179
+ self.object_id / 2,
180
+ self.host,
181
+ self.port,
182
+ @conn ? "connected" : "not connected",
183
+ self.base_dn,
184
+ @bound_as ? @bound_as.dump : "anonymous",
185
+ @schema ? @schema.inspect : "(schema not loaded)",
186
+ ]
187
+ end
188
+
189
+
190
+ ### Return the LDAP::Conn object associated with this directory, creating it with the
191
+ ### current options if necessary.
192
+ def conn
193
+ return @conn ||= self.connect
194
+ end
195
+
196
+
197
+ ### Return the URI object that corresponds to the directory.
198
+ def uri
199
+ uri_parts = {
200
+ :scheme => self.connect_type == :ssl ? 'ldaps' : 'ldap',
201
+ :host => self.host,
202
+ :port => self.port,
203
+ :dn => '/' + self.base_dn
204
+ }
205
+
206
+ return URI::LDAP.build( uri_parts )
207
+ end
208
+
209
+
210
+ ### Bind as the specified +user_dn+ and +password+. If the optional +block+ is given,
211
+ ### it will be executed with the receiver bound, then returned to its previous state when
212
+ ### the block exits.
213
+ def bind( user_dn, password )
214
+ user_dn = user_dn.dn if user_dn.respond_to?( :dn )
215
+
216
+ self.log.info "Binding with connection %p as: %s" % [ self.conn, user_dn ]
217
+ self.conn.bind( user_dn.to_s, password )
218
+ @bound_as = user_dn.to_s
219
+ end
220
+
221
+
222
+ ### Execute the provided +block+ after binding as +user_dn+ with the given +password+. After
223
+ ### the block returns, the original binding (if any) will be restored.
224
+ def bound_as( user_dn, password )
225
+ raise LocalJumpError, "no block given" unless block_given?
226
+ previous_bind_dn = @bound_as
227
+ self.with_duplicate_conn do
228
+ self.bind( user_dn, password )
229
+ yield
230
+ end
231
+ ensure
232
+ @bound_as = previous_bind_dn
233
+ end
234
+
235
+
236
+ ### Returns +true+ if the directory's connection has already established a binding.
237
+ def bound?
238
+ return self.conn.bound?
239
+ end
240
+ alias_method :is_bound?, :bound?
241
+
242
+
243
+ ### Ensure that the the receiver's connection is unbound.
244
+ def unbind
245
+ if @conn.bound?
246
+ old_conn = @conn
247
+ @conn = old_conn.dup
248
+ old_conn.unbind
249
+ end
250
+ end
251
+
252
+
253
+ ### Return the RDN string to the given +dn+ from the base of the directory.
254
+ def rdn_to( dn )
255
+ base_re = Regexp.new( ',' + Regexp.quote(self.base_dn) + '$' )
256
+ return dn.to_s.sub( base_re, '' )
257
+ end
258
+
259
+
260
+ ### Given a Treequel::Branch object, find its corresponding LDAP::Entry and return
261
+ ### it.
262
+ def get_entry( branch )
263
+ self.log.debug "Looking up entry for %p" % [ branch.dn ]
264
+ return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)' ).first
265
+ rescue LDAP::ResultError => err
266
+ self.log.info " search for %p failed: %s" % [ branch.dn, err.message ]
267
+ return nil
268
+ end
269
+
270
+
271
+ ### Given a Treequel::Branch object, find its corresponding LDAP::Entry and return
272
+ ### it with its operational attributes (http://tools.ietf.org/html/rfc4512#section-3.4)
273
+ ### included.
274
+ def get_extended_entry( branch )
275
+ self.log.debug "Looking up entry (with operational attributes) for %p" % [ branch.dn ]
276
+ return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)', %w[* +] ).first
277
+ rescue LDAP::ResultError => err
278
+ self.log.info " search for %p failed: %s" % [ branch.dn, err.message ]
279
+ return nil
280
+ end
281
+
282
+
283
+ ### Fetch the schema from the server.
284
+ def schema
285
+ unless @schema
286
+ schemahash = self.conn.schema
287
+ @schema = Treequel::Schema.new( schemahash )
288
+ end
289
+
290
+ return @schema
291
+ end
292
+
293
+
294
+ ### Perform a +scope+ search at +base+ using the specified +filter+. The +scope+ argument
295
+ ### can be one of +:onelevel+, +:base+, or +:subtree+. Results will be returned as instances
296
+ ### of the given +collectclass+.
297
+ def search( base, scope=:subtree, filter='(objectClass=*)', parameters={} )
298
+ collectclass = nil
299
+
300
+ # If the base argument is an object whose class knows how to create instances of itself
301
+ # from an LDAP::Entry, use it instead of Treequel::Branch to wrap results
302
+ if parameters.key?( :results_class )
303
+ collectclass = parameters.delete( :results_class )
304
+ else
305
+ collectclass = base.class.respond_to?( :new_from_entry ) ? base.class : Treequel::Branch
306
+ end
307
+
308
+ # Format the arguments in the way #search_ext2 expects them
309
+ base, scope, filter, searchparams =
310
+ self.normalize_search_parameters( base, scope, filter, parameters )
311
+
312
+ # Unwrap the search parameters from the hash in the correct order
313
+ self.log.debug {
314
+ attrlist = SEARCH_PARAMETER_ORDER.inject([]) do |list, param|
315
+ list << "%s: %p" % [ param, searchparams[param] ]
316
+ end
317
+ "searching with base: %p, scope: %p, filter: %p, %s" %
318
+ [ base, scope, filter, attrlist.join(', ') ]
319
+ }
320
+ parameters = searchparams.values_at( *SEARCH_PARAMETER_ORDER )
321
+
322
+ # Wrap each result in the class derived from the 'base' argument
323
+ self.log.debug "Searching via search_ext2 with arguments: %p" % [[
324
+ base, scope, filter, *parameters
325
+ ]]
326
+ if block_given?
327
+ self.conn.search_ext2( base, scope, filter, *parameters ).each do |entry|
328
+ yield collectclass.new_from_entry( entry, self )
329
+ end
330
+ else
331
+ return self.conn.search_ext2( base, scope, filter, *parameters ).
332
+ collect {|entry| collectclass.new_from_entry(entry, self) }
333
+ end
334
+
335
+ rescue RuntimeError => err
336
+ conn = self.conn
337
+
338
+ # The LDAP library raises a plain RuntimeError with an incorrect message if the
339
+ # connection goes away, so it's caught here to rewrap it
340
+ case err.message
341
+ when /no result returned by search/i
342
+ raise LDAP::ResultError.new( LDAP.err2string(conn.err) )
343
+ else
344
+ raise
345
+ end
346
+ end
347
+
348
+
349
+ ### Modify the entry specified by the given +dn+ with the specified +mods+, which can be
350
+ ### either an Array of LDAP::Mod objects or a Hash of attribute/value pairs.
351
+ def modify( branch, mods )
352
+ normattrs = self.normalize_attributes( mods )
353
+ self.log.debug "Modifying %s with attributes: %p" % [ branch.dn, normattrs ]
354
+ self.conn.modify( branch.dn, normattrs )
355
+ end
356
+
357
+
358
+ ### Delete the entry specified by the given +branch+.
359
+ def delete( branch )
360
+ self.log.info "Deleting %s from the directory." % [ branch ]
361
+ self.conn.delete( branch.dn )
362
+ end
363
+
364
+
365
+ ### Create the entry for the given +branch+, setting its attributes to +newattrs+.
366
+ def create( branch, newattrs={} )
367
+ newdn = branch.dn
368
+ schema = self.schema
369
+
370
+ # Merge RDN attributes with existing ones, combining any that exist in both
371
+ self.log.debug "Smushing rdn attributes %p into %p" % [ branch.rdn_attributes, newdn ]
372
+ newattrs.merge!( branch.rdn_attributes ) do |key, *values|
373
+ values.flatten
374
+ end
375
+
376
+ normattrs = self.normalize_attributes( newattrs )
377
+ raise ArgumentError, "Can't create an entry with no objectClasses" unless
378
+ normattrs.key?( 'objectClass' )
379
+ normattrs['objectClass'].each do |oc|
380
+ raise ArgumentError, "No such objectClass #{oc.inspect}" unless
381
+ schema.object_classes.key?(oc.to_sym)
382
+ end
383
+ raise ArgumentError, "Can't create an entry with no structural objectClass" unless
384
+ normattrs['objectClass'].any? {|oc| schema.object_classes[oc.to_sym].structural? }
385
+
386
+ self.log.debug "Creating an entry at %s with the attributes: %p" % [ newdn, normattrs ]
387
+ self.conn.add( newdn, normattrs )
388
+
389
+ return true
390
+ end
391
+
392
+
393
+ ### Move the entry from the specified +branch+ to the new entry specified by
394
+ ### +newdn+. Returns the (moved) branch object.
395
+ def move( branch, newdn )
396
+ source_rdn, source_parent_dn = branch.split_dn( 2 )
397
+ new_rdn, new_parent_dn = newdn.split( /\s*,\s*/, 2 )
398
+
399
+ if new_parent_dn.nil?
400
+ new_parent_dn = source_parent_dn
401
+ newdn = [new_rdn, new_parent_dn].join(',')
402
+ end
403
+
404
+ if new_parent_dn != source_parent_dn
405
+ raise Treequel::Error,
406
+ "can't (yet) move an entry to a new parent"
407
+ end
408
+
409
+ self.log.debug "Modrdn (move): %p -> %p within %p" % [ source_rdn, new_rdn, source_parent_dn ]
410
+
411
+ self.conn.modrdn( branch.dn, new_rdn, true )
412
+ branch.dn = newdn
413
+ end
414
+
415
+
416
+ ### Add +conversion+ mapping for the specified +oid+. A conversion is any object that
417
+ ### responds to #[] with a String argument(e.g., Proc, Method, Hash); the argument is
418
+ ### the raw value String returned from the LDAP entry, and it should return the
419
+ ### converted value. Adding a mapping with a nil +conversion+ effectively clears it.
420
+ def add_syntax_mapping( oid, conversion=nil )
421
+ conversion = Proc.new if block_given?
422
+ @syntax_mapping[ oid ] = conversion
423
+ end
424
+
425
+
426
+ ### Map the specified +value+ to its Ruby datatype if one is registered for the given
427
+ ### syntax +oid+. If there is no conversion registered, just return the +value+ as-is.
428
+ def convert_syntax_value( oid, value )
429
+ self.log.debug "Converting value %p using the syntax rule for %p" % [ value, oid ]
430
+ unless conversion = @syntax_mapping[ oid ]
431
+ self.log.debug " ...no conversion, returning it as-is."
432
+ return value
433
+ end
434
+
435
+ self.log.debug " ...found conversion: %p" % [ conversion ]
436
+ return conversion[ value ]
437
+ end
438
+
439
+
440
+
441
+ #########
442
+ protected
443
+ #########
444
+
445
+ ### Delegate attribute/value calls on the directory itself to the directory's #base Branch.
446
+ def method_missing( attribute, *args )
447
+ return self.base.send( attribute, *args )
448
+ end
449
+
450
+
451
+ ### Create a new LDAP::Conn object with the current host, port, and connect_type
452
+ ### and return it.
453
+ def connect
454
+ conn = nil
455
+
456
+ case @connect_type
457
+ when :tls
458
+ self.log.debug "Connecting using TLS to %s:%d" % [ @host, @port ]
459
+ conn = LDAP::SSLConn.new( @host, @port, true )
460
+ when :ssl
461
+ self.log.debug "Connecting using SSL to %s:%d" % [ @host, @port ]
462
+ conn = LDAP::SSLConn.new( host, port )
463
+ else
464
+ self.log.debug "Connecting using an unencrypted connection to %s:%d" % [ @host, @port ]
465
+ conn = LDAP::Conn.new( host, port )
466
+ end
467
+
468
+ conn.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
469
+ conn.set_option( LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_OFF )
470
+
471
+ return conn
472
+ end
473
+
474
+
475
+ ### Fetch the default base dn for the server from the server's Root DSE.
476
+ def get_default_base_dn
477
+ dse = self.conn.root_dse
478
+ return '' if dse.nil? || dse.empty?
479
+ return dse.first['namingContexts'].first
480
+ end
481
+
482
+
483
+ ### Execute a block with a copy of the current connection, restoring the original
484
+ ### after the block returns.
485
+ def with_duplicate_conn
486
+ original_conn = self.conn
487
+ @conn = original_conn.dup
488
+ self.log.info "Executing with %p, a copy of connection %p" % [ @conn, original_conn ]
489
+ yield
490
+ ensure
491
+ self.log.info " restoring original connection %p." % [ original_conn ]
492
+ @conn = original_conn
493
+ end
494
+
495
+
496
+ ### Normalize the attributes in +hash+ to be of the form expected by the
497
+ ### LDAP library (i.e., keys as Strings, values as Arrays of Strings)
498
+ def normalize_attributes( hash )
499
+ normhash = {}
500
+ hash.each do |key,val|
501
+ val = [ val ] unless val.is_a?( Array )
502
+ val.collect! {|obj| obj.to_s }
503
+
504
+ normhash[ key.to_s ] = val
505
+ end
506
+
507
+ normhash.delete( 'dn' )
508
+
509
+ return normhash
510
+ end
511
+
512
+
513
+ ### Normalize the parameters to the #search method into the format expected by
514
+ ### the LDAP::Conn#Search_ext2 method and return them as a Hash.
515
+ def normalize_search_parameters( base, scope, filter, parameters )
516
+ search_paramhash = SEARCH_DEFAULTS.merge( parameters )
517
+
518
+ # Use the DN of the base object if it's an object that knows what a DN is
519
+ base = base.dn if base.respond_to?( :dn )
520
+ scope = SCOPE[scope.to_sym] if scope.respond_to?( :to_sym ) && SCOPE.key?( scope.to_sym )
521
+ filter = filter.to_s
522
+
523
+ # Split seconds and microseconds from the timeout value, convert the
524
+ # fractional part to µsec
525
+ timeout = search_paramhash.delete( :timeout ) || 0
526
+ search_paramhash[:timeout_s] = timeout.truncate
527
+ search_paramhash[:timeout_us] = Integer((timeout - timeout.truncate) * 1_000_000)
528
+
529
+ ### Sorting in Ruby-LDAP is not significantly more useful than just sorting
530
+ ### the returned entries from Ruby, as it happens client-side anyway (i.e., entries
531
+ ### are still returned from the server in arbitrary/insertion order, and then the client
532
+ ### sorts those
533
+ search_paramhash[:sort_func] = nil
534
+ search_paramhash[:sort_attribute] = ''
535
+
536
+ return base, scope, filter, search_paramhash
537
+ end
538
+
539
+ end # class Treequel::Directory
540
+
541
+