Class: Treequel::Directory

Inherits:
Object
  • Object
show all
Extended by:
Loggability, Delegation
Includes:
Constants, HashUtilities
Defined in:
lib/treequel/directory.rb

Overview

The object in Treequel that represents a connection to a directory, the binding to that directory, and the base from which all DNs start.

Constant Summary collapse

DEFAULT_OPTIONS =

The default directory options

{
  :host          => 'localhost',
  :port          => LDAP::LDAP_PORT,
  :connect_type  => :tls,
  :base_dn       => nil,
  :bind_dn       => nil,
  :pass          => nil,
  :results_class => Treequel::Branch,
}
DEFAULT_ATTRIBUTE_CONVERSIONS =

Default mapping of SYNTAX OIDs to conversions from an LDAP string. See #add_attribute_conversions for more information on what a valid conversion is.

{
  OIDS::BIT_STRING_SYNTAX         => lambda {|bs, _| bs[0..-1].to_i(2) },
  OIDS::BOOLEAN_SYNTAX            => { 'TRUE' => true, 'FALSE' => false },
  OIDS::GENERALIZED_TIME_SYNTAX   => lambda {|string, _| Time.parse(string) },
  OIDS::UTC_TIME_SYNTAX           => lambda {|string, _| Time.parse(string) },
  OIDS::INTEGER_SYNTAX            => lambda {|string, _| Integer(string) },
  OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|dn, directory|
    resclass = directory.results_class
    resclass.new( directory, dn )
  },
}
DEFAULT_OBJECT_CONVERSIONS =

Default mapping of SYNTAX OIDs to conversions to an LDAP string from a Ruby object. See #add_object_conversion for more information on what a valid conversion is.

{
  OIDS::BIT_STRING_SYNTAX         => lambda {|bs, _| bs.to_i.to_s(2) },
  OIDS::BOOLEAN_SYNTAX            => lambda {|obj, _| obj ? 'TRUE' : 'FALSE' },
  OIDS::GENERALIZED_TIME_SYNTAX   => lambda {|time, _| time.ldap_generalized },
  OIDS::UTC_TIME_SYNTAX           => lambda {|time, _| time.ldap_utc },
  OIDS::INTEGER_SYNTAX            => lambda {|obj, _| Integer(obj).to_s },
  OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|obj, _| obj.dn },
}
SEARCH_PARAMETER_ORDER =

The order in which hash arguments should be extracted from Hash parameters to #search

[
  :selectattrs,
  :attrsonly,
  :server_controls,
  :client_controls,
  :timeout_s,
  :timeout_us,
  :limit,
  :sort_attribute,
  :sort_func,
].freeze
SEARCH_DEFAULTS =

Default values to pass to LDAP::Conn#search_ext2; they’ll be passed in the order specified by SEARCH_PARAMETER_ORDER.

{
  :selectattrs     => ['*'],
  :attrsonly       => false,
  :server_controls => nil,
  :client_controls => nil,
  :timeout         => 0,
  :limit           => 0,
  :sortby          => nil,
}.freeze
DELEGATED_BRANCH_METHODS =

The methods that get delegated to the directory’s #base branch.

Treequel::Branch.instance_methods(false).collect {|m| m.to_sym }

Constants included from Constants

Constants::CONTROL_NAMES, Constants::CONTROL_OIDS, Constants::EXTENSION_NAMES, Constants::EXTENSION_OIDS, Constants::FEATURE_NAMES, Constants::FEATURE_OIDS, Constants::MINIMAL_OPERATIONAL_ATTRIBUTES, Constants::SCOPE, Constants::SCOPE_NAME

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Delegation

def_ivar_delegators, def_method_delegators

Methods included from HashUtilities

merge_recursively, normalize_attributes, stringify_keys, symbolify_keys

Constructor Details

#initialize(options = {}) ⇒ Directory

Create a new Treequel::Directory with the given options. Options is a hash with one or more of the following key-value pairs:

:host

The LDAP host to connect to (default: ‘localhost’).

:port

The port number to connect to (default: LDAP::LDAP_PORT).

