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,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
+