| Class | ActiveLdap::Adapter::Base |
| In: |
lib/active_ldap/adapter/base.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, :&, :|] |
# 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 16
16: def initialize(configuration={})
17: @connection = nil
18: @disconnected = false
19: @configuration = configuration.dup
20: @logger = @configuration.delete(:logger)
21: @configuration.assert_valid_keys(VALID_ADAPTER_CONFIGURATION_KEYS)
22: VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
23: instance_variable_set("@#{name}", configuration[name])
24: end
25: end
# File lib/active_ldap/adapter/base.rb, line 166
166: def add(dn, entries, options={})
167: begin
168: operation(options) do
169: yield(dn, entries)
170: end
171: rescue LdapError::NoSuchObject
172: raise EntryNotFound, _("No such entry: %s") % dn
173: rescue LdapError::InvalidDnSyntax
174: raise DistinguishedNameInvalid.new(dn)
175: rescue LdapError::AlreadyExists
176: raise EntryAlreadyExist, _("%s: %s") % [$!.message, dn]
177: rescue LdapError::StrongAuthRequired
178: raise StrongAuthenticationRequired, _("%s: %s") % [$!.message, dn]
179: rescue LdapError::ObjectClassViolation
180: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
181: rescue LdapError::UnwillingToPerform
182: raise OperationNotPermitted, _("%s: %s") % [$!.message, dn]
183: end
184: end
# File lib/active_ldap/adapter/base.rb, line 48
48: def bind(options={})
49: bind_dn = options[:bind_dn] || @bind_dn
50: try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl
51: if options.has_key?(:allow_anonymous)
52: allow_anonymous = options[:allow_anonymous]
53: else
54: allow_anonymous = @allow_anonymous
55: end
56:
57: # Rough bind loop:
58: # Attempt 1: SASL if available
59: # Attempt 2: SIMPLE with credentials if password block
60: # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
61: if try_sasl and sasl_bind(bind_dn, options)
62: @logger.info {_('Bound by SASL as %s') % bind_dn}
63: elsif simple_bind(bind_dn, options)
64: @logger.info {_('Bound by simple as %s') % bind_dn}
65: elsif allow_anonymous and bind_as_anonymous(options)
66: @logger.info {_('Bound as anonymous')}
67: else
68: message = yield if block_given?
69: message ||= _('All authentication methods exhausted.')
70: raise AuthenticationError, message
71: end
72:
73: bound?
74: end
# File lib/active_ldap/adapter/base.rb, line 76
76: def bind_as_anonymous(options={})
77: operation(options) do
78: yield
79: end
80: end
# File lib/active_ldap/adapter/base.rb, line 27
27: def connect(options={})
28: host = options[:host] || @host
29: port = options[:port] || @port
30: method = ensure_method(options[:method] || @method)
31: @disconnected = false
32: @connection = yield(host, port, method)
33: prepare_connection(options)
34: bind(options)
35: end
# File lib/active_ldap/adapter/base.rb, line 82
82: def connecting?
83: !@connection.nil? and !@disconnected
84: end
# File lib/active_ldap/adapter/base.rb, line 151
151: def delete(targets, options={})
152: targets = [targets] unless targets.is_a?(Array)
153: return if targets.empty?
154: target = nil
155: begin
156: operation(options) do
157: targets.each do |target|
158: yield(target)
159: end
160: end
161: rescue LdapError::NoSuchObject
162: raise EntryNotFound, _("No such entry: %s") % target
163: end
164: end
# File lib/active_ldap/adapter/base.rb, line 37
37: def disconnect!(options={})
38: return if @connection.nil?
39: unbind(options)
40: @connection = nil
41: end
# File lib/active_ldap/adapter/base.rb, line 112
112: def load(ldifs, options={})
113: operation(options) do
114: ldifs.split(/(?:\r?\n){2,}/).each do |ldif|
115: yield(ldif)
116: end
117: end
118: end
# File lib/active_ldap/adapter/base.rb, line 186
186: def modify(dn, entries, options={})
187: begin
188: operation(options) do
189: yield(dn, entries)
190: end
191: rescue LdapError::UndefinedType
192: raise
193: rescue LdapError::ObjectClassViolation
194: raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
195: end
196: end
# File lib/active_ldap/adapter/base.rb, line 43
43: def rebind(options={})
44: unbind(options) if bound?
45: connect(options)
46: end
# File lib/active_ldap/adapter/base.rb, line 86
86: def schema(options={})
87: @schema ||= operation(options) do
88: base = options[:base]
89: attrs = options[:attributes]
90:
91: attrs ||= [
92: 'objectClasses',
93: 'attributeTypes',
94: 'matchingRules',
95: 'matchingRuleUse',
96: 'dITStructureRules',
97: 'dITContentRules',
98: 'nameForms',
99: 'ldapSyntaxes',
100: #'extendedAttributeInfo', # if we need RANGE-LOWER/UPPER.
101: ]
102: base ||= root_dse_values('subschemaSubentry', options)[0]
103: base ||= 'cn=schema'
104: dn, attributes = search(:base => base,
105: :scope => :base,
106: :filter => '(objectClass=subschema)',
107: :attributes => attrs).first
108: Schema.new(attributes)
109: end
110: end
# File lib/active_ldap/adapter/base.rb, line 120
120: def search(options={})
121: filter = parse_filter(options[:filter]) || 'objectClass=*'
122: attrs = options[:attributes] || []
123: scope = ensure_scope(options[:scope] || @scope)
124: base = options[:base]
125: limit = options[:limit] || 0
126: limit = nil if limit <= 0
127:
128: attrs = attrs.to_a # just in case
129:
130: values = []
131: callback = Proc.new do |value, block|
132: value = block.call(value) if block
133: values << value
134: end
135:
136: begin
137: operation(options) do
138: yield(base, scope, filter, attrs, limit, callback)
139: end
140: rescue LdapError
141: # Do nothing on failure
142: @logger.info do
143: args = [$!.class, $!.message, filter, attrs.inspect]
144: _("Ignore error %s(%s): filter %s: attributes: %s") % args
145: end
146: end
147:
148: values
149: end
# File lib/active_ldap/adapter/base.rb, line 461
461: def assert_filter_logical_operator(operator)
462: return if operator.nil?
463: unless filter_logical_operator?(operator)
464: raise ArgumentError,
465: _("invalid logical operator: %s: available operators: %s") %
466: [operator.inspect, LOGICAL_OPERATORS.inspect]
467: end
468: 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 517
517: def can_reconnect?(options={})
518: retry_limit = options[:retry_limit] || @retry_limit
519: reconnect_attempts = options[:reconnect_attempts] || 0
520:
521: retry_limit < 0 or reconnect_attempts < (retry_limit - 1)
522: end
# File lib/active_ldap/adapter/base.rb, line 440
440: def collection?(object)
441: !object.is_a?(String) and object.respond_to?(:each)
442: end
# File lib/active_ldap/adapter/base.rb, line 380
380: def construct_component(key, value, operator=nil)
381: value, options = extract_filter_value_options(value)
382: if collection?(value)
383: values = []
384: value.each do |val|
385: if collection?(val)
386: values.concat(val.collect {|v| [key, v]})
387: else
388: values << [key, val]
389: end
390: end
391: values[0] = values[0][1] if filter_logical_operator?(values[0][1])
392: parse_filter(values, operator)
393: else
394: [
395: "(",
396: escape_filter_key(key),
397: options[:operator] || "=",
398: escape_filter_value(value, options),
399: ")"
400: ].join
401: end
402: end
# File lib/active_ldap/adapter/base.rb, line 425
425: def construct_filter(components, operator=nil)
426: operator = normalize_filter_logical_operator(operator)
427: components = components.compact
428: case components.size
429: when 0
430: nil
431: when 1
432: filter = components[0]
433: filter = "(!#{filter})" if operator == :not
434: filter
435: else
436: "(#{operator == :and ? '&' : '|'}#{components.join})"
437: end
438: end
# File lib/active_ldap/adapter/base.rb, line 404
404: def escape_filter_key(key)
405: escape_filter_value(key.to_s)
406: end
# File lib/active_ldap/adapter/base.rb, line 408
408: def escape_filter_value(value, options={})
409: case value
410: when Numeric, DN
411: value = value.to_s
412: when Time
413: value = Schema::GeneralizedTime.new.normalize_value(value)
414: end
415: value.gsub(/(?:[()\\\0]|\*\*?)/) do |s|
416: if s == "*"
417: s
418: else
419: s = "*" if s == "**"
420: "\\%02X" % s[0]
421: end
422: end
423: end
# File lib/active_ldap/adapter/base.rb, line 365
365: def extract_filter_value_options(value)
366: options = {}
367: if value.is_a?(Array)
368: case value[0]
369: when Hash
370: options = value[0]
371: value = value[1]
372: when "=", "~=", "<=", "=>"
373: options[:operator] = value[1]
374: value = value[1]
375: end
376: end
377: [value, options]
378: end
# File lib/active_ldap/adapter/base.rb, line 445
445: def filter_logical_operator?(operator)
446: LOGICAL_OPERATORS.include?(operator)
447: end
# File lib/active_ldap/adapter/base.rb, line 222
222: def need_credential_sasl_mechanism?(mechanism)
223: not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism)
224: end
# File lib/active_ldap/adapter/base.rb, line 355
355: def normalize_array_filter(filter, operator=nil)
356: filter_operator, *components = filter
357: if filter_logical_operator?(filter_operator)
358: operator = filter_operator
359: else
360: components.unshift(filter_operator)
361: end
362: [operator, components]
363: end
# File lib/active_ldap/adapter/base.rb, line 449
449: def normalize_filter_logical_operator(operator)
450: assert_filter_logical_operator(operator)
451: case (operator || :and)
452: when :and, :&
453: :and
454: when :or, :|
455: :or
456: else
457: :not
458: end
459: end
# File lib/active_ldap/adapter/base.rb, line 202
202: def operation(options)
203: retried = false
204: begin
205: reconnect_if_need
206: try_reconnect = !options.has_key?(:try_reconnect) ||
207: options[:try_reconnect]
208: with_timeout(try_reconnect, options) do
209: yield
210: end
211: rescue Errno::EPIPE
212: if retried or !try_reconnect
213: raise
214: else
215: retried = true
216: @disconnected = true
217: retry
218: end
219: end
220: end
# File lib/active_ldap/adapter/base.rb, line 305
305: def parse_filter(filter, operator=nil)
306: return nil if filter.nil?
307: if !filter.is_a?(String) and !filter.respond_to?(:collect)
308: filter = filter.to_s
309: end
310:
311: case filter
312: when String
313: parse_filter_string(filter)
314: when Hash
315: components = filter.sort_by {|k, v| k.to_s}.collect do |key, value|
316: construct_component(key, value, operator)
317: end
318: construct_filter(components, operator)
319: else
320: operator, components = normalize_array_filter(filter, operator)
321:
322: components = components.collect do |component|
323: if component.is_a?(Array) and component.size == 2
324: key, value = component
325: if filter_logical_operator?(key)
326: parse_filter(component)
327: elsif value.is_a?(Hash)
328: parse_filter(value, key)
329: else
330: construct_component(key, value, operator)
331: end
332: elsif component.is_a?(Symbol)
333: assert_filter_logical_operator(component)
334: nil
335: else
336: parse_filter(component, operator)
337: end
338: end
339: construct_filter(components, operator)
340: end
341: end
# File lib/active_ldap/adapter/base.rb, line 343
343: def parse_filter_string(filter)
344: if /\A\s*\z/.match(filter)
345: nil
346: else
347: if filter[0, 1] == "("
348: filter
349: else
350: "(#{filter})"
351: end
352: end
353: end
# File lib/active_ldap/adapter/base.rb, line 226
226: def password(bind_dn, options={})
227: passwd = options[:password] || @password
228: return passwd if passwd
229:
230: password_block = options[:password_block] || @password_block
231: # TODO: Give a warning to reconnect users with password clearing
232: # Get the passphrase for the first time, or anew if we aren't storing
233: if password_block.respond_to?(:call)
234: passwd = password_block.call(bind_dn)
235: else
236: @logger.error {_('password_block not nil or Proc object. Ignoring.')}
237: return nil
238: end
239:
240: # Store the password for quick reference later
241: if options.has_key?(:store_password)
242: store_password = options[:store_password]
243: else
244: store_password = @store_password
245: end
246: @password = store_password ? passwd : nil
247:
248: passwd
249: 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 472
472: def reconnect(options={})
473: options = options.dup
474: force = options[:force]
475: retry_limit = options[:retry_limit] || @retry_limit
476: retry_wait = options[:retry_wait] || @retry_wait
477: options[:reconnect_attempts] ||= 0
478:
479: loop do
480: unless can_reconnect?(options)
481: raise ConnectionError,
482: _('Giving up trying to reconnect to LDAP server.')
483: end
484:
485: @logger.debug {_('Attempting to reconnect')}
486: disconnect!
487:
488: # Reset the attempts if this was forced.
489: options[:reconnect_attempts] = 0 if force
490: options[:reconnect_attempts] += 1 if retry_limit >= 0
491: begin
492: connect(options)
493: break
494: rescue => detail
495: @logger.error do
496: _("Reconnect to server failed: %s\n" \
497: "Reconnect to server failed backtrace:\n" \
498: "%s") % [detail.exception, detail.backtrace.join("\n")]
499: end
500: # Do not loop if forced
501: raise ConnectionError, detail.message if force
502: end
503:
504: # Sleep before looping
505: sleep retry_wait
506: end
507:
508: true
509: end
# File lib/active_ldap/adapter/base.rb, line 511
511: def reconnect_if_need(options={})
512: reconnect(options) if !connecting? and can_reconnect?(options)
513: end
# File lib/active_ldap/adapter/base.rb, line 524
524: def root_dse_values(key, options={})
525: dse = root_dse([key], options)[0]
526: return [] if dse.nil?
527: dse[key] || dse[key.downcase] || []
528: end
# File lib/active_ldap/adapter/base.rb, line 262
262: def sasl_bind(bind_dn, options={})
263: return false unless bind_dn
264:
265: # Get all SASL mechanisms
266: mechanisms = operation(options) do
267: root_dse_values("supportedSASLMechanisms")
268: end
269:
270: if options.has_key?(:sasl_quiet)
271: sasl_quiet = options[:sasl_quiet]
272: else
273: sasl_quiet = @sasl_quiet
274: end
275:
276: sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
277: sasl_mechanisms.each do |mechanism|
278: next unless mechanisms.include?(mechanism)
279: operation(options) do
280: yield(bind_dn, mechanism, sasl_quiet)
281: return true if bound?
282: end
283: end
284: false
285: end
# File lib/active_ldap/adapter/base.rb, line 287
287: def simple_bind(bind_dn, options={})
288: return false unless bind_dn
289:
290: passwd = password(bind_dn, options)
291: return false unless passwd
292:
293: begin
294: operation(options) do
295: yield(bind_dn, passwd)
296: bound?
297: end
298: rescue LdapError::InvalidDnSyntax
299: raise DistinguishedNameInvalid.new(bind_dn)
300: rescue LdapError::InvalidCredentials
301: false
302: end
303: end
# File lib/active_ldap/adapter/base.rb, line 251
251: def with_timeout(try_reconnect=true, options={}, &block)
252: begin
253: Timeout.alarm(@timeout, &block)
254: rescue Timeout::Error => e
255: @logger.error {_('Requested action timed out.')}
256: retry if try_reconnect and @retry_on_timeout and reconnect(options)
257: @logger.error {e.message}
258: raise TimeoutError, e.message
259: end
260: end