:connect_type

The type of connection to establish; :tls, :ssl, or :plain. Defaults to :tls.

:base_dn

The base DN of the directory; defaults to the first naming context of the directory’s root DSE.

:bind_dn

The DN of the user to bind as; if unset, binds anonymously.

:pass

The password to use when binding.

:results_class

The class to instantiate by default for entries fetched from the Directory.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/treequel/directory.rb', line 128

def initialize( options={} )
  options                = DEFAULT_OPTIONS.merge( options )

  @host                  = options[:host]
  @port                  = options[:port]
  @connect_type          = options[:connect_type]
  @results_class         = options[:results_class]

  @conn                  = nil
  @bound_user            = nil


  @object_conversions    = DEFAULT_OBJECT_CONVERSIONS.dup
  @attribute_conversions = DEFAULT_ATTRIBUTE_CONVERSIONS.dup
  @registered_controls   = []

  @base_dn               = options[:base_dn] || self.get_default_base_dn
  @base                  = nil

  # Immediately bind if credentials are passed to the initializer.
  if ( options[:bind_dn] && options[:pass] )
    self.bind( options[:bind_dn], options[:pass] )
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(attribute, *args) ⇒ Object (protected)

Delegate attribute/value calls on the directory itself to the directory’s #base Branch.



644
645
646
# File 'lib/treequel/directory.rb', line 644

def method_missing( attribute, *args )
  return self.base.send( attribute, *args )
end

Instance Attribute Details

#base_dnObject

The base DN of the directory



190
191
192
# File 'lib/treequel/directory.rb', line 190

def base_dn
  @base_dn
end

#bound_userObject (readonly)

The DN of the user the directory is bound as



196
197
198
# File 'lib/treequel/directory.rb', line 196

def bound_user
  @bound_user
end

#connect_typeObject

The type of connection to establish



184
185
186
# File 'lib/treequel/directory.rb', line 184

def connect_type
  @connect_type
end

#hostObject

The host to connect to.



178
179
180
# File 'lib/treequel/directory.rb', line 178

def host
  @host
end

#portObject

The port to connect to.



181
182
183
# File 'lib/treequel/directory.rb', line 181

def port
  @port
end

#registered_controlsObject (readonly)

The control modules that are registered with the directory



193
194
195
# File 'lib/treequel/directory.rb', line 193

def registered_controls
  @registered_controls
end

#results_classObject

The Class to instantiate when wrapping results fetched from the Directory.



187
188
189
# File 'lib/treequel/directory.rb', line 187

def results_class
  @results_class
end

Instance Method Details

#add_attribute_conversion(oid, conversion = nil) ⇒ Object

Add conversion mapping for attributes of specified oid to a Ruby object. A conversion is any object that responds to #[] with a String argument(e.g., Proc, Method, Hash); the argument is the raw value String returned from the LDAP entry, and it should return the converted value. Adding a mapping with a nil conversion effectively clears it.



521
522
523
524
# File 'lib/treequel/directory.rb', line 521

def add_attribute_conversion( oid, conversion=nil )
  conversion = Proc.new if block_given?
  @attribute_conversions[ oid ] = conversion
end

#add_object_conversion(oid, conversion = nil) ⇒ Object

Add conversion mapping for the specified oid. A conversion is any object that responds to #[] with an object argument(e.g., Proc, Method, Hash); the argument is the Ruby object that’s being set as a value in an LDAP entry, and it should return the raw LDAP string. Adding a mapping with a nil conversion effectively clears it.



531
532
533
534
# File 'lib/treequel/directory.rb', line 531

def add_object_conversion( oid, conversion=nil )
  conversion = Proc.new if block_given?
  @object_conversions[ oid ] = conversion
end

#baseObject

Fetch the Branch for the base node of the directory.



206
207
208
# File 'lib/treequel/directory.rb', line 206

def base
  return @base ||= self.results_class.new( self, self.base_dn )
end

#bind(user_dn, password) ⇒ Object Also known as: bind_as

Bind as the specified user_dn and password.



282
283
284
285
286
287
288
# File 'lib/treequel/directory.rb', line 282

