Directory

class
Superclass
Object
Included Modules
Treequel::Constants
Treequel::HashUtilities
Extended With
Loggability
Treequel::Delegation

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

Constants

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.

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.

DEFAULT_OPTIONS

The default directory options

DELEGATED_BRANCH_METHODS

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

SEARCH_DEFAULTS

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

SEARCH_PARAMETER_ORDER

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

search

Attributes

base_dn[RW]

The base DN of the directory

bound_user[R]

The DN of the user the directory is bound as

connect_type[RW]

The type of connection to establish

host[RW]

The host to connect to.

port[RW]

The port to connect to.

registered_controls[R]

The control modules that are registered with the directory

results_class[RW]

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

Public Class Methods

anchor
new( options={} )

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.

# File lib/treequel/directory.rb, line 127
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

Public Instance Methods

anchor
add_attribute_conversion( oid, conversion=nil )

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.

# File lib/treequel/directory.rb, line 520
def add_attribute_conversion( oid, conversion=nil )
        conversion = Proc.new if block_given?
        @attribute_conversions[ oid ] = conversion
end
anchor
add_object_conversion( oid, conversion=nil )

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.

# File lib/treequel/directory.rb, line 530
def add_object_conversion( oid, conversion=nil )
        conversion = Proc.new if block_given?
        @object_conversions[ oid ] = conversion
end
anchor
base()

Fetch the Branch for the base node of the directory.

# File lib/treequel/directory.rb, line 205
def base
        return @base ||= self.results_class.new( self, self.base_dn )
end
anchor
bind( user_dn, password )

Bind as the specified user_dn and password.

# File lib/treequel/directory.rb, line 281
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
Also aliased as: bind_as
anchor
bind_as( user_dn, password )
Alias for: bind
anchor
bound?()

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

# File lib/treequel/directory.rb, line 306
def bound?
        return self.conn.bound?
end
Also aliased as: is_bound?
anchor
bound_as( user_dn, password ) { || ... }

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.

# File lib/treequel/directory.rb, line 293
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
anchor
conn()

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

# File lib/treequel/directory.rb, line 239
def conn
        return @conn ||= self.connect
end
anchor
connected?()

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.

# File lib/treequel/directory.rb, line 247
def connected?
        return @conn ? true : false
end
anchor
convert_to_attribute( oid, 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).

# File lib/treequel/directory.rb, line 582
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
anchor
convert_to_object( oid, attribute )

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.

# File lib/treequel/directory.rb, line 561
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
anchor
create( branch, newattrs={} )

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.

# File lib/treequel/directory.rb, line 484
def create( branch, newattrs={} )
        newattrs = normalize_attributes( newattrs ) if newattrs.is_a?( Hash )
        self.conn.add( branch.to_s, newattrs )

        return true
end
anchor
delete( branch )

Delete the entry specified by the given branch.

# File lib/treequel/directory.rb, line 476
def delete( branch )
        self.log.info "Deleting %s from the directory." % [ branch ]
        self.conn.delete( branch.dn )
end
anchor
get_entry( branch )

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

# File lib/treequel/directory.rb, line 331
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
anchor
get_extended_entry( branch )

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.

# File lib/treequel/directory.rb, line 343
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
anchor
initialize_copy( original )

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

# File lib/treequel/directory.rb, line 155
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
anchor
inspect()

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

