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