def bind( user_dn, password )
  user_dn = user_dn.dn if user_dn.respond_to?( :dn )

  self.log.info "Binding with connection %p as: %s" % [ self.conn, user_dn ]
  self.conn.bind( user_dn.to_s, password )
  @bound_user = user_dn.to_s
end

#bound?Boolean Also known as: is_bound?

Returns true if the directory’s connection is already bound to the directory.

Returns:

  • (Boolean)


307
308
309
# File 'lib/treequel/directory.rb', line 307

def bound?
  return self.conn.bound?
end

#bound_as(user_dn, password) ⇒ Object

Execute the provided block after binding as user_dn with the given password. After the block returns, the original binding (if any) will be restored.



294
295
296
297
298
299
300
301
302
303
# File 'lib/treequel/directory.rb', line 294

def bound_as( user_dn, password )
  raise LocalJumpError, "no block given" unless block_given?
  previous_bind_dn = @bound_user
  self.with_duplicate_conn do
    self.bind( user_dn, password )
    yield
  end
ensure
  @bound_user = previous_bind_dn
end

#connObject

Return the LDAP::Conn object associated with this directory, creating it with the current options if necessary.



240
241
242
# File 'lib/treequel/directory.rb', line 240

def conn
  return @conn ||= self.connect
end

#connected?Boolean

Returns true if a connection has been established. This does not necessarily mean that the connection is still valid, it just means it successfully established one at some point.

Returns:

  • (Boolean)


248
249
250
# File 'lib/treequel/directory.rb', line 248

def connected?
  return @conn ? true : false
end

#convert_to_attribute(oid, object) ⇒ Object

Map the specified Ruby object to its LDAP string equivalent if a conversion is registered for the given syntax oid. If there is no conversion registered, just returns the value as a String (via #to_s).



583
584
585
586
587
588
589
590
591
# File 'lib/treequel/directory.rb', line 583

def convert_to_attribute( oid, object )
  return object.to_s unless conversion = @object_conversions[ oid ]

  if conversion.respond_to?( :call )
    return conversion.call( object, self )
  else
    return conversion[ object ]
  end
end

#convert_to_object(oid, attribute) ⇒ Object

Map the specified LDAP attribute to its Ruby datatype if one is registered for the given syntax oid. If there is no conversion registered, just return the value as-is.



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/treequel/directory.rb', line 562

def convert_to_object( oid, attribute )
  if conversion = @attribute_conversions[ oid ]
    if conversion.respond_to?( :call )
      attribute = conversion.call( attribute, self )
    else
      attribute = conversion[ attribute ]
    end
  end

  # Force the encoding to UTF8, as that's what the directory should be returning.
  # Ruby-LDAP returns values as ASCII-8BIT.
  attribute = attribute.dup.force_encoding( Encoding::UTF_8 ) if
    attribute.respond_to?( :force_encoding )

  return attribute
end

#create(branch, newattrs = {}) ⇒ Object

Create the entry for the given branch, setting its attributes to newattrs, which can be either a Hash of attributes, or an Array of LDAP::Mod objects.



485
486
487
488
489
490
# File 'lib/treequel/directory.rb', line 485

def create( branch, newattrs={} )
  newattrs = normalize_attributes( newattrs ) if newattrs.is_a?( Hash )
  self.conn.add( branch.to_s, newattrs )

  return true
end

#delete(branch) ⇒ Object

Delete the entry specified by the given branch.



477
478
479
480
# File 'lib/treequel/directory.rb', line 477

def delete( branch )
  self.log.info "Deleting %s from the directory." % [ branch ]
  self.conn.delete( branch.dn )
end

#get_entry(branch) ⇒ Object

Given a Treequel::Branch object, find its corresponding LDAP::Entry and return it.



332
333
334
335
336
337
338
# File 'lib/treequel/directory.rb', line 332

def get_entry( branch )
  self.log.debug "Looking up entry for %p" % [ branch.dn ]
  return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)' ).first
rescue LDAP::ResultError => err
  self.log.info "  search for %p failed: %s" % [ branch.dn, err.message ]
  return nil
