class Treequel::Directory

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

new( options={} ) click to toggle source

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 123
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

add_attribute_conversion( oid, conversion=nil ) click to toggle source

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 519
def add_attribute_conversion( oid, conversion=nil )
        conversion = Proc.new if block_given?
        @attribute_conversions[ oid ] = conversion
end
add_object_conversion( oid, conversion=nil ) click to toggle source

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 529
def add_object_conversion( oid, conversion=nil )
        conversion = Proc.new if block_given?
        @object_conversions[ oid ] = conversion
end
base() click to toggle source

Fetch the Branch for the base node of the directory.

# File lib/treequel/directory.rb, line 201
def base
        return @base ||= self.results_class.new( self, self.base_dn )
end
bind( user_dn, password ) click to toggle source

Bind as the specified user_dn and password.

# File lib/treequel/directory.rb, line 277
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
bind_as( user_dn, password ) click to toggle source
Alias for: bind
bound?() click to toggle source

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

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

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 289
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
conn() click to toggle source

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

# File lib/treequel/directory.rb, line 235
def conn
        return @conn ||= self.connect
end
connected?() click to toggle source

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 243
def connected?
        return @conn ? true : false
end
convert_to_attribute( oid, object ) click to toggle source

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 574
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 ) click to toggle source

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 560
def convert_to_object( oid, attribute )
        return attribute unless conversion = @attribute_conversions[ oid ]

        if conversion.respond_to?( :call )
                return conversion.call( attribute, self )
        else
                return conversion[ attribute ]
        end
end
create( branch, newattrs={} ) click to toggle source

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 483
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 ) click to toggle source

Delete the entry specified by the given branch.

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

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

# File lib/treequel/directory.rb, line 327
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 ) click to toggle source

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 339
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=*)', ]* +] ).first
rescue LDAP::ResultError => err
        self.log.info "  search for %p failed: %s" % [ branch.dn, err.message ]
        return nil
end
initialize_copy( original ) click to toggle source

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 151
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
inspect() click to toggle source

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

# File lib/treequel/directory.rb, line 219
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
is_bound?() click to toggle source
Alias for: bound?
modify( branch, mods ) click to toggle source

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 462
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 ) click to toggle source

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 493
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 ) click to toggle source

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

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

Drop the existing connection and establish a new one.

# File lib/treequel/directory.rb, line 249
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_control( *modules ) click to toggle source
Alias for: register_controls
register_controls( *modules ) click to toggle source

Register the specified modules

# File lib/treequel/directory.rb, line 536
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
root_dse() click to toggle source

Fetch the root DSE as a Treequel::Branch.

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

Fetch the schema from the server.

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

        return @schema
end
supported_control_oids() click to toggle source

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 595
def supported_control_oids
        return self.root_dse[:supportedControl]
end
supported_controls() click to toggle source

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 588
def supported_controls
        return self.supported_control_oids.collect {|oid| CONTROL_NAMES[oid] || oid }
end
supported_extension_oids() click to toggle source

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 610
def supported_extension_oids
        return self.root_dse[:supportedExtension]
end
supported_extensions() click to toggle source

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 603
def supported_extensions
        return self.supported_extension_oids.collect {|oid| EXTENSION_NAMES[oid] || oid }
end
supported_feature_oids() click to toggle source

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 625
def supported_feature_oids
        return self.root_dse[:supportedFeatures]
end
supported_features() click to toggle source

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 618
def supported_features
        return self.supported_feature_oids.collect {|oid| FEATURE_NAMES[oid] || oid }
end
to_s() click to toggle source

Returns a string that describes the directory

# File lib/treequel/directory.rb, line 207
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
unbind() click to toggle source

Ensure that the the receiver's connection is unbound.

# File lib/treequel/directory.rb, line 309
def unbind
        if @conn.bound?
                old_conn = @conn
                @conn = old_conn.dup
                old_conn.unbind
        end
end
uri() click to toggle source

Return the URI object that corresponds to the directory.

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

connect() click to toggle source

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

# File lib/treequel/directory.rb, line 642
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
get_default_base_dn() click to toggle source

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

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

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

# File lib/treequel/directory.rb, line 635
def method_missing( attribute, *args )
        return self.base.send( attribute, *args )
end
normalize_search_parameters( base, scope, filter, parameters ) click to toggle source

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 685
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
with_duplicate_conn() { || ... } click to toggle source

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

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