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