lib/active_ldap/adapter/base.rb in activeldap-0.9.0 vs lib/active_ldap/adapter/base.rb in activeldap-0.10.0

- old
+ new

@@ -1,6 +1,9 @@ +require 'benchmark' + require 'active_ldap/schema' +require 'active_ldap/entry_attribute' require 'active_ldap/ldap_error' module ActiveLdap module Adapter class Base @@ -11,35 +14,46 @@ :retry_wait, :bind_dn, :password, :password_block, :try_sasl, :sasl_mechanisms, :sasl_quiet, :allow_anonymous, :store_password, :scope] + + @@row_even = true + + attr_reader :runtime def initialize(configuration={}) + @runtime = 0 @connection = nil @disconnected = false + @entry_attributes = {} @configuration = configuration.dup @logger = @configuration.delete(:logger) @configuration.assert_valid_keys(VALID_ADAPTER_CONFIGURATION_KEYS) VALID_ADAPTER_CONFIGURATION_KEYS.each do |name| instance_variable_set("@#{name}", configuration[name]) end end + def reset_runtime + runtime, @runtime = @runtime, 0 + runtime + end + def connect(options={}) host = options[:host] || @host port = options[:port] || @port method = ensure_method(options[:method] || @method) @disconnected = false - @connection = yield(host, port, method) + @connection, @uri, @with_start_tls = yield(host, port, method) prepare_connection(options) bind(options) end def disconnect!(options={}) return if @connection.nil? unbind(options) - @connection = nil + @connection = @uri = @with_start_tls = nil end def rebind(options={}) unbind(options) if bound? connect(options) @@ -51,24 +65,25 @@ if options.has_key?(:allow_anonymous) allow_anonymous = options[:allow_anonymous] else allow_anonymous = @allow_anonymous end + options = options.merge(:allow_anonymous => allow_anonymous) # 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 '') if try_sasl and sasl_bind(bind_dn, options) - @logger.info {_('Bound by SASL as %s') % bind_dn} + @logger.info {_('Bound to %s by SASL as %s') % [target, bind_dn]} elsif simple_bind(bind_dn, options) - @logger.info {_('Bound by simple as %s') % bind_dn} + @logger.info {_('Bound to %s by simple as %s') % [target, bind_dn]} elsif allow_anonymous and bind_as_anonymous(options) - @logger.info {_('Bound as anonymous')} + @logger.info {_('Bound to %s as anonymous') % target} else message = yield if block_given? - message ||= _('All authentication methods exhausted.') + message ||= _('All authentication methods for %s exhausted.') % target raise AuthenticationError, message end bound? end @@ -107,16 +122,13 @@ :attributes => attrs).first Schema.new(attributes) end end - def load(ldifs, options={}) - operation(options) do - ldifs.split(/(?:\r?\n){2,}/).each do |ldif| - yield(ldif) - end - end + def entry_attribute(object_classes) + @entry_attributes[object_classes.uniq.sort] ||= + EntryAttribute.new(schema, object_classes) end def search(options={}) filter = parse_filter(options[:filter]) || 'objectClass=*' attrs = options[:attributes] || [] @@ -193,30 +205,45 @@ rescue LdapError::ObjectClassViolation raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn] end end + def modify_rdn(dn, new_rdn, delete_old_rdn, new_superior, options={}) + operation(options) do + yield(dn, new_rdn, delete_old_rdn, new_superior) + end + end + + def log_info(name, runtime, info=nil) + return unless @logger + return unless @logger.debug? + message = "LDAP: #{name} (#{'%f' % runtime})" + @logger.debug(format_log_entry(message, info)) + end + private def prepare_connection(options) end def operation(options) retried = false + options = options.dup + options[:try_reconnect] = true unless options.has_key?(:try_reconnect) + try_reconnect = false begin - reconnect_if_need - try_reconnect = !options.has_key?(:try_reconnect) || - options[:try_reconnect] + reconnect_if_need(options) + try_reconnect = options[:try_reconnect] with_timeout(try_reconnect, options) do yield end - rescue Errno::EPIPE - if retried or !try_reconnect - raise - else + rescue Errno::EPIPE, ConnectionError + if try_reconnect and !retried retried = true @disconnected = true retry + else + raise end end end def need_credential_sasl_mechanism?(mechanism) @@ -251,11 +278,11 @@ def with_timeout(try_reconnect=true, options={}, &block) begin Timeout.alarm(@timeout, &block) rescue Timeout::Error => e @logger.error {_('Requested action timed out.')} - retry if try_reconnect and @retry_on_timeout and reconnect(options) + retry if @retry_on_timeout and try_reconnect and reconnect(options) @logger.error {e.message} raise TimeoutError, e.message end end @@ -288,10 +315,20 @@ return false unless bind_dn passwd = password(bind_dn, options) return false unless passwd + if passwd.empty? + if options[:allow_anonymous] + @logger.info {_("Skip simple bind with empty password.")} + return false + else + raise AuthenticationError, + _("Can't use empty password for simple bind.") + end + end + begin operation(options) do yield(bind_dn, passwd) bound? end @@ -316,28 +353,11 @@ construct_component(key, value, operator) end construct_filter(components, operator) else operator, components = normalize_array_filter(filter, operator) - - components = components.collect do |component| - if component.is_a?(Array) and component.size == 2 - key, value = component - if filter_logical_operator?(key) - parse_filter(component) - elsif value.is_a?(Hash) - parse_filter(value, key) - else - construct_component(key, value, operator) - end - elsif component.is_a?(Symbol) - assert_filter_logical_operator(component) - nil - else - parse_filter(component, operator) - end - end + components = construct_components(components, operator) construct_filter(components, operator) end end def parse_filter_string(filter) @@ -356,10 +376,11 @@ filter_operator, *components = filter if filter_logical_operator?(filter_operator) operator = filter_operator else components.unshift(filter_operator) + components = [components] unless filter_operator.is_a?(Array) end [operator, components] end def extract_filter_value_options(value) @@ -368,35 +389,66 @@ case value[0] when Hash options = value[0] value = value[1] when "=", "~=", "<=", "=>" - options[:operator] = value[1] - value = value[1] + options[:operator] = value[0] + if value.size > 2 + value = value[1..-1] + else + value = value[1] + end end end [value, options] end + def construct_components(components, operator) + components.collect do |component| + if component.is_a?(Array) + if filter_logical_operator?(component[0]) + parse_filter(component) + elsif component.size == 2 + key, value = component + if value.is_a?(Hash) + parse_filter(value, key) + else + construct_component(key, value, operator) + end + else + construct_component(component[0], component[1..-1], operator) + end + elsif component.is_a?(Symbol) + assert_filter_logical_operator(component) + nil + else + parse_filter(component, operator) + end + end + end + def construct_component(key, value, operator=nil) value, options = extract_filter_value_options(value) + comparison_operator = options[:operator] || "=" if collection?(value) + return nil if value.empty? + operator, value = normalize_array_filter(value, operator) values = [] value.each do |val| if collection?(val) - values.concat(val.collect {|v| [key, v]}) + values.concat(val.collect {|v| [key, comparison_operator, v]}) else - values << [key, val] + values << [key, comparison_operator, val] end end values[0] = values[0][1] if filter_logical_operator?(values[0][1]) parse_filter(values, operator) else [ "(", escape_filter_key(key), - options[:operator] || "=", + comparison_operator, escape_filter_value(value, options), ")" ].join end end @@ -523,9 +575,73 @@ def root_dse_values(key, options={}) dse = root_dse([key], options)[0] return [] if dse.nil? dse[key] || dse[key.downcase] || [] + end + + def root_dse(attrs, options={}) + search(:base => "", + :scope => :base, + :attributes => attrs).collect do |dn, attributes| + attributes + end + end + + def construct_uri(host, port, ssl) + protocol = ssl ? "ldaps" : "ldap" + URI.parse("#{protocol}://#{host}:#{port}").to_s + end + + def target + return nil if @uri.nil? + if @with_start_tls + "#{@uri}(StartTLS)" + else + @uri + end + end + + def log(name, info=nil) + if block_given? + if @logger and @logger.debug? + result = nil + runtime = Benchmark.realtime {result = yield} + @runtime += runtime + log_info(name, runtime, info) + result + else + yield + end + else + log_info(name, info, 0) + nil + end + rescue Exception + log_info("#{name}: FAILED", 0, + (info || {}).merge(:error => $!.class.name, + :error_message => $!.message)) + raise + end + + def format_log_entry(message, info=nil) + if ActiveLdap::Base.colorize_logging + if @@row_even + message_color, dump_color = "4;36;1", "0;1" + else + @@row_even = true + message_color, dump_color = "4;35;1", "0" + end + @@row_even = !@@row_even + + log_entry = " \e[#{message_color}m#{message}\e[0m" + log_entry << ": \e[#{dump_color}m#{info.inspect}\e[0m" if info + log_entry + else + log_entry = message + log_entry += ": #{info.inspect}" if info + log_entry + end end end end end