| Class | ActiveLdap::Adapter::Base |
| In: |
lib/active_ldap/adapter/ldap.rb
lib/active_ldap/adapter/net_ldap.rb lib/active_ldap/adapter/jndi.rb lib/active_ldap/adapter/base.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 202
202: def add(dn, entries, options={})
203: dn = ensure_dn_string(dn)
204: begin
205: operation(options) do
206: yield(dn, entries)
207: end
208: rescue LdapError::NoSuchObject
209: raise EntryNotFound, _("No such entry: %s") % dn
210: rescue LdapError::InvalidDnSyntax
211: raise DistinguishedNameInvalid.new(dn)
212: rescue LdapError::AlreadyExists
213: raise EntryAlreadyExist, _("%s: %s") % [$!.message, dn]
214: rescue LdapError::StrongAuthRequired
215: raise StrongAuthenticationRequired, _("%s: %s") % [$!.message, dn]
216: rescue LdapError::ObjectClassViolation
217: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
218: rescue LdapError::UnwillingToPerform
219: raise OperationNotPermitted, _("%s: %s") % [$!.message, dn]
220: end
221: 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 183
183: def delete(targets, options={})
184: targets = [targets] unless targets.is_a?(Array)
185: return if targets.empty?
186: begin
187: operation(options) do
188: targets.each do |target|
189: target = ensure_dn_string(target)
190: begin
191: yield(target)
192: rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
193: raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
194: end
195: end
196: end
197: rescue LdapError::NoSuchObject
198: raise EntryNotFound, _("No such entry: %s") % target
199: end
200: 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 146
146: def entry_attribute(object_classes)
147: @entry_attributes[object_classes.uniq.sort] ||=
148: EntryAttribute.new(schema, object_classes)
149: end
# File lib/active_ldap/adapter/base.rb, line 247
247: def log_info(name, runtime_in_seconds, info=nil)
248: return unless @logger
249: return unless @logger.debug?
250: message = "LDAP: #{name} (#{'%.1f' % (runtime_in_seconds * 1000)}ms)"
251: @logger.debug(format_log_entry(message, info))
252: end
# File lib/active_ldap/adapter/base.rb, line 223
223: def modify(dn, entries, options={})
224: dn = ensure_dn_string(dn)
225: begin
226: operation(options) do
227: begin
228: yield(dn, entries)
229: rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
230: raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
231: end
232: end
233: rescue LdapError::UndefinedType
234: raise
235: rescue LdapError::ObjectClassViolation
236: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
237: end
238: end
# File lib/active_ldap/adapter/base.rb, line 240
240: def modify_rdn(dn, new_rdn, delete_old_rdn, new_superior, options={})
241: dn = ensure_dn_string(dn)
242: operation(options) do
243: yield(dn, new_rdn, delete_old_rdn, new_superior)
244: end
245: end
# File lib/active_ldap/adapter/base.rb, line 142
142: def naming_contexts
143: root_dse_values('namingContexts')
144: 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: dn, attributes = search(:base => base,
135: :scope => :base,
136: :filter => '(objectClass=subschema)',
137: :attributes => attrs).first
138: Schema.new(attributes)
139: end
140: end
# File lib/active_ldap/adapter/base.rb, line 151
151: def search(options={})
152: filter = parse_filter(options[:filter]) || 'objectClass=*'
153: attrs = options[:attributes] || []
154: scope = ensure_scope(options[:scope] || @scope)
155: base = options[:base]
156: limit = options[:limit] || 0
157: limit = nil if limit <= 0
158:
159: attrs = attrs.to_a # just in case
160:
161: values = []
162: callback = Proc.new do |value, block|
163: value = block.call(value) if block
164: values << value
165: end
166:
167: base = ensure_dn_string(base)
168: begin
169: operation(options) do
170: yield(base, scope, filter, attrs, limit, callback)
171: end
172: rescue LdapError::NoSuchObject, LdapError::InvalidDnSyntax
173: # Do nothing on failure
174: @logger.info do
175: args = [$!.class, $!.message, filter, attrs.inspect]
176: _("Ignore error %s(%s): filter %s: attributes: %s") % args
177: end
178: end
179:
180: values
181: 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 556
556: def assert_filter_logical_operator(operator)
557: return if operator.nil?
558: unless filter_logical_operator?(operator)
559: raise ArgumentError,
560: _("invalid logical operator: %s: available operators: %s") %
561: [operator.inspect, LOGICAL_OPERATORS.inspect]
562: end
563: 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 617
617: def can_reconnect?(options={})
618: retry_limit = options[:retry_limit] || @retry_limit
619: reconnect_attempts = options[:reconnect_attempts] || 0
620:
621: retry_limit < 0 or reconnect_attempts <= retry_limit
622: end
# File lib/active_ldap/adapter/base.rb, line 535
535: def collection?(object)
536: !object.is_a?(String) and object.respond_to?(:each)
537: end
# File lib/active_ldap/adapter/base.rb, line 468
468: def construct_component(key, value, operator=nil)
469: value, options = extract_filter_value_options(value)
470: comparison_operator = options[:operator] || "="
471: if collection?(value)
472: return nil if value.empty?
473: operator, value = normalize_array_filter(value, operator)
474: values = []
475: value.each do |val|
476: if collection?(val)
477: values.concat(val.collect {|v| [key, comparison_operator, v]})
478: else
479: values << [key, comparison_operator, val]
480: end
481: end
482: values[0] = values[0][1] if filter_logical_operator?(values[0][1])
483: parse_filter(values, operator)
484: else
485: [
486: "(",
487: escape_filter_key(key),
488: comparison_operator,
489: escape_filter_value(value, options),
490: ")"
491: ].join
492: end
493: end
# File lib/active_ldap/adapter/base.rb, line 444
444: def construct_components(components, operator)
445: components.collect do |component|
446: if component.is_a?(Array)
447: if filter_logical_operator?(component[0])
448: parse_filter(component)
449: elsif component.size == 2
450: key, value = component
451: if value.is_a?(Hash)
452: parse_filter(value, key)
453: else
454: construct_component(key, value, operator)
455: end
456: else
457: construct_component(component[0], component[1..-1], operator)
458: end
459: elsif component.is_a?(Symbol)
460: assert_filter_logical_operator(component)
461: nil
462: else
463: parse_filter(component, operator)
464: end
465: end
466: end
# File lib/active_ldap/adapter/base.rb, line 520
520: def construct_filter(components, operator=nil)
521: operator = normalize_filter_logical_operator(operator)
522: components = components.compact
523: case components.size
524: when 0
525: nil
526: when 1
527: filter = components[0]
528: filter = "(!#{filter})" if operator == :not
529: filter
530: else
531: "(#{operator == :and ? '&' : '|'}#{components.join})"
532: end
533: end
# File lib/active_ldap/adapter/base.rb, line 642
642: def construct_uri(host, port, ssl)
643: protocol = ssl ? "ldaps" : "ldap"
644: URI.parse("#{protocol}://#{host}:#{port}").to_s
645: end
# File lib/active_ldap/adapter/base.rb, line 694
694: def ensure_dn_string(dn)
695: if dn.is_a?(DN)
696: dn.to_s
697: else
698: dn
699: end
700: end
# File lib/active_ldap/adapter/base.rb, line 255
255: def ensure_port(method)
256: if method == :ssl
257: URI::LDAPS::DEFAULT_PORT
258: else
259: URI::LDAP::DEFAULT_PORT
260: end
261: end
# File lib/active_ldap/adapter/base.rb, line 495
495: def escape_filter_key(key)
496: escape_filter_value(key.to_s)
497: end
# File lib/active_ldap/adapter/base.rb, line 499
499: def escape_filter_value(value, options={})
500: case value
501: when Numeric, DN
502: value = value.to_s
503: when Time
504: value = Schema::GeneralizedTime.new.normalize_value(value)
505: end
506: value.gsub(/(?:[()\\\0]|\*\*?)/) do |s|
507: if s == "*"
508: s
509: else
510: s = "*" if s == "**"
511: if s.respond_to?(:getbyte)
512: "\\%02X" % s.getbyte(0)
513: else
514: "\\%02X" % s[0]
515: end
516: end
517: end
518: end
# File lib/active_ldap/adapter/base.rb, line 425
425: def extract_filter_value_options(value)
426: options = {}
427: if value.is_a?(Array)
428: case value[0]
429: when Hash
430: options = value[0]
431: value = value[1]
432: when "=", "~=", "<=", ">="
433: options[:operator] = value[0]
434: if value.size > 2
435: value = value[1..-1]
436: else
437: value = value[1]
438: end
439: end
440: end
441: [value, options]
442: end
# File lib/active_ldap/adapter/base.rb, line 540
540: def filter_logical_operator?(operator)
541: LOGICAL_OPERATORS.include?(operator)
542: end
# File lib/active_ldap/adapter/base.rb, line 674
674: def format_log_entry(message, info=nil)
675: if ActiveLdap::Base.colorize_logging
676: if @@row_even
677: message_color, dump_color = "4;36;1", "0;1"
678: else
679: @@row_even = true
680: message_color, dump_color = "4;35;1", "0"
681: end
682: @@row_even = !@@row_even
683:
684: log_entry = " \e[#{message_color}m#{message}\e[0m"
685: log_entry << ": \e[#{dump_color}m#{info.inspect}\e[0m" if info
686: log_entry
687: else
688: log_entry = message
689: log_entry += ": #{info.inspect}" if info
690: log_entry
691: end
692: end
# File lib/active_ldap/adapter/base.rb, line 656
656: def log(name, info=nil)
657: if block_given?
658: result = nil
659: seconds = Benchmark.realtime {result = yield}
660: @runtime += seconds
661: log_info(name, seconds, info)
662: result
663: else
664: log_info(name, 0, info)
665: nil
666: end
667: rescue Exception
668: log_info("#{name}: FAILED", 0,
669: (info || {}).merge(:error => $!.class.name,
670: :error_message => $!.message))
671: raise
672: end
# File lib/active_ldap/adapter/base.rb, line 288
288: def need_credential_sasl_mechanism?(mechanism)
289: not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism)
290: end
# File lib/active_ldap/adapter/base.rb, line 414
414: def normalize_array_filter(filter, operator=nil)
415: filter_operator, *components = filter
416: if filter_logical_operator?(filter_operator)
417: operator = filter_operator
418: else
419: components.unshift(filter_operator)
420: components = [components] unless filter_operator.is_a?(Array)
421: end
422: [operator, components]
423: end
# File lib/active_ldap/adapter/base.rb, line 544
544: def normalize_filter_logical_operator(operator)
545: assert_filter_logical_operator(operator)
546: case (operator || :and)
547: when :and, :&
548: :and
549: when :or, :|
550: :or
551: else
552: :not
553: end
554: end
# File lib/active_ldap/adapter/base.rb, line 266
266: def operation(options)
267: retried = false
268: options = options.dup
269: options[:try_reconnect] = true unless options.has_key?(:try_reconnect)
270: try_reconnect = false
271: begin
272: reconnect_if_need(options)
273: try_reconnect = options[:try_reconnect]
274: with_timeout(try_reconnect, options) do
275: yield
276: end
277: rescue ConnectionError
278: if try_reconnect and !retried
279: retried = true
280: @disconnected = true
281: retry
282: else
283: raise
284: end
285: end
286: end
# File lib/active_ldap/adapter/base.rb, line 381
381: def parse_filter(filter, operator=nil)
382: return nil if filter.nil?
383: if !filter.is_a?(String) and !filter.respond_to?(:collect)
384: filter = filter.to_s
385: end
386:
387: case filter
388: when String
389: parse_filter_string(filter)
390: when Hash
391: components = filter.sort_by {|k, v| k.to_s}.collect do |key, value|
392: construct_component(key, value, operator)
393: end
394: construct_filter(components, operator)
395: else
396: operator, components = normalize_array_filter(filter, operator)
397: components = construct_components(components, operator)
398: construct_filter(components, operator)
399: end
400: end
# File lib/active_ldap/adapter/base.rb, line 402
402: def parse_filter_string(filter)
403: if /\A\s*\z/.match(filter)
404: nil
405: else
406: if filter[0, 1] == "("
407: filter
408: else
409: "(#{filter})"
410: end
411: end
412: end
# File lib/active_ldap/adapter/base.rb, line 292
292: def password(bind_dn, options={})
293: passwd = options[:password] || @password
294: return passwd if passwd
295:
296: password_block = options[:password_block] || @password_block
297: # TODO: Give a warning to reconnect users with password clearing
298: # Get the passphrase for the first time, or anew if we aren't storing
299: if password_block.respond_to?(:call)
300: passwd = password_block.call(bind_dn)
301: else
302: @logger.error {_('password_block not nil or Proc object. Ignoring.')}
303: return nil
304: end
305:
306: # Store the password for quick reference later
307: if options.has_key?(:store_password)
308: store_password = options[:store_password]
309: else
310: store_password = @store_password
311: end
312: @password = store_password ? passwd : nil
313:
314: passwd
315: 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 567
567: def reconnect(options={})
568: options = options.dup
569: force = options[:force]
570: retry_limit = options[:retry_limit] || @retry_limit
571: retry_wait = options[:retry_wait] || @retry_wait
572: options[:reconnect_attempts] ||= 0
573:
574: loop do
575: @logger.debug {_('Attempting to reconnect')}
576: disconnect!
577:
578: # Reset the attempts if this was forced.
579: options[:reconnect_attempts] = 0 if force
580: options[:reconnect_attempts] += 1 if retry_limit >= 0
581: begin
582: connect(options)
583: break
584: rescue AuthenticationError
585: raise
586: rescue => detail
587: @logger.error do
588: _("Reconnect to server failed: %s\n" \
589: "Reconnect to server failed backtrace:\n" \
590: "%s") % [detail.exception, detail.backtrace.join("\n")]
591: end
592: # Do not loop if forced
593: raise ConnectionError, detail.message if force
594: end
595:
596: unless can_reconnect?(options)
597: raise ConnectionError,
598: _('Giving up trying to reconnect to LDAP server.')
599: end
600:
601: # Sleep before looping
602: sleep retry_wait
603: end
604:
605: true
606: end
# File lib/active_ldap/adapter/base.rb, line 608
608: def reconnect_if_need(options={})
609: return if connecting?
610: with_timeout(false, options) do
611: reconnect(options)
612: end
613: end
# File lib/active_ldap/adapter/base.rb, line 634
634: def root_dse(attrs, options={})
635: search(:base => "",
636: :scope => :base,
637: :attributes => attrs).collect do |dn, attributes|
638: attributes
639: end
640: end
# File lib/active_ldap/adapter/base.rb, line 624
624: def root_dse_values(key, options={})
625: dse = root_dse([key], options)[0]
626: return [] if dse.nil?
627: normalized_key = key.downcase
628: dse.each do |_key, _value|
629: return _value if _key.downcase == normalized_key
630: end
631: []
632: end
# File lib/active_ldap/adapter/base.rb, line 336
336: def sasl_bind(bind_dn, options={})
337: # Get all SASL mechanisms
338: mechanisms = operation(options) do
339: root_dse_values("supportedSASLMechanisms")
340: end
341:
342: if options.has_key?(:sasl_quiet)
343: sasl_quiet = options[:sasl_quiet]
344: else
345: sasl_quiet = @sasl_quiet
346: end
347:
348: sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
349: sasl_mechanisms.each do |mechanism|
350: next unless mechanisms.include?(mechanism)
351: return true if yield(bind_dn, mechanism, sasl_quiet)
352: end
353: false
354: end
# File lib/active_ldap/adapter/base.rb, line 356
356: def simple_bind(bind_dn, options={})
357: return false unless bind_dn
358:
359: passwd = password(bind_dn, options)
360: return false unless passwd
361:
362: if passwd.empty?
363: if options[:allow_anonymous]
364: @logger.info {_("Skip simple bind with empty password.")}
365: return false
366: else
367: raise AuthenticationError,
368: _("Can't use empty password for simple bind.")
369: end
370: end
371:
372: begin
373: yield(bind_dn, passwd)
374: rescue LdapError::InvalidDnSyntax
375: raise DistinguishedNameInvalid.new(bind_dn)
376: rescue LdapError::InvalidCredentials
377: false
378: end
379: end
# File lib/active_ldap/adapter/base.rb, line 647
647: def target
648: return nil if @uri.nil?
649: if @with_start_tls
650: "#{@uri}(StartTLS)"
651: else
652: @uri
653: end
654: end
# File lib/active_ldap/adapter/base.rb, line 317
317: def with_timeout(try_reconnect=true, options={}, &block)
318: n_retries = 0
319: retry_limit = options[:retry_limit] || @retry_limit
320: begin
321: Timeout.alarm(@timeout, &block)
322: rescue Timeout::Error => e
323: @logger.error {_('Requested action timed out.')}
324: if @retry_on_timeout and retry_limit < 0 and n_retries <= retry_limit
325: if connecting?
326: retry
327: elsif try_reconnect
328: retry if with_timeout(false, options) {reconnect(options)}
329: end
330: end
331: @logger.error {e.message}
332: raise TimeoutError, e.message
333: end
334: end