lib/activeldap/base.rb in ruby-activeldap-debug-0.7.1 vs lib/activeldap/base.rb in ruby-activeldap-debug-0.7.2

- old
+ new

@@ -38,11 +38,11 @@ # OO-interface to LDAP assuming pam/nss_ldap-style organization with Active specifics # Each subclass does a ldapsearch for the matching entry. # If no exact match, raise an error. # If match, change all LDAP attributes in accessor attributes on the object. # -- these are ACTUALLY populated from schema - see subschema.rb example - # -- @conn.schema().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}} + # -- @conn.schema2().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}} # -- extract objectClasses from match and populate # Multiple entries become lists. # If this isn't read-only then lists become multiple entries, etc. # AttributeEmpty @@ -92,10 +92,15 @@ # An exception raised when there is an issue assigning a value to # an attribute class AttributeAssignmentError < RuntimeError end + # TimeoutError + # + # An exception raised when a connection action fails due to a timeout + class TimeoutError < RuntimeError + end # Base # # Base is the primary class which contains all of the core # ActiveLDAP functionality. It is meant to only ever be subclassed @@ -194,21 +199,30 @@ # :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. + # :return_objects - indicates whether find/find_all will return objects or just the distinguished name attribute value of the matches + # :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned. + # :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true # 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 = DEFAULT_CONFIG.dup config.keys.each do |key| - if key == :base + case key + when :base # Scrub before inserting base = config[:base].gsub(/['}{#]/, '') Base.class_eval("def Base.base();'#{base}';end") + when :ldap_scope + if config[:ldap_scope].class != Fixnum + raise ConfigurationError, ':ldap_scope must be a Fixnum' + end + Base.class_eval("def Base.ldap_scope();#{config[:ldap_scope]};end") else @@config[key] = config[key] end end # Assign a easier name for the logger @@ -240,62 +254,130 @@ rescue # Doesn't matter. end @@conn = nil # Make sure it is cleaned up - ObjectSpace.garbage_collect + # This causes Ruby/LDAP memory corruption. + # ObjectSpace.garbage_collect end # Return the LDAP connection object currently in use - def Base.connection + # Alternately execute a command against the connection + # object "safely" using a given block. Use the given + # "errmsg" for any error conditions. + def Base.connection(exc=RuntimeError.new('unknown error'), try_reconnect = true) + # Block was given! Let's safely provide access. + if block_given? + begin + Timeout.alarm(@@config[:timeout]) do + begin + yield @@conn + rescue => e + # Raise an LDAP error instead of RuntimeError or whatever + @@logger.debug("Converting #{e} to useful exception") + raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0 + # Else reraise + @@logger.debug('Reraising') + raise e + end + end + rescue Timeout::Error => e + @@logger.error('Requested action timed out.') + retry if try_reconnect and @@config[:retry_on_timeout] and Base.reconnect() + message = e.message + message = exc.message unless exc.nil? + @@logger.error(message) + raise TimeoutError, message + rescue RuntimeError => e + @@logger.error("#{e.class} exception occurred in connection block") + @@logger.error("Exception message: #{e.message}") + @@logger.error("Exception backtrace: #{e.backtrace}") + @@logger.error(exc.message) unless exc.nil? + retry if try_reconnect and Base.reconnect() + raise exc unless exc.nil? + return nil + rescue LDAP::ServerDown => e + @@logger.error("#{e.class} exception occurred in connection block") + @@logger.error("Exception message: #{e.message}") + @@logger.error("Exception backtrace: #{e.backtrace}") + @@logger.error(exc.message) unless exc.nil? + retry if try_reconnect and Base.reconnect() + raise exc unless exc.nil? + return nil + rescue LDAP::ResultError => e + @@logger.error("#{e.class} exception occurred in connection block") + @@logger.error("Exception message: #{e.message}") + @@logger.error("Exception backtrace: #{e.backtrace}") + @@logger.error(exc.message) unless exc.nil? + retry if try_reconnect and Base.reconnect() + raise exc unless exc.nil? + return nil + rescue LDAP::UndefinedType => e + @@logger.error("#{e.class} exception occurred in connection block") + @@logger.error("Exception message: #{e.message}") + @@logger.error("Exception backtrace: #{e.backtrace}") + # Do not retry - not a connection error + raise exc unless exc.nil? + return nil + # Catch all - to be remedied later + rescue => e + @@logger.error("#{e.class} exception occurred in connection block") + @@logger.error("Exception message: #{e.message}") + @@logger.error("Exception backtrace: #{e.backtrace}") + @@logger.error("Error in catch all: please send debug log to ActiveLDAP author") + @@logger.error(exc.message) unless exc.nil? + raise exc unless exc.nil? + return nil + end + end return @@conn end # Set the LDAP connection avoiding Base.connect or multiplexing connections def Base.connection=(conn) @@conn = conn end + # Determine if we have exceed the retry limit or not. + # True is reconnecting is allowed - False if not. + def Base.can_reconnect? + # Allow connect if we've never connected. + return true unless @@config + if @@reconnect_attempts < (@@config[:retries] - 1) or + @@config[:retries] < 0 + return true + end + return false + 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) + unless @@config + @@logger.error('Ignoring force: Base.reconnect called before Base.connect') if force + @@logger.debug('Base.reconnect called before Base.connect - calling Base.connect') + Base.connect + return true + end 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 + if Base.can_reconnect? @@logger.debug('Attempting to reconnect') + Base.close() # Reset the attempts if this was forced. - @@reconnect_attempts = 0 if @@reconnect_attempts != 0 + @@reconnect_attempts = 0 if force + @@reconnect_attempts += 1 if @@config[:retries] >= 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 @@ -313,49 +395,35 @@ # search # # Wraps Ruby/LDAP connection.search to make it easier to search for specific # data without cracking open Base.connection def Base.search(config={}) - unless Base.connection - if @@config - ActiveLDAP::Base.connect(@@config) - else - ActiveLDAP::Base.connect - end - end + Base.reconnect if Base.connection.nil? and Base.can_reconnect? config[:filter] = 'objectClass=*' unless config.has_key? :filter config[:attrs] = [] unless config.has_key? :attrs config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope config[:base] = base() unless config.has_key? :base values = [] config[:attrs] = config[:attrs].to_a # just in case - begin - @@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m| + result = Base.connection() do |conn| + 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| if config[:attrs].member? attr or config[:attrs].empty? res[attr] = m.vals(attr).dup end end 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. - retry if Base.reconnect() + end + if result.nil? # Do nothing on failure @@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}" - rescue => detail - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - raise detail end return values end # find @@ -363,18 +431,12 @@ # Finds the first match for value where |value| is the value of some # |field|, or the wildcard match. This is only useful for derived classes. # usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true) # Subclass.find('some*val') # - def Base.find(config = {}) - unless Base.connection - if @@config - ActiveLDAP::Base.connect(@@config) - else - ActiveLDAP::Base.connect - end - end + def Base.find(config='*') + Base.reconnect if Base.connection.nil? and Base.can_reconnect? if self.class == Class klass = self.ancestors[0].to_s.split(':').last real_klass = self.ancestors[0] else @@ -382,109 +444,74 @@ real_klass = self.class end # Allow a single string argument attr = dnattr() - objects = false + objects = @@config[:return_objects] val = config # Or a hash - if config.respond_to?"has_key?" + if config.respond_to?(:has_key?) attr = config[:attribute] || dnattr() val = config[:value] || '*' - objects = config[:objects] + objects = config[:objects] || @@config[:return_objects] end - matches = [] - - begin + Base.connection(ConnectionError.new("Failed in #{self.class}#find(#{config.inspect})")) do |conn| # Get some attributes - @@conn.search(base(), @@config[:ldap_scope], "(#{attr}=#{val})") do |m| + conn.search(base(), 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. - retry if Base.reconnect() - - # Do nothing on failure - @@logger.debug "no matches for #{attr}=#{val}" - rescue => detail - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - raise detail end + # If we're here, there were no results 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 = {}) - unless Base.connection - if @@config - ActiveLDAP::Base.connect(@@config) - else - ActiveLDAP::Base.connect - end - end + def Base.find_all(config='*') + Base.reconnect if Base.connection.nil? and Base.can_reconnect? if self.class == Class real_klass = self.ancestors[0] else real_klass = self.class end # Allow a single string argument val = config attr = dnattr() - objects = false + objects = @@config[:return_objects] # Or a hash - if config.respond_to?"has_key?" + if config.respond_to?(:has_key?) val = config[:value] || '*' attr = config[:attribute] || dnattr() - objects = config[:objects] + objects = config[:objects] || @@config[:return_objects] end matches = [] - - begin + Base.connection(ConnectionError.new("Failed in #{self.class}#find_all(#{config.inspect})")) do |conn| # Get some attributes - @@conn.search(base(), @@config[:ldap_scope], - "(#{attr}=#{val})") do |m| + conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m| # Extract the dnattr value dnval = m.dn.split(/,/)[0].split(/=/)[1] if objects matches.push(real_klass.new(m)) else matches.push(dnval) end end - rescue RuntimeError => detail - #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}" - rescue => detail - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - raise detail end return matches end private_class_method :find_all @@ -497,11 +524,11 @@ # ldap server such as 'dc=example,dc=com', and # it should be overwritten by including # configuration.rb into this class. # When subclassing, the specified prefix will be concatenated. def Base.base - 'dc=example,dc=com' + 'dc=localdomain' end # Base.dnattr # # This is a placeholder for the class method that will @@ -524,12 +551,23 @@ # arrays to occurs. def Base.required_classes [] end + # Base.ldap_scope + # + # This method when included into Base provides + # an inheritable, overwritable configuration setting + # + # This value should be the default LDAP scope behavior + # desired. + def Base.ldap_scope + LDAP::LDAP_SCOPE_ONELEVEL + end + ### All instance methods, etc # new # # Creates a new instance of Base initializing all class and all @@ -537,24 +575,16 @@ # exist for dnattr, the first one put here will be authoritative # TODO: Add # support for relative distinguished names # val can be a dn attribute value, a full DN, or a LDAP::Entry. The use # 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 - 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.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 + # Make sure we're connected + Base.reconnect if Base.connection.nil? and Base.can_reconnect? + if val.class == LDAP::Entry # Call import, which is basically initialize # without accessing LDAP. @@logger.debug "initialize: val is a LDAP::Entry - running import." import(val) @@ -567,15 +597,15 @@ @data = {} # where the r/w entry data is stored @ldap_data = {} # original ldap entry data @attr_methods = {} # list of valid method calls for attributes used for dereferencing @last_oc = false # for use in other methods for "caching" if dnattr().empty? - raise RuntimeError, "dnattr() not set for this class." + raise ConfigurationError, "dnattr() not set for this class: #{self.class}" end - # Break val apart if it is a dn - if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i) + # Extract dnattr if val looks like a dn + if val.match(/^#{dnattr()}=([^,=]+),/i) val = $1 elsif val.match(/[=,]/) @@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN." val = '' end @@ -587,21 +617,21 @@ else # Create what should be the authoritative DN @dn = "#{dnattr()}=#{val},#{base()}" # Search for the existing entry - begin + Base.connection(ConnectionError.new("Failed in #{self.class}#new(#{val.inspect})")) do |conn| # Get some attributes - Base.connection.search(base(), @@config[:ldap_scope], "(#{dnattr()}=#{val})") do |m| + conn.search(base(), ldap_scope(), "(#{dnattr()}=#{val})") do |m| @exists = true # Save DN @dn = m.dn # Load up data into tmp @@logger.debug("loading entry: #{@dn}") m.attrs.each do |attr| # Load with subtypes just like @data - @@logger.debug("calling make_subtypes for m.vals(attr).dup") + @@logger.debug('calling make_subtypes for m.vals(attr).dup') safe_attr, value = make_subtypes(attr, m.vals(attr).dup) @@logger.debug("finished make_subtypes for #{attr}") # Add subtype to any existing values if @ldap_data.has_key? safe_attr value.each do |v| @@ -610,31 +640,37 @@ else @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. - 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. if @exists + # Make sure the server uses objectClass and not objectclass + unless @ldap_data.has_key?('objectClass') + real_objc = @ldap_data.grep(/^objectclass$/i) + if real_objc.size == 1 + @ldap_data['objectClass'] = @ldap_data[real_objc] + @ldap_data.delete(real_objc) + else + raise AttributeEmpty, 'objectClass was not sent by LDAP server!' + end + end + # Populate schema data send(:apply_objectclass, @ldap_data['objectClass']) # Populate real data now that we have the schema with aliases @ldap_data.each do |pair| real_attr = @attr_methods[pair[0]] @@logger.debug("new: #{pair[0].inspect} method maps to #{real_attr}") + if real_attr.nil? + @@logger.error("Unable to resolve attribute value #{pair[0].inspect}. " + + "Unpredictable behavior likely!") + end @data[real_attr] = pair[1].dup @@logger.debug("new: #{real_attr} set to #{pair[1]}") end else send(:apply_objectclass, required_classes()) @@ -654,11 +690,11 @@ # Return attribute methods so that a program can determine available # attributes dynamically without schema awareness def attributes @@logger.debug("stub: attributes called") send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc - return @attr_methods.keys + return @attr_methods.keys.map {|x|x.downcase}.uniq end # exists? # # Return whether the entry exists in LDAP or not @@ -704,11 +740,12 @@ end # Make sure all MUST attributes have a value @data['objectClass'].each do |objc| @must.each do |req_attr| - deref = @attr_methods[req_attr] + # Downcase to ensure we catch schema problems + deref = @attr_methods[req_attr.downcase] # Set default if it wasn't yet set. @data[deref] = [] if @data[deref].nil? # Check for missing requirements. if @data[deref].empty? raise AttributeEmpty, @@ -722,24 +759,14 @@ # delete # # Delete this entry from LDAP def delete @@logger.debug("stub: delete called") - begin - @@conn.delete(@dn) + Base.connection(DeleteError.new( + "Failed to delete LDAP entry: '#{@dn}'")) do |conn| + 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. - retry if Base.reconnect() - raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'" - rescue LDAP::ResultError => detail - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'" end end # write @@ -776,17 +803,26 @@ end ldap_data[key].delete(value) end end end - @@logger.debug("#write: subtypes expanded for @ldap_data") + @@logger.debug('#write: subtypes expanded for @ldap_data') # Expand subtypes to real data entries, but leave @data alone - @@logger.debug("#write: dup'ing @data") + @@logger.debug('#write: duping @data') data = Marshal.load(Marshal.dump(@data)) - @@logger.debug("#write: finished dup'ing @data") - @@logger.debug("#write: expanding subtypes for @data") + @@logger.debug('#write: finished duping @data') + + @@logger.debug('#write: removing disallowed attributes from @data') + bad_attrs = @data.keys - (@must+@may) + bad_attrs.each do |removeme| + data.delete(removeme) + end + @@logger.debug('#write: finished removing disallowed attributes from @data') + + + @@logger.debug('#write: expanding subtypes for @data') data.keys.each do |key| data[key].each do |value| if value.class == Hash suffix, real_value = extract_subtypes(value) if data.has_key? key + suffix @@ -796,22 +832,21 @@ end data[key].delete(value) end end end - @@logger.debug("#write: subtypes expanded for @data") + @@logger.debug('#write: subtypes expanded for @data') - if @exists # Cycle through all attrs to determine action action = {} replaceable = [] # Now that all the subtypes will be treated as unique attributes # we can see what's changed and add anything that is brand-spankin' # new. - @@logger.debug("#write: traversing ldap_data determining replaces and deletes") + @@logger.debug('#write: traversing ldap_data determining replaces and deletes') ldap_data.each do |pair| suffix = '' binary = 0 name, *suffix_a = pair[0].split(/;/) @@ -839,12 +874,12 @@ @@logger.debug("removing attribute from existing entry: #{name+suffix}") entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, [])) end end end - @@logger.debug("#write: finished traversing ldap_data") - @@logger.debug("#write: traversing data determining adds") + @@logger.debug('#write: finished traversing ldap_data') + @@logger.debug('#write: traversing data determining adds') data.each do |pair| suffix = '' binary = 0 name, *suffix_a = pair[0].split(/;/) @@ -862,31 +897,19 @@ # REPLACE will function like ADD, but doesn't hit EQUALITY problems # 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") - begin + @@logger.debug('#write: traversing data complete') + Base.connection(WriteError.new( + "Failed to modify: '#{entry}'")) do |conn| @@logger.debug("#write: modifying #{@dn}") - @@conn.modify(@dn, entry) - @@logger.debug("#write: modify successful") - rescue RuntimeError => detail - #todo# check for SERVER_DOWN - # the connection may have gone stale. let's reconnect and retry. - retry if Base.reconnect() - raise WriteError, "Could not update LDAP entry: #{detail}" - rescue => detail - @@logger.debug(LDAP::err2exception(@@conn.err).inspect) - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - @@logger.debug("Failed to write: #{entry}") - raise WriteError, "Could not update LDAP entry: #{detail}" + conn.modify(@dn, entry) + @@logger.debug('#write: modify successful') end else # add everything! - @@logger.debug("#write: adding all attribute value pairs") + @@logger.debug('#write: adding all attribute value pairs') @@logger.debug("#write: adding #{@attr_methods[dnattr()].inspect} = #{data[@attr_methods[dnattr()]].inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()], data[@attr_methods[dnattr()]])) @@logger.debug("#write: adding objectClass = #{data[@attr_methods['objectClass']].inspect}") entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass', @@ -901,34 +924,30 @@ 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 - begin + Base.connection(WriteError.new( + "Failed to add: '#{entry}'")) do |conn| @@logger.debug("#write: adding #{@dn}") - @@conn.add(@dn, entry) + conn.add(@dn, entry) @@logger.debug("#write: add successful") @exists = true - rescue RuntimeError => detail - # The connection may have gone stale. Let's reconnect and retry. - retry if Base.reconnect() - raise WriteError, "Could not add LDAP entry[#{Base.connection.err2string(Base.connection.err)}]: #{detail}" - rescue LDAP::ResultError => detail - if LDAP::err2exception(@@conn.err)[0] == LDAP::ServerDown - @@logger.debug("Failed to write: #{entry}") - retry if Base.reconnect() - end - 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") - @ldap_data = Marshal.load(Marshal.dump(@data)) - @@logger.debug("#write: @ldap_data reset complete") - @@logger.debug("stub: write exitted") + @ldap_data = Marshal.load(Marshal.dump(data)) + # Delete items disallowed by objectclasses. + # They should have been removed from ldap. + @@logger.debug('#write: removing attributes from @ldap_data not sent in data') + bad_attrs.each do |removeme| + @ldap_data.delete(removeme) + end + @@logger.debug('#write: @ldap_data reset complete') + @@logger.debug('stub: write exitted') end - # method_missing # # If a given method matches an attribute or an attribute alias # then call the appropriate method. # TODO: Determine if it would be better to define each allowed method @@ -968,11 +987,11 @@ def methods return __methods + attributes() end - private + private # import(LDAP::Entry) # # Overwrites an existing entry (usually called by new) # with the data given in the data given in LDAP::Entry. @@ -1070,18 +1089,10 @@ @may.uniq! (@must+@may).each do |attr| # Update attr_method with appropriate define_attribute_methods(attr) end - - # Delete all now innew_ocid attributes given the new objectClasses - @data.keys.each do |key| - # If it's not a proper aliased attribute, drop it - unless @attr_methods.has_key? key - @data.delete(key) - end - end end # Enforce typing: @@ -1196,25 +1207,22 @@ @@logger.error("Failed to connect using #{@@config[:method]}") raise e end # Enforce LDAPv3 - @@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) + Base.connection(nil, false) do |conn| + conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) + 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." + exc = ConnectionError.new("Unable to retrieve schema from server (#{@@config[:method]})") + Base.connection(exc, false) do |conn| + @@schema = @@conn.schema2() if @@schema.nil? end - @@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]} using #{@@config[:method]}" end # Wrapper all bind activity def Base.do_bind() @@ -1226,43 +1234,39 @@ 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') + @@logger.info('Bound anonymous') else - @@logger.error('Failed to bind using any available method') raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0 + raise AuthenticationError, 'All authentication methods exhausted.' end return @@conn.bound? end + private_class_method :do_bind # 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() + Base.connection(nil, false) do |conn| + conn.bind() return true - 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 + return false end + private_class_method :do_anonymous_bind # Base.do_simple_bind # # Bind to LDAP with the given DN and password def Base.do_simple_bind(bind_dn) # Bail if we have no password or password block - if not @@config[:password_block].nil? and not @@config[:password].nil? + if @@config[:password_block].nil? and @@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 @@ -1280,64 +1284,69 @@ @@logger.error('Skipping simple bind: ' + ':password_block and :password options are empty.') return false end - begin - @@conn.bind(bind_dn, password) - rescue => e - @@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}" - # 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 + + Base.connection(nil, false) do |conn| + conn.bind(bind_dn, password) + return true + end + return false end + private_class_method :do_simple_bind # Base.do_sasl_bind # # Bind to LDAP with the given DN using any available SASL methods def Base.do_sasl_bind(bind_dn) # Get all SASL mechanisms - mechanisms = @@conn.root_dse[0]['supportedSASLMechanisms'] + # + mechanisms = [] + exc = ConnectionError.new('Root DSE query failed') + Base.connection(exc, false) do |conn| + mechanisms = conn.root_dse[0]['supportedSASLMechanisms'] + end # Use GSSAPI if available # 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') + Base.connection(nil, false) do |conn| + 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." - return false end end return false end + private_class_method :do_sasl_bind # base # # Returns the value of self.class.base # This is just syntactic sugar def base @@logger.debug("stub: called base") self.class.base end + # ldap_scope + # + # Returns the value of self.class.ldap_scope + # This is just syntactic sugar + def ldap_scope + @@logger.debug("stub: called ldap_scope") + self.class.ldap_scope + end + # required_classes # # Returns the value of self.class.required_classes # This is just syntactic sugar def required_classes @@ -1417,9 +1426,11 @@ end aliases = Base.schema.attribute_aliases(attr) aliases.each do |ali| @@logger.debug("associating #{ali} --> #{attr}") @attr_methods[ali] = attr + @@logger.debug("associating #{ali.downcase} --> #{attr}") + @attr_methods[ali.downcase] = attr end @@logger.debug("stub: leaving define_attribute_methods(#{attr.inspect})") end # array_of