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