| Class | ActiveLdap::Adapter::Base |
| In: |
lib/active_ldap/adapter/base.rb
lib/active_ldap/adapter/jndi.rb lib/active_ldap/adapter/ldap.rb lib/active_ldap/adapter/net_ldap.rb |
| Parent: | Object |
| VALID_ADAPTER_CONFIGURATION_KEYS | = | [:host, :port, :method, :timeout, :retry_on_timeout, :retry_limit, :retry_wait, :bind_dn, :password, :password_block, :try_sasl, :sasl_mechanisms, :sasl_quiet, :allow_anonymous, :store_password, :scope, :sasl_options] |
| LOGICAL_OPERATORS | = | [:and, :or, :not, :&, :|] |
| runtime | [R] |
# File lib/active_ldap/adapter/jndi.rb, line 7
7: def jndi_connection(options)
8: require 'active_ldap/adapter/jndi_connection'
9: Jndi.new(options)
10: end
# File lib/active_ldap/adapter/ldap.rb, line 7
7: def ldap_connection(options)
8: require 'active_ldap/adapter/ldap_ext'
9: Ldap.new(options)
10: end
# File lib/active_ldap/adapter/net_ldap.rb, line 9
9: def net_ldap_connection(options)
10: require 'active_ldap/adapter/net_ldap_ext'
11: NetLdap.new(options)
12: end
# File lib/active_ldap/adapter/base.rb, line 23
23: def initialize(configuration={})
24: @runtime = 0
25: @connection = nil
26: @disconnected = false
27: @bound = false
28: @bind_tried = false
29: @entry_attributes = {}
30: @configuration = configuration.dup
31: @logger = @configuration.delete(:logger)
32: @configuration.assert_valid_keys(VALID_ADAPTER_CONFIGURATION_KEYS)
33: VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
34: instance_variable_set("@#{name}", configuration[name])
35: end
36: end
# File lib/active_ldap/adapter/base.rb, line 197
197: def add(dn, entries, options={})
198: dn = ensure_dn_string(dn)
199: begin
200: operation(options) do
201: yield(dn, entries)
202: end
203: rescue LdapError::NoSuchObject
204: raise EntryNotFound, _("No such entry: %s") % dn
205: rescue LdapError::InvalidDnSyntax
206: raise DistinguishedNameInvalid.new(dn)
207: rescue LdapError::AlreadyExists
208: raise EntryAlreadyExist, _("%s: %s") % [$!.message, dn]
209: rescue LdapError::StrongAuthRequired
210: raise StrongAuthenticationRequired, _("%s: %s") % [$!.message, dn]
211: rescue LdapError::ObjectClassViolation
212: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
213: rescue LdapError::UnwillingToPerform
214: raise OperationNotPermitted, _("%s: %s") % [$!.message, dn]
215: end
216: end
# File lib/active_ldap/adapter/base.rb, line 67
67: def bind(options={})
68: @bind_tried = true
69:
70: bind_dn = ensure_dn_string(options[:bind_dn] || @bind_dn)
71: try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl
72: if options.has_key?(:allow_anonymous)
73: allow_anonymous = options[:allow_anonymous]
74: else
75: allow_anonymous = @allow_anonymous
76: end
77: options = options.merge(:allow_anonymous => allow_anonymous)
78:
79: # Rough bind loop:
80: # Attempt 1: SASL if available
81: # Attempt 2: SIMPLE with credentials if password block
82: # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
83: if try_sasl and sasl_bind(bind_dn, options)
84: @logger.info {_('Bound to %s by SASL as %s') % [target, bind_dn]}
85: elsif simple_bind(bind_dn, options)
86: @logger.info {_('Bound to %s by simple as %s') % [target, bind_dn]}
87: elsif allow_anonymous and bind_as_anonymous(options)
88: @logger.info {_('Bound to %s as anonymous') % target}
89: else
90: message = yield if block_given?
91: message ||= _('All authentication methods for %s exhausted.') % target
92: raise AuthenticationError, message
93: end
94:
95: @bound = true
96: @bound
97: end
# File lib/active_ldap/adapter/base.rb, line 104
104: def bind_as_anonymous(options={})
105: yield
106: end
# File lib/active_ldap/adapter/base.rb, line 112
112: def bound?
113: connecting? and @bound
114: end
# File lib/active_ldap/adapter/base.rb, line 43
43: def connect(options={})
44: host = options[:host] || @host
45: method = options[:method] || @method || :plain
46: port = options[:port] || @port || ensure_port(method)
47: method = ensure_method(method)
48: @disconnected = false
49: @bound = false
50: @bind_tried = false
51: @connection, @uri, @with_start_tls = yield(host, port, method)
52: prepare_connection(options)
53: bind(options)
54: end
# File lib/active_ldap/adapter/base.rb, line 108
108: def connecting?
109: !@connection.nil? and !@disconnected
110: end
# File lib/active_ldap/adapter/base.rb, line 178
178: def delete(targets, options={})
179: targets = [targets] unless targets.is_a?(Array)
180: return if targets.empty?
181: begin
182: operation(options) do
183: targets.each do |target|
184: target = ensure_dn_string(target)
185: begin
186: yield(target)
187: rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
188: raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
189: end
190: end
191: end
192: rescue LdapError::NoSuchObject
193: raise EntryNotFound, _("No such entry: %s") % target
194: end
195: end
# File lib/active_ldap/adapter/base.rb, line 56
56: def disconnect!(options={})
57: unbind(options)
58: @connection = @uri = @with_start_tls = nil
59: @disconnected = true
60: end
# File lib/active_ldap/adapter/base.rb, line 150
150: def entry_attribute(object_classes)
151: @entry_attributes[object_classes.uniq.sort] ||=
152: EntryAttribute.new(schema, object_classes)
153: end
# File lib/active_ldap/adapter/base.rb, line 242
242: def log_info(name, runtime_in_seconds, info=nil)
243: return unless @logger
244: return unless @logger.debug?
245: message = "LDAP: #{name} (#{'%.1f' % (runtime_in_seconds * 1000)}ms)"
246: @logger.debug(format_log_entry(message, info))
247: end
# File lib/active_ldap/adapter/base.rb, line 218
218: def modify(dn, entries, options={})
219: dn = ensure_dn_string(dn)
220: begin
221: operation(options) do
222: begin
223: yield(dn, entries)
224: rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
225: raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
226: end
227: end
228: rescue LdapError::UndefinedType
229: raise
230: rescue LdapError::ObjectClassViolation
231: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
232: end
233: end
# File lib/active_ldap/adapter/base.rb, line 235
235: def modify_rdn(dn, new_rdn, delete_old_rdn, new_superior, options={})
236: dn = ensure_dn_string(dn)
237: operation(options) do
238: yield(dn, new_rdn, delete_old_rdn, new_superior)
239: end
240: end
# File lib/active_ldap/adapter/base.rb, line 146
146: def naming_contexts
147: root_dse_values('namingContexts')
148: end
# File lib/active_ldap/adapter/base.rb, line 62
62: def rebind(options={})
63: unbind(options) if bound?
64: connect(options)
65: end
# File lib/active_ldap/adapter/base.rb, line 38
38: def reset_runtime
39: runtime, @runtime = @runtime, 0
40: runtime
41: end
# File lib/active_ldap/adapter/base.rb, line 116
116: def schema(options={})
117: @schema ||= operation(options) do
118: base = options[:base]
119: attrs = options[:attributes]
120:
121: attrs ||= [
122: 'objectClasses',
123: 'attributeTypes',
124: 'matchingRules',
125: 'matchingRuleUse',
126: 'dITStructureRules',
127: 'dITContentRules',
128: 'nameForms',
129: 'ldapSyntaxes',
130: #'extendedAttributeInfo', # if we need RANGE-LOWER/UPPER.
131: ]
132: base ||= root_dse_values('subschemaSubentry', options)[0]
133: base ||= 'cn=schema'
134: schema = nil
135: search(:base => base,
136: :scope => :base,
137: :filter => '(objectClass=subschema)',
138: :attributes => attrs,
139: :limit => 1) do |dn, attributes|
140: schema = Schema.new(attributes)
141: end
142: schema || Schema.new([])
143: end
144: end
# File lib/active_ldap/adapter/base.rb, line 155
155: def search(options={})
156: filter = parse_filter(options[:filter]) || 'objectClass=*'
157: attrs = options[:attributes] || []
158: scope = ensure_scope(options[:scope] || @scope)
159: base = options[:base]
160: limit = options[:limit] || 0
161: limit = nil if limit <= 0
162:
163: attrs = attrs.to_a # just in case
164: base = ensure_dn_string(base)
165: begin
166: operation(options) do
167: yield(base, scope, filter, attrs, limit)
168: end
169: rescue LdapError::NoSuchObject, LdapError::InvalidDnSyntax
170: # Do nothing on failure
171: @logger.info do
172: args = [$!.class, $!.message, filter, attrs.inspect]
173: _("Ignore error %s(%s): filter %s: attributes: %s") % args
174: end
175: end
176: end
# File lib/active_ldap/adapter/base.rb, line 99
99: def unbind(options={})
100: yield if @connection and (@bind_tried or bound?)
101: @bind_tried = @bound = false
102: end
# File lib/active_ldap/adapter/base.rb, line 555
555: def assert_filter_logical_operator(operator)
556: return if operator.nil?
557: unless filter_logical_operator?(operator)
558: raise ArgumentError,
559: _("invalid logical operator: %s: available operators: %s") %
560: [operator.inspect, LOGICAL_OPERATORS.inspect]
561: end
562: end
Determine if we have exceed the retry limit or not. True is reconnecting is allowed - False if not.
# File lib/active_ldap/adapter/base.rb, line 616
616: def can_reconnect?(options={})
617: retry_limit = options[:retry_limit] || @retry_limit
618: reconnect_attempts = options[:reconnect_attempts] || 0
619:
620: retry_limit < 0 or reconnect_attempts <= retry_limit
621: end
# File lib/active_ldap/adapter/base.rb, line 534
534: def collection?(object)
535: !object.is_a?(String) and object.respond_to?(:each)
536: end
# File lib/active_ldap/adapter/base.rb, line 467
467: def construct_component(key, value, operator=nil)
468: value, options = extract_filter_value_options(value)
469: comparison_operator = options[:operator] || "="
470: if collection?(value)
471: return nil if value.empty?
472: operator, value = normalize_array_filter(value, operator)
473: values = []
474: value.each do |val|
475: if collection?(val)
476: values.concat(val.collect {|v| [key, comparison_operator, v]})
477: else
478: values << [key, comparison_operator, val]
479: end
480: end
481: values[0] = values[0][1] if filter_logical_operator?(values[0][1])
482: parse_filter(values, operator)
483: else
484: [
485: "(",
486: escape_filter_key(key),
487: comparison_operator,
488: escape_filter_value(value, options),
489: ")"
490: ].join
491: end
492: end
# File lib/active_ldap/adapter/base.rb, line 443
443: def construct_components(components, operator)
444: components.collect do |component|
445: if component.is_a?(Array)
446: if filter_logical_operator?(component[0])
447: parse_filter(component)
448: elsif component.size == 2
449: key, value = component
450: if value.is_a?(Hash)
451: parse_filter(value, key)
452: else
453: construct_component(key, value, operator)
454: end
455: else
456: construct_component(component[0], component[1..-1], operator)
457: end
458: elsif component.is_a?(Symbol)
459: assert_filter_logical_operator(component)
460: nil
461: else
462: parse_filter(component, operator)
463: end
464: end
465: end
# File lib/active_ldap/adapter/base.rb, line 519
519: def construct_filter(components, operator=nil)
520: operator = normalize_filter_logical_operator(operator)
521: components = components.compact
522: case components.size
523: when 0
524: nil
525: when 1
526: filter = components[0]
527: filter = "(!#{filter})" if operator == :not
528: filter
529: else
530: "(#{operator == :and ? '&' : '|'}#{components.join})"
531: end
532: end
# File lib/active_ldap/adapter/base.rb, line 644
644: def construct_uri(host, port, ssl)
645: protocol = ssl ? "ldaps" : "ldap"
646: URI.parse("#{protocol}://#{host}:#{port}").to_s
647: end
# File lib/active_ldap/adapter/base.rb, line 331
331: def do_in_timeout(timeout, &block)
332: Timeout.alarm(timeout, &block)
333: end
# File lib/active_ldap/adapter/base.rb, line 696
696: def ensure_dn_string(dn)
697: if dn.is_a?(DN)
698: dn.to_s
699: else
700: dn
701: end
702: end
# File lib/active_ldap/adapter/base.rb, line 250
250: def ensure_port(method)
251: if method == :ssl
252: URI::LDAPS::DEFAULT_PORT
253: else
254: URI::LDAP::DEFAULT_PORT
255: end
256: end
# File lib/active_ldap/adapter/base.rb, line 494
494: def escape_filter_key(key)
495: escape_filter_value(key.to_s)
496: end
# File lib/active_ldap/adapter/base.rb, line 498
498: def escape_filter_value(value, options={})
499: case value
500: when Numeric, DN
501: value = value.to_s
502: when Time
503: value = Schema::GeneralizedTime.new.normalize_value(value)
504: end
505: value.gsub(/(?:[:()\\\0]|\*\*?)/) do |s|
506: if s == "*"
507: s
508: else
509: s = "*" if s == "**"
510: if s.respond_to?(:getbyte)
511: "\\%02X" % s.getbyte(0)
512: else
513: "\\%02X" % s[0]
514: end
515: end
516: end
517: end
# File lib/active_ldap/adapter/base.rb, line 424
424: def extract_filter_value_options(value)
425: options = {}
426: if value.is_a?(Array)
427: case value[0]
428: when Hash
429: options = value[0]
430: value = value[1]
431: when "=", "~=", "<=", ">="
432: options[:operator] = value[0]
433: if value.size > 2
434: value = value[1..-1]
435: else
436: value = value[1]
437: end
438: end
439: end
440: [value, options]
441: end
# File lib/active_ldap/adapter/base.rb, line 539
539: def filter_logical_operator?(operator)
540: LOGICAL_OPERATORS.include?(operator)
541: end
# File lib/active_ldap/adapter/base.rb, line 676
676: def format_log_entry(message, info=nil)
677: if ActiveLdap::Base.colorize_logging
678: if @@row_even
679: message_color, dump_color = "4;36;1", "0;1"
680: else
681: @@row_even = true
682: message_color, dump_color = "4;35;1", "0"
683: end
684: @@row_even = !@@row_even
685:
686: log_entry = " \e[#{message_color}m#{message}\e[0m"
687: log_entry << ": \e[#{dump_color}m#{info.inspect}\e[0m" if info
688: log_entry
689: else
690: log_entry = message
691: log_entry += ": #{info.inspect}" if info
692: log_entry
693: end
694: end
# File lib/active_ldap/adapter/base.rb, line 658
658: def log(name, info=nil)
659: if block_given?
660: result = nil
661: seconds = Benchmark.realtime {result = yield}
662: @runtime += seconds
663: log_info(name, seconds, info)
664: result
665: else
666: log_info(name, 0, info)
667: nil
668: end
669: rescue Exception
670: log_info("#{name}: FAILED", 0,
671: (info || {}).merge(:error => $!.class.name,
672: :error_message => $!.message))
673: raise
674: end
# File lib/active_ldap/adapter/base.rb, line 283
283: def need_credential_sasl_mechanism?(mechanism)
284: not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism)
285: end
# File lib/active_ldap/adapter/base.rb, line 413
413: def normalize_array_filter(filter, operator=nil)
414: filter_operator, *components = filter
415: if filter_logical_operator?(filter_operator)
416: operator = filter_operator
417: else
418: components.unshift(filter_operator)
419: components = [components] unless filter_operator.is_a?(Array)
420: end
421: [operator, components]
422: end
# File lib/active_ldap/adapter/base.rb, line 543
543: def normalize_filter_logical_operator(operator)
544: assert_filter_logical_operator(operator)
545: case (operator || :and)
546: when :and, :&
547: :and
548: when :or, :|
549: :or
550: else
551: :not
552: end
553: end
# File lib/active_ldap/adapter/base.rb, line 261
261: def operation(options)
262: retried = false
263: options = options.dup
264: options[:try_reconnect] = true unless options.has_key?(:try_reconnect)
265: try_reconnect = false
266: begin
267: reconnect_if_need(options)
268: try_reconnect = options[:try_reconnect]
269: with_timeout(try_reconnect, options) do
270: yield
271: end
272: rescue ConnectionError
273: if try_reconnect and !retried
274: retried = true
275: @disconnected = true
276: retry
277: else
278: raise
279: end
280: end
281: end
# File lib/active_ldap/adapter/base.rb, line 380
380: def parse_filter(filter, operator=nil)
381: return nil if filter.nil?
382: if !filter.is_a?(String) and !filter.respond_to?(:collect)
383: filter = filter.to_s
384: end
385:
386: case filter
387: when String
388: parse_filter_string(filter)
389: when Hash
390: components = filter.sort_by {|k, v| k.to_s}.collect do |key, value|
391: construct_component(key, value, operator)
392: end
393: construct_filter(components, operator)
394: else
395: operator, components = normalize_array_filter(filter, operator)
396: components = construct_components(components, operator)
397: construct_filter(components, operator)
398: end
399: end
# File lib/active_ldap/adapter/base.rb, line 401
401: def parse_filter_string(filter)
402: if /\A\s*\z/.match(filter)
403: nil
404: else
405: if filter[0, 1] == "("
406: filter
407: else
408: "(#{filter})"
409: end
410: end
411: end
# File lib/active_ldap/adapter/base.rb, line 287
287: def password(bind_dn, options={})
288: passwd = options[:password] || @password
289: return passwd if passwd
290:
291: password_block = options[:password_block] || @password_block
292: # TODO: Give a warning to reconnect users with password clearing
293: # Get the passphrase for the first time, or anew if we aren't storing
294: if password_block.respond_to?(:call)
295: passwd = password_block.call(bind_dn)
296: else
297: @logger.error {_('password_block not nil or Proc object. Ignoring.')}
298: return nil
299: end
300:
301: # Store the password for quick reference later
302: if options.has_key?(:store_password)
303: store_password = options[:store_password]
304: else
305: store_password = @store_password
306: end
307: @password = store_password ? passwd : nil
308:
309: passwd
310: end
Attempts to reconnect up to the number of times allowed If forced, try once then fail with ConnectionError if not connected.
# File lib/active_ldap/adapter/base.rb, line 566
566: def reconnect(options={})
567: options = options.dup
568: force = options[:force]
569: retry_limit = options[:retry_limit] || @retry_limit
570: retry_wait = options[:retry_wait] || @retry_wait
571: options[:reconnect_attempts] ||= 0
572:
573: loop do
574: @logger.debug {_('Attempting to reconnect')}
575: disconnect!
576:
577: # Reset the attempts if this was forced.
578: options[:reconnect_attempts] = 0 if force
579: options[:reconnect_attempts] += 1 if retry_limit >= 0
580: begin
581: connect(options)
582: break
583: rescue AuthenticationError
584: raise
585: rescue => detail
586: @logger.error do
587: _("Reconnect to server failed: %s\n" \
588: "Reconnect to server failed backtrace:\n" \
589: "%s") % [detail.exception, detail.backtrace.join("\n")]
590: end
591: # Do not loop if forced
592: raise ConnectionError, detail.message if force
593: end
594:
595: unless can_reconnect?(options)
596: raise ConnectionError,
597: _('Giving up trying to reconnect to LDAP server.')
598: end
599:
600: # Sleep before looping
601: sleep retry_wait
602: end
603:
604: true
605: end
# File lib/active_ldap/adapter/base.rb, line 607
607: def reconnect_if_need(options={})
608: return if connecting?
609: with_timeout(false, options) do
610: reconnect(options)
611: end
612: end
# File lib/active_ldap/adapter/base.rb, line 633
633: def root_dse(attrs, options={})
634: found_attributes = nil
635: search(:base => "",
636: :scope => :base,
637: :attributes => attrs,
638: :limit => 1) do |dn, attributes|
639: found_attributes = attributes
640: end
641: found_attributes
642: end
# File lib/active_ldap/adapter/base.rb, line 623
623: def root_dse_values(key, options={})
624: dse = root_dse([key], options)
625: return [] if dse.nil?
626: normalized_key = key.downcase
627: dse.each do |_key, _value|
628: return _value if _key.downcase == normalized_key
629: end
630: []
631: end
# File lib/active_ldap/adapter/base.rb, line 335
335: def sasl_bind(bind_dn, options={})
336: # Get all SASL mechanisms
337: mechanisms = operation(options) do
338: root_dse_values("supportedSASLMechanisms")
339: end
340:
341: if options.has_key?(:sasl_quiet)
342: sasl_quiet = options[:sasl_quiet]
343: else
344: sasl_quiet = @sasl_quiet
345: end
346:
347: sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
348: sasl_mechanisms.each do |mechanism|
349: next unless mechanisms.include?(mechanism)
350: return true if yield(bind_dn, mechanism, sasl_quiet)
351: end
352: false
353: end
# File lib/active_ldap/adapter/base.rb, line 355
355: def simple_bind(bind_dn, options={})
356: return false unless bind_dn
357:
358: passwd = password(bind_dn, options)
359: return false unless passwd
360:
361: if passwd.empty?
362: if options[:allow_anonymous]
363: @logger.info {_("Skip simple bind with empty password.")}
364: return false
365: else
366: raise AuthenticationError,
367: _("Can't use empty password for simple bind.")
368: end
369: end
370:
371: begin
372: yield(bind_dn, passwd)
373: rescue LdapError::InvalidDnSyntax
374: raise DistinguishedNameInvalid.new(bind_dn)
375: rescue LdapError::InvalidCredentials
376: false
377: end
378: end
# File lib/active_ldap/adapter/base.rb, line 649
649: def target
650: return nil if @uri.nil?
651: if @with_start_tls
652: "#{@uri}(StartTLS)"
653: else
654: @uri
655: end
656: end
# File lib/active_ldap/adapter/base.rb, line 312
312: def with_timeout(try_reconnect=true, options={}, &block)
313: n_retries = 0
314: retry_limit = options[:retry_limit] || @retry_limit
315: begin
316: do_in_timeout(@timeout, &block)
317: rescue Timeout::Error => e
318: @logger.error {_('Requested action timed out.')}
319: if @retry_on_timeout and retry_limit < 0 and n_retries <= retry_limit
320: if connecting?
321: retry
322: elsif try_reconnect
323: retry if with_timeout(false, options) {reconnect(options)}
324: end
325: end
326: @logger.error {e.message}
327: raise TimeoutError, e.message
328: end
329: end