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