lib/activeldap/base.rb in ruby-activeldap-debug-0.6.0 vs lib/activeldap/base.rb in ruby-activeldap-debug-0.7.0
- old
+ new
@@ -48,10 +48,16 @@
#
# An exception raised when a required attribute is found to be empty
class AttributeEmpty < RuntimeError
end
+ # ConfigurationError
+ #
+ # An exception raised when there is a problem with Base.connect arguments
+ class ConfigurationError < RuntimeError
+ end
+
# DeleteError
#
# An exception raised when an ActiveLDAP delete action fails
class DeleteError < RuntimeError
end
@@ -100,18 +106,20 @@
# All class-wide variables
@@config = nil # Container for current connection settings
@@schema = nil # LDAP server's schema
@@conn = nil # LDAP connection
+ @@reconnect_attempts = 0 # Number of reconnects attempted
# Driver generator
#
# TODO add type checking
# This let's you call this method to create top-level extension object. This
# is really just a proof of concept and has not truly useful purpose.
# example: Base.create_object(:class => "user", :dnattr => "uid", :classes => ['top'])
#
+ # THIS METHOD IS DANGEROUS. INPUT IS NOT SANITIZED.
def Base.create_object(config={})
# Just upcase the first letter of the new class name
str = config[:class]
class_name = str[0].chr.upcase + str[1..-1]
@@ -158,11 +166,11 @@
end
end
self.class.module_eval <<-"end_eval"
class ::#{class_name} < ActiveLDAP::Base
- ldap_mapping :dnattr => "#{attr}, :prefix => "#{prefix}", :classes => "#{classes}"
+ ldap_mapping :dnattr => "#{attr}", :prefix => "#{prefix}", :classes => #{classes}
#{belongs_to.join("\n")}
#{has_many.join("\n")}
end
end_eval
end
@@ -175,66 +183,52 @@
# :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous
# :user specifies the username to bind with.
# :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=dataspill,dc=org. Overrides @@bind_format.
# :password_block specifies a Proc object that will yield a String to be used as the password when called.
# :logger specifies a preconfigured Log4r::Logger to be used for all logging
- # :host overrides the configuration.rb @@host setting with the LDAP server hostname
- # :port overrides the configuration.rb @@port setting for the LDAP server port
+ # :host sets the LDAP server hostname
+ # :port sets the LDAP server port
# :base overwrites Base.base - this affects EVERYTHING
# :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false)
# :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
- # :retries - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs.
- def Base.connect(config={}) # :user, :password_block, :logger
+ # :retries - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs. -1 means infinite.
+ # :sasl_quiet - if true, sets @sasl_quiet on the Ruby/LDAP connection
+ # :method - whether to use :ssl, :tls, or :plain (unencrypted)
+ # :retry_wait - seconds to wait before retrying a connection
+ # :ldap_scope - dictates how to find objects. ONELEVEL by default to avoid dn_attr collisions across OUs. Think before changing.
+ # See lib/configuration.rb for defaults for each option
+ def Base.connect(config={})
# Process config
# Class options
## These will be replace by configuration.rb defaults if defined
- @@config = {}
- @@config[:host] = config[:host] || @@host
- @@config[:port] = config[:port] || @@port
- @@config[:retries] = config[:retries] || 3
- if config[:base]
- Base.class_eval <<-"end_eval"
- def Base.base
- '#{config[:base]}'
- end
- end_eval
+ @@config = DEFAULT_CONFIG.dup
+ config.keys.each do |key|
+ if key == :base
+ # Scrub before inserting
+ base = config[:base].gsub(/['}{#]/, '')
+ Base.class_eval("def Base.base();'#{base}';end")
+ else
+ @@config[key] = config[key]
+ end
end
- @@config[:bind_format] = config[:bind_format] || @@bind_format
-
- @@logger = config[:logger] || nil
+ # Assign a easier name for the logger
+ @@logger = @@config[:logger] || nil
# Setup default logger to console
if @@logger.nil?
@@logger = Log4r::Logger.new('activeldap')
@@logger.level = Log4r::OFF
Log4r::StderrOutputter.new 'console'
@@logger.add('console')
end
- # Method options
- user = nil
- password_block = nil
- @@config[:allow_anonymous] = true
- @@config[:try_sasl] = false
+ # Reset for the new connection
+ @@reconnect_attempts = 0
- @@config[:user] = config[:user] || user
- @@config[:allow_anonymous] = config[:allow_anonymous] if config.has_key? :allow_anonymous
- @@config[:try_sasl] = config[:try_sasl]
- @@config[:password_block] = config[:password_block] if config.has_key? :password_block
-
- # Setup bind credentials
- @@config[:user] = ENV['USER'] unless @@config[:user]
-
+ # Make the connection.
do_connect()
-
- # Load Schema (if not straight SSL...)
- begin
- @@schema = @@conn.schema() if @@schema.nil?
- rescue => detail
- raise ConnectionError, "#{detail.exception} - LDAP connection failure, or server does not support schema queries."
- end
- # Cleanly return
+ # Make irb users happy with a 'true'
return true
end # Base.connect
# Base.close
# This method deletes the LDAP connection object.
@@ -258,10 +252,60 @@
# Set the LDAP connection avoiding Base.connect or multiplexing connections
def Base.connection=(conn)
@@conn = conn
end
+ # Attempts to reconnect up to the number of times allowed
+ # If forced, try once then fail with ConnectionError if not connected.
+ def Base.reconnect(force=false)
+ not_connected = true
+ while not_connected
+ # Just to be clean, unbind if possible
+ begin
+ @@conn.unbind() if not @@conn.nil? and @@conn.bound?
+ rescue
+ # Ignore complaints.
+ end
+ @@conn = nil
+
+ if @@config[:retries] == -1 or force == true
+ @@logger.debug('Attempting to reconnect')
+
+ # Reset the attempts if this was forced.
+ @@reconnect_attempts = 0 if @@reconnect_attempts != 0
+ begin
+ do_connect()
+ not_connected = false
+ rescue => detail
+ @@logger.error("Reconnect to server failed: #{detail.exception}")
+ @@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}")
+ # Do not loop if forced
+ raise ConnectionError, detail.message if force
+ end
+ elsif @@reconnect_attempts < @@config[:retries]
+ @@logger.debug('Attempting to reconnect')
+ @@reconnect_attempts += 1
+ begin
+ do_connect()
+ not_connected = false
+ # Reset to 0 if a connection was made.
+ @@reconnect_attempts = 0
+ rescue => detail
+ @@logger.error("Reconnect to server failed: #{detail.exception}")
+ @@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}")
+ end
+ else
+ # Raise a warning
+ raise ConnectionError, 'Giving up trying to reconnect to LDAP server.'
+ end
+
+ # Sleep before looping
+ sleep @@config[:retry_wait]
+ end
+ return true
+ end
+
# Return the schema object
def Base.schema
@@schema
end
@@ -284,11 +328,10 @@
config[:base] = base() unless config.has_key? :base
values = []
config[:attrs] = config[:attrs].to_a # just in case
- tries = 0
begin
@@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
res = {}
res['dn'] = [m.dn.dup] # For consistency with the below
m.attrs.each do |attr|
@@ -299,18 +342,13 @@
values.push(res)
end
rescue RuntimeError => detail
#TODO# Check for 'No message' when retrying
# The connection may have gone stale. Let's reconnect and retry.
- if tries > @@config[:retries]
- # Do nothing on failure
- @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}"
- end
- tries += 1
- # Reconnect and rebind.
- do_connect()
- retry
+ retry if Base.reconnect()
+ # Do nothing on failure
+ @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}"
end
return values
end
# find
@@ -348,40 +386,34 @@
objects = config[:objects]
end
matches = []
- tries = 0
begin
# Get some attributes
- @@conn.search(base(), LDAP::LDAP_SCOPE_ONELEVEL, "(#{attr}=#{val})") do |m|
+ @@conn.search(base(), @@config[:ldap_scope], "(#{attr}=#{val})") do |m|
# Extract the dnattr value
dnval = m.dn.split(/,/)[0].split(/=/)[1]
if objects
return real_klass.new(m)
else
return dnval
end
end
rescue RuntimeError => detail
- #todo# check for 'no message' when retrying
- # the connection may have gone stale. let's reconnect and retry.
- if tries > @@config[:retries]
- # do nothing on failure
- @@logger.debug "no matches for #{attr}=#{val}"
- end
- tries += 1
- # reconnect and rebind.
- do_connect()
- retry
+ #TODO# Check for 'No message' when retrying
+ # The connection may have gone stale. Let's reconnect and retry.
+ retry if Base.reconnect()
+
+ # Do nothing on failure
+ @@logger.debug "no matches for #{attr}=#{val}"
end
return nil
end
private_class_method :find
-
# find_all
#
# Finds all matches for value where |value| is the value of some
# |field|, or the wildcard match. This is only useful for derived classes.
def Base.find_all(config = {})
@@ -410,14 +442,13 @@
objects = config[:objects]
end
matches = []
- tries = 0
begin
# Get some attributes
- @@conn.search(base(), LDAP::LDAP_SCOPE_ONELEVEL,
+ @@conn.search(base(), @@config[:ldap_scope],
"(#{attr}=#{val})") do |m|
# Extract the dnattr value
dnval = m.dn.split(/,/)[0].split(/=/)[1]
if objects
@@ -425,23 +456,16 @@
else
matches.push(dnval)
end
end
rescue RuntimeError => detail
- #todo# check for 'no message' when retrying
+ #TODO# Check for 'No message' when retrying
+ # The connection may have gone stale. Let's reconnect and retry.
+ retry if Base.reconnect()
- #TODO# This is broken because search gives bad messages.
-
- # the connection may have gone stale. let's reconnect and retry.
- if tries > @@config[:retries]
- # do nothing on failure
- @@logger.debug "no matches for #{attr}=#{val}"
- end
- tries += 1
- # reconnect and rebind.
- do_connect()
- retry
+ # Do nothing on failure
+ @@logger.debug "no matches for #{attr}=#{val}"
end
return matches
end
private_class_method :find_all
@@ -497,17 +521,20 @@
# with a LDAP::Entry is primarily meant for internal use by find and
# find_all.
def initialize(val)
@exists = false
# Try a default connection if none made explicitly
- unless Base.connection
+ if Base.connection.nil? and @@reconnect_attempts < @@config[:retries]
# Use @@config if it has been prepopulated and the conn is down.
if @@config
- ActiveLDAP::Base.connect(@@config)
+ ActiveLDAP::Base.reconnect
else
ActiveLDAP::Base.connect
end
+ elsif Base.connection.nil?
+ @@logger.error('Attempted to initialize a new object with no connection')
+ raise ConnectionError, 'Number of reconnect attempts exceeded.'
end
if val.class == LDAP::Entry
# Call import, which is basically initialize
# without accessing LDAP.
@@logger.debug "initialize: val is a LDAP::Entry - running import."
@@ -541,14 +568,13 @@
else
# Create what should be the authoritative DN
@dn = "#{dnattr()}=#{val},#{base()}"
# Search for the existing entry
- tries = 0
begin
# Get some attributes
- Base.connection.search(base(), LDAP::LDAP_SCOPE_ONELEVEL, "(#{dnattr()}=#{val})") do |m|
+ Base.connection.search(base(), @@config[:ldap_scope], "(#{dnattr()}=#{val})") do |m|
@exists = true
# Save DN
@dn = m.dn
# Load up data into tmp
@@logger.debug("loading entry: #{@dn}")
@@ -566,21 +592,17 @@
@ldap_data[safe_attr] = value
end
end
end
rescue RuntimeError => detail
- #todo# check for 'no message' when retrying
- # the connection may have gone stale. let's reconnect and retry.
- if tries <= @@config[:retries]
- tries += 1
- # reconnect and rebind.
- do_connect()
- retry
- else
- @@logger.error('new: unable to search for entry')
- raise detail
- end
+ #TODO# Check for 'No message' when retrying
+ # The connection may have gone stale. Let's reconnect and retry.
+ retry if Base.reconnect()
+
+ # Do nothing on failure
+ @@logger.error('new: unable to search for entry')
+ raise detail
rescue LDAP::ResultError
end
end
# Do the actual object setup work.
@@ -676,30 +698,23 @@
end
end
@@logger.debug("stub: validate finished")
end
-
# delete
#
# Delete this entry from LDAP
def delete
@@logger.debug("stub: delete called")
- tries = 0
begin
@@conn.delete(@dn)
@exists = false
rescue RuntimeError => detail
#todo# check for 'no message' when retrying
# the connection may have gone stale. let's reconnect and retry.
- if tries > @@config[:retries]
- raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
- end
- tries += 1
- # reconnect and rebind.
- do_connect()
- retry
+ retry if Base.reconnect()
+ raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
rescue LDAP::ResultError => detail
raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
end
end
@@ -821,25 +836,19 @@
# TODO: Added equality(attr) to Schema2
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty?
end
end
@@logger.debug("#write: traversing data complete")
- tries = 0
begin
@@logger.debug("#write: modifying #{@dn}")
@@conn.modify(@dn, entry)
@@logger.debug("#write: modify successful")
rescue RuntimeError => detail
#todo# check for 'no message' when retrying
# the connection may have gone stale. let's reconnect and retry.
- if tries > @@config[:retries]
- raise WriteError, "Could not update LDAP entry: #{detail}"
- end
- tries += 1
- # reconnect and rebind.
- do_connect()
- retry
+ retry if Base.reconnect()
+ raise WriteError, "Could not update LDAP entry: #{detail}"
rescue => detail
raise WriteError, "Could not update LDAP entry: #{detail}"
end
else # add everything!
@@logger.debug("#write: adding all attribute value pairs")
@@ -859,25 +868,19 @@
end
@@logger.debug("adding attribute to new entry: #{pair[0].inspect}: #{pair[1].inspect}")
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
end
end
- tries = 0
begin
@@logger.debug("#write: adding #{@dn}")
@@conn.add(@dn, entry)
@@logger.debug("#write: add successful")
@exists = true
- rescue RuntimeError => e
+ rescue RuntimeError => detail
# The connection may have gone stale. Let's reconnect and retry.
- if tries > @@config[:retries]
- raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
- end
- tries += 1
- # Reconnect and rebind.
- do_connect()
- retry
+ retry if Base.reconnect()
+ raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
rescue LDAP::ResultError => detail
raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}"
end
end
@@logger.debug("#write: resetting @ldap_data to a dup of @data")
@@ -1136,98 +1139,133 @@
# Performs the actually connection. This separate so that it may
# be called to refresh stale connections.
def Base.do_connect()
- # Wrap the whole thing an try retries times
- tries = 0
- begin
- # Connect to LDAP
- begin
- # SSL using START_TLS
+ begin
+ case @@config[:method]
+ when :ssl
+ @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
+ when :tls
@@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true)
- rescue
- @@logger.warn "Warning: Failed to connect using TLS!"
- begin
- @@logger.warn "Warning: Attempting SSL connection . . ."
- @@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
- # HACK: Load the schema here because otherwise you can't tell if the
- # HACK: SSLConn is a real SSL connection.
- @@schema = @@conn.schema() if @@schema.nil?
- rescue
- @@logger.warn "Warning: Attempting unencrypted connection . . ."
- @@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
- end
- end
- @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]}."
+ when :plain
+ @@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
+ else
+ raise ConfigurationError,"#{@@config[:method]} is not one of the available connect methods :ssl, :tls, or :plain"
+ end
+ rescue ConfigurationError => e
+ # Pass through
+ raise e
+ rescue => e
+ @@logger.error("Failed to connect using #{@@config[:method]}")
+ raise e
+ end
- # Enforce LDAPv3
- @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
+ # Enforce LDAPv3
+ @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
- # Authenticate
- do_bind
- rescue => e
- # Retry
- tries += 1
- raise e if tries > @@config[:retries]
- retry
- end
- end
+ # Authenticate
+ do_bind
+ # Retrieve the schema. We need this to automagically determine attributes
+ begin
+ @@schema = @@conn.schema() if @@schema.nil?
+ rescue => e
+ @@logger.error("Failed to retrieve the schema (#{@@config[:method]})")
+ @@logger.error("Schema failure exception: #{e.exception}")
+ @@logger.error("Schema failure backtrace: #{e.backtrace}")
+ raise ConnectionError, "#{e.exception} - LDAP connection failure, or server does not support schema queries."
+ end
+ @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]} using #{@@config[:method]}"
+ end
+
# Wrapper all bind activity
def Base.do_bind()
bind_dn = @@config[:bind_format] % [@@config[:user]]
- if @@config[:password_block]
- password = @@config[:password_block].call
- @@config[:password_block] = Proc.new { password }
- end
-
# Rough bind loop:
# Attempt 1: SASL if available
# Attempt 2: SIMPLE with credentials if password block
# Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
- auth = false
- auth = do_sasl_bind(bind_dn) if @@config[:try_sasl]
- auth = do_simple_bind(bind_dn) unless auth
- auth = do_anonymous_bind(bind_dn) if not auth and @@config[:allow_anonymous]
-
- unless auth
- raise AuthenticationError, "All authentication mechanisms failed"
+ if @@config[:try_sasl] and do_sasl_bind(bind_dn)
+ @@logger.info('Bound SASL')
+ elsif do_simple_bind(bind_dn)
+ @@logger.info('Bound simple')
+ elsif @@config[:allow_anonymous] and do_anonymous_bind(bind_dn)
+ @@logger.info('Bound simple')
+ else
+ @@logger.error('Failed to bind using any available method')
+ raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0
end
- return auth
+
+ return @@conn.bound?
end
-
# Base.do_anonymous_bind
#
# Bind to LDAP with the given DN, but with no password. (anonymous!)
def Base.do_anonymous_bind(bind_dn)
@@logger.info "Attempting anonymous authentication"
begin
@@conn.bind()
return true
- rescue
+ rescue => e
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
+ @@logger.debug "Exception: #{e.exception}"
+ @@logger.debug "Backtrace: #{e.backtrace}"
@@logger.warn "Warning: Anonymous authentication failed."
+ @@logger.warn "message: #{e.message}"
return false
end
end
# Base.do_simple_bind
#
- # Bind to LDAP with the given DN and password_block.call()
+ # Bind to LDAP with the given DN and password
def Base.do_simple_bind(bind_dn)
- return false unless @@config[:password_block].respond_to? :call
+ # Bail if we have no password or password block
+ if not @@config[:password_block].nil? and not @@config[:password].nil?
+ return false
+ end
+
+ # TODO: Give a warning to reconnect users with password clearing
+ # Get the passphrase for the first time, or anew if we aren't storing
+ password = ''
+ if not @@config[:password].nil?
+ password = @@config[:password]
+ elsif not @@config[:password_block].nil?
+ unless @@config[:password_block].respond_to?(:call)
+ @@logger.error('Skipping simple bind: ' +
+ ':password_block not nil or Proc object. Ignoring.')
+ return false
+ end
+ password = @@config[:password_block].call
+ else
+ @@logger.error('Skipping simple bind: ' +
+ ':password_block and :password options are empty.')
+ return false
+ end
+
begin
- @@conn.bind(bind_dn, @@config[:password_block].call())
- return true
- rescue
+ @@conn.bind(bind_dn, password)
+ rescue => e
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
- @@logger.warn "Warning: SIMPLE authentication failed."
+ # TODO: replace this with LDAP::err2exception()
+ if @@conn.err == LDAP::LDAP_SERVER_DOWN
+ @@logger.error "Warning: " + e.message
+ else
+ @@logger.warn "Warning: SIMPLE authentication failed."
+ end
return false
end
+ # Store the password for quick reference later
+ if @@config[:store_password]
+ @@config[:password] = password
+ elsif @@config[:store_password] == false
+ @@config[:password] = nil
+ end
+ return true
end
# Base.do_sasl_bind
#
# Bind to LDAP with the given DN using any available SASL methods
@@ -1238,9 +1276,10 @@
# Currently only GSSAPI is supported with Ruby/LDAP from
# http://caliban.org/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
# TODO: Investigate further SASL support
if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
begin
+ @@conn.sasl_quiet = @@config[:sasl_quiet] if @@config.has_key?(:sasl_quiet)
@@conn.sasl_bind(bind_dn, 'GSSAPI')
return true
rescue
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
@@logger.warn "Warning: SASL GSSAPI authentication failed."