end

#get_extended_entry(branch) ⇒ Object

Given a Treequel::Branch object, find its corresponding LDAP::Entry and return it with its operational attributes (tools.ietf.org/html/rfc4512#section-3.4) included.



344
345
346
347
348
349
350
# File 'lib/treequel/directory.rb', line 344

def get_extended_entry( branch )
  self.log.debug "Looking up entry (with operational attributes) for %p" % [ branch.dn ]
  return self.conn.search_ext2( branch.dn, SCOPE[:base], '(objectClass=*)', %w[* +] ).first
rescue LDAP::ResultError => err
  self.log.info "  search for %p failed: %s" % [ branch.dn, err.message ]
  return nil
end

#initialize_copy(original) ⇒ Object

Copy constructor – the duplicate should have a distinct connection, bound user, and should have a distinct copy of the original‘s registered controls.



156
157
158
159
160
161
162
163
# File 'lib/treequel/directory.rb', line 156

def initialize_copy( original )
  @conn       = nil
  @bound_user = nil

  @object_conversions    = @object_conversions.dup
  @attribute_conversions = @attribute_conversions.dup
  @registered_controls   = @registered_controls.dup
end

#inspectObject

Return a human-readable representation of the object suitable for debugging



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/treequel/directory.rb', line 224

def inspect
  return %{#<%s:0x%0x %s:%d (%s) base_dn=%p, bound as=%s, schema=%s>} % [
    self.class.name,
    self.object_id / 2,
    self.host,
    self.port,
    @conn ? "connected" : "not connected",
    self.base_dn,
    @bound_user ? @bound_user.dump : "anonymous",
    @schema ? @schema.inspect : "(schema not loaded)",
  ]
end

#modify(branch, mods) ⇒ Object

Modify the entry specified by the given dn with the specified mods, which can be either an Array of LDAP::Mod objects or a Hash of attribute/value pairs.



464
465
466
467
468
469
470
471
472
473
# File 'lib/treequel/directory.rb', line 464

def modify( branch, mods )
  if mods.first.respond_to?( :mod_op )
    self.log.debug "Modifying %s with LDAP mod objects: %p" % [ branch.dn, mods ]
    self.conn.modify( branch.dn, mods )
  else
    normattrs = normalize_attributes( mods )
    self.log.debug "Modifying %s with: %p" % [ branch.dn, normattrs ]
    self.conn.modify( branch.dn, normattrs )
  end
end

#move(branch, newdn) ⇒ Object

Move the entry from the specified branch to the new entry specified by newdn. Returns the (moved) branch object.



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
# File 'lib/treequel/directory.rb', line 495

def move( branch, newdn )
  source_rdn, source_parent_dn = branch.split_dn( 2 )
  new_rdn, new_parent_dn = newdn.split( /\s*,\s*/, 2 )

  if new_parent_dn.nil?
    new_parent_dn = source_parent_dn
    newdn = [new_rdn, new_parent_dn].join(',')
  end

  if new_parent_dn != source_parent_dn
    raise Treequel::Error,
      "can't (yet) move an entry to a new parent"
  end

  self.log.debug "Modrdn (move): %p -> %p within %p" % [ source_rdn, new_rdn, source_parent_dn ]

  self.conn.modrdn( branch.dn, new_rdn, true )
  branch.dn = newdn
end

#rdn_to(dn) ⇒ Object

Return the RDN string to the given dn from the base of the directory.



324
325
326
327
# File 'lib/treequel/directory.rb', line 324

def rdn_to( dn )
  base_re = Regexp.new( ',' + Regexp.quote(self.base_dn) + '$' )
  return dn.to_s.sub( base_re, '' )
end

#reconnectObject

Drop the existing connection and establish a new one.



254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/treequel/directory.rb', line 254

def reconnect
  self.log.info "Reconnecting to %s..." % [ self.uri ]
  @conn = self.connect
  self.log.info "...reconnected."

  return true
rescue LDAP::ResultError => err
  self.log.error "%s while attempting to reconnect to %s: %s" %
    [ err.class.name, self.uri, err.message ]
  raise "Couldn't reconnect to %s: %s: %s" %
    [ self.uri, err.class.name, err.message ]
end

#register_controls(*modules) ⇒ Object Also known as: register_control

Register the specified modules



538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/treequel/directory.rb', line 538

def register_controls( *modules )
  supported_controls = self.supported_control_oids
  self.log.debug "Got %d supported controls: %p" %
    [ supported_controls.length, supported_controls ]

  modules.each do |mod|
    oid = mod.const_get( :OID ) if mod.const_defined?( :OID )
    raise NotImplementedError, "%s doesn't define an OID" % [ mod.name ] if oid.nil?

    self.log.debug "Checking for directory support for %p (%s)" % [ mod, oid ]

    if supported_controls.include?( oid )
      @registered_controls << mod
    else
      raise Treequel::UnsupportedControl,
        "%s is not supported by %s" % [ mod.name, self.uri ]
    end
  end
end

#root_dseObject

Fetch the root DSE as a Treequel::Branch.



200
201
202
# File 'lib/treequel/directory.rb', line 200

def root_dse
  return self.search( '', :base, '(objectClass=*)', :selectattrs => ['+', '*'] ).first
end

#schemaObject

Fetch the schema from the server.



354
355
356
357
358
359
360
361
# File 'lib/treequel/directory.rb', line 354

def schema
  unless @schema
    schemahash = self.conn.schema
    @schema = Treequel::Schema.new( schemahash )
  end

  return @schema
end

#search(base, scope = :subtree, filter = '(objectClass=*)', options = {}) ⇒ Object

Perform a scope search at base using the specified filter. The scope can be one of :onelevel, :base, or :subtree. The search filter should be a RFC4515-style filter either as a String or something that stringifies to one (e.g., a Treequel::Filter). The available search options are:

:results_class

The Class to use when wrapping results; if not specified, defaults to the class of base if it responds to #new_from_entry, or the directory object’s #results_class if it does not.

:selectattrs

The attributes to return from the search; defaults to ‘*’, which means to return all non-operational attributes. Specifying ‘+’ will cause the search to include operational parameters as well.

:attrsonly

If +true, the LDAP::Entry objects returned from the search won’t have attribute values. This has no real effect on Treequel::Branches, but is provided in case other results_class classes need it. Defaults to false.

:server_controls

Any server controls that should be sent with the search as an Array of LDAP::Control objects.

:client_controls

Any client controls that should be applied to the search as an Array of LDAP::Control objects.

:timeout

The number of (possibly floating-point) seconds after which the search request should be aborted.

:limit

The maximum number of results to return from the server.

:sort_attribute

An Array of String attribute names to sort by.

:sort_func

A function that will provide sorting.

Returns the array of results, each of which is wrapped in the options. If a block is given, it acts like a filter: it’s called once for each result, and the array of return values from the block is returned instead.



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/treequel/directory.rb', line 401

def search( base, scope=:subtree, filter='(objectClass=*)', options={} )
  collectclass = nil

  # If the base argument is an object whose class knows how to create instances of itself
  # from an LDAP::Entry, use it instead of Treequel::Branch to wrap results
  if options.key?( :results_class )
    collectclass = options.delete( :results_class )
  else
    collectclass = base.class.respond_to?( :new_from_entry ) ?
      base.class :
      self.results_class
  end

  # Format the arguments in the way #search_ext2 expects them
  base_dn, scope, filter, searchopts =
    self.normalize_search_parameters( base, scope, filter, options )

  # Unwrap the search options from the hash in the correct order
  self.log.debug do
    attrlist = SEARCH_PARAMETER_ORDER.inject([]) do |list, param|
      list << "%s: %p" % [ param, searchopts[param] ]
    end
    "searching with base: %p, scope: %p, filter: %p, %s" %
      [ base_dn, scope, filter, attrlist.join(', ') ]
  end
  parameters = searchopts.values_at( *SEARCH_PARAMETER_ORDER )

  # Wrap each result in the class derived from the 'base' argument
  self.log.debug "Searching via search_ext2 with arguments: %p" % [[
    base_dn, scope, filter, *parameters
  ]]

  results = []
  self.conn.search_ext2( base_dn, scope, filter, *parameters ).each do |entry|
    branch = collectclass.new_from_entry( entry, self )
    branch.include_operational_attrs = true if
      base.respond_to?( :include_operational_attrs? ) &&
      base.include_operational_attrs?

    if block_given?
      results << yield( branch )
    else
      results << branch
    end
  end

  return results
rescue RuntimeError => err
  conn = self.conn

  # The LDAP library raises a plain RuntimeError with an incorrect message if the
  # connection goes away, so it's caught here to rewrap it
  case err.message
  when /no result returned by search/i
    raise LDAP::ResultError.new( LDAP.err2string(conn.err) )
  else
    raise
  end
end

#supported_control_oidsObject

Return an Array of OID strings representing the controls supported by the Directory, as listed in the directory’s root DSE.



604
605
606
# File 'lib/treequel/directory.rb', line 604

def supported_control_oids
  return self.root_dse[:supportedControl]
end

#supported_controlsObject

Return an Array of Symbols for the controls supported by the Directory, as listed in the directory’s root DSE. Any controls which aren’t known (i.e., don’t have an entry in Treequel::Constants::CONTROL_NAMES), the numeric OID will be returned as-is.



597
598
599
# File 'lib/treequel/directory.rb', line 597

def supported_controls
  return self.supported_control_oids.collect {|oid| CONTROL_NAMES[oid] || oid }
end

#supported_extension_oidsObject

Return an Array of OID strings representing the extensions supported by the Directory, as listed in the directory’s root DSE.



619
620
621
# File 'lib/treequel/directory.rb', line 619

def supported_extension_oids
  return self.root_dse[:supportedExtension]
end

#supported_extensionsObject

Return an Array of Symbols for the extensions supported by the Directory, as listed in the directory’s root DSE. Any extensions which aren’t known (i.e., don’t have an entry in Treequel::Constants::EXTENSION_NAMES), the numeric OID will be returned as-is.



612
613
614
# File 'lib/treequel/directory.rb', line 612

def supported_extensions
  return self.supported_extension_oids.collect {|oid| EXTENSION_NAMES[oid] || oid }
end

#supported_feature_oidsObject

Return an Array of OID strings representing the features supported by the Directory, as listed in the directory’s root DSE.



634
635
636
# File 'lib/treequel/directory.rb', line 634

def supported_feature_oids
  return self.root_dse[:supportedFeatures]
end

#supported_featuresObject

Return an Array of Symbols for the features supported by the Directory, as listed in the directory’s root DSE. Any features which aren’t known (i.e., don’t have an entry in Treequel::Constants::FEATURE_NAMES), the numeric OID will be returned as-is.



627
628
629
# File 'lib/treequel/directory.rb', line 627

def supported_features
  return self.supported_feature_oids.collect {|oid| FEATURE_NAMES[oid] || oid }
end

#to_sObject

Returns a string that describes the directory



212
213
214
215
216
217
218
219
220
# File 'lib/treequel/directory.rb', line 212

def to_s
  return "%s:%d (%s, %s, %s)" % [
    self.host,
    self.port,
    self.base_dn,
    self.connect_type,
    self.bound? ? @bound_user : 'anonymous'
    ]
end

#unbindObject

Ensure that the the receiver’s connection is unbound.



314
315
316
317
318
319
320
# File 'lib/treequel/directory.rb', line 314

def unbind
  if @conn.bound?
    old_conn = @conn
    @conn = old_conn.dup
    old_conn.unbind
  end
end

#uriObject

Return the URI object that corresponds to the directory.



269
270
271
272
273
274
275
276
277
278
# File 'lib/treequel/directory.rb', line 269

def uri
  uri_parts = {
    :scheme => self.connect_type == :ssl ? 'ldaps' : 'ldap',
    :host   => self.host,
    :port   => self.port,
    :dn     => '/' + self.base_dn
  }

  return URI::LDAP.build( uri_parts )
end