# File lib/treequel/directory.rb, line 223
def inspect
        return %Q{#<%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
anchor
is_bound?()
Alias for: bound?
anchor
modify( branch, mods )

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.

# File lib/treequel/directory.rb, line 463
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
anchor
move( branch, newdn )

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

# File lib/treequel/directory.rb, line 494
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
anchor
rdn_to( dn )

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

# File lib/treequel/directory.rb, line 323
def rdn_to( dn )
        base_re = Regexp.new( ',' + Regexp.quote(self.base_dn) + '$' )
        return dn.to_s.sub( base_re, '' )
end
anchor
reconnect()

Drop the existing connection and establish a new one.

# File lib/treequel/directory.rb, line 253
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
anchor
register_control( *modules )
Alias for: register_controls
anchor
register_controls( *modules )

Register the specified modules

# File lib/treequel/directory.rb, line 537
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
Also aliased as: register_control
anchor
root_dse()

Fetch the root DSE as a Treequel::Branch.

# File lib/treequel/directory.rb, line 199
def root_dse
        return self.search( '', :base, '(objectClass=*)', :selectattrs => ['+', '*'] ).first
end
anchor
schema()

Fetch the schema from the server.

# File lib/treequel/directory.rb, line 353
def schema
        unless @schema
                schemahash = self.conn.schema
                @schema = Treequel::Schema.new( schemahash )
        end

        return @schema
end
anchor
search( base, scope=:subtree, filter='(objectClass=*)', options={} ) { |branch| ... }

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.

# File lib/treequel/directory.rb, line 400
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
anchor
supported_control_oids()

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

# File lib/treequel/directory.rb, line 603
def supported_control_oids
        return self.root_dse[:supportedControl]
end
anchor
supported_controls()

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.

# File lib/treequel/directory.rb, line 596
def supported_controls
        return self.supported_control_oids.collect {|oid| CONTROL_NAMES[oid] || oid }
end
anchor
supported_extension_oids()

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

# File lib/treequel/directory.rb, line 618
def supported_extension_oids
        return self.root_dse[:supportedExtension]
end
anchor
supported_extensions()

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.

# File lib/treequel/directory.rb, line 611
def supported_extensions
        return self.supported_extension_oids.collect {|oid| EXTENSION_NAMES[oid] || oid }
end
anchor
supported_feature_oids()

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

# File lib/treequel/directory.rb, line 633
def supported_feature_oids
        return self.root_dse[:supportedFeatures]
end
anchor
supported_features()

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.

# File lib/treequel/directory.rb, line 626
def supported_features
        return self.supported_feature_oids.collect {|oid| FEATURE_NAMES[oid] || oid }
end
anchor
to_s()

Returns a string that describes the directory

# File lib/treequel/directory.rb, line 211
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
anchor
unbind()

Ensure that the the receiver's connection is unbound.

# File lib/treequel/directory.rb, line 313
def unbind
        if @conn.bound?
                old_conn = @conn
                @conn = old_conn.dup
                old_conn.unbind
        end
end
anchor
uri()

Return the URI object that corresponds to the directory.

# File lib/treequel/directory.rb, line 268
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

Protected Instance Methods

anchor
connect()

Create a new LDAP::Conn object with the current host, port, and #connect_type and return it.

# File lib/treequel/directory.rb, line 650
def connect
        conn = nil

        case @connect_type
        when :tls
                self.log.debug "Connecting using TLS to %s:%d" % [ @host, @port ]
                conn = LDAP::SSLConn.new( @host, @port, true )
        when :ssl
                self.log.debug "Connecting using SSL to %s:%d" % [ @host, @port ]
                conn = LDAP::SSLConn.new( @host, @port )
        else
                self.log.debug "Connecting using an unencrypted connection to %s:%d" % [ @host, @port ]
                conn = LDAP::Conn.new( @host, @port )
        end

        conn.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 )
        conn.set_option( LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_OFF )

        return conn
end
anchor
get_default_base_dn()

Fetch the default base dn for the server from the server's Root DSE.

# File lib/treequel/directory.rb, line 673
def get_default_base_dn
        return self.root_dse[:namingContexts].first.dn
end
anchor
method_missing( attribute, *args )

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

# File lib/treequel/directory.rb, line 643
def method_missing( attribute, *args )
        return self.base.send( attribute, *args )
end
anchor
normalize_search_parameters( base, scope, filter, parameters )

Normalize the parameters to the search method into the format expected by the LDAP::Conn#Search_ext2 method and return them as a Hash.

# File lib/treequel/directory.rb, line 693
def normalize_search_parameters( base, scope, filter, parameters )
        search_paramhash = SEARCH_DEFAULTS.merge( parameters )

        # Use the DN of the base object if it's an object that knows what a DN is
        base = base.dn if base.respond_to?( :dn )
        scope = SCOPE[scope.to_sym] if scope.respond_to?( :to_sym ) && SCOPE.key?( scope.to_sym )
        filter = filter.to_s

        # Split seconds and microseconds from the timeout value, convert the
        # fractional part to µsec
        timeout = search_paramhash.delete( :timeout ) || 0
        search_paramhash[:timeout_s] = timeout.truncate
        search_paramhash[:timeout_us] = Integer((timeout - timeout.truncate) * 1_000_000)

        ### Sorting in Ruby-LDAP is not significantly more useful than just sorting
        ### the returned entries from Ruby, as it happens client-side anyway (i.e., entries
        ### are still returned from the server in arbitrary/insertion order, and then the client
        ### sorts those
        search_paramhash[:sort_func] = nil
        search_paramhash[:sort_attribute] = ''

        return base, scope, filter, search_paramhash
end
anchor
with_duplicate_conn() { || ... }

Execute a block with a copy of the current connection, restoring the original after the block returns.

# File lib/treequel/directory.rb, line 680
def with_duplicate_conn
        original_conn = self.conn
        @conn = original_conn.dup
        self.log.info "Executing with %p, a copy of connection %p" % [ @conn, original_conn ]
        yield
ensure
        self.log.info "  restoring original connection %p." % [ original_conn ]
        @conn = original_conn
end