The object in Treequel that represents a connection to a directory, the binding to that directory, and the base from which all DNs start.
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 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.
The default directory options
The methods that get delegated to the directory's #base branch.
Default values to pass to LDAP::Conn#search_ext2; they'll be passed in the order specified by SEARCH_PARAMETER_ORDER.
The order in which hash arguments should be extracted from Hash parameters to #search
The base DN of the directory
The DN of the user the directory is bound as
The type of connection to establish
The host to connect to.
The port to connect to.
The control modules that are registered with the directory
The Class to instantiate when wrapping results fetched from the 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.
# 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
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 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
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 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
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
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
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
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
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
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 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 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
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
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
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
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
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 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
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
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 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
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
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
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_s
The number of seconds (in addition to :timeout_us) after which the search request should be aborted.
:timeout_us
The number of microseconds (in addition to :timeout_s) 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 399 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/ raise LDAP::ResultError.new( LDAP.err2string(conn.err) ) else raise end end
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
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
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
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
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
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
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
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
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
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
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
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
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