ruby-activeldap 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/CHANGES +5 -0
  2. data/Manifest.txt +91 -25
  3. data/README +22 -0
  4. data/Rakefile +41 -8
  5. data/TODO +1 -6
  6. data/examples/config.yaml.example +5 -0
  7. data/examples/example.der +0 -0
  8. data/examples/example.jpg +0 -0
  9. data/examples/groupadd +41 -0
  10. data/examples/groupdel +35 -0
  11. data/examples/groupls +49 -0
  12. data/examples/groupmod +42 -0
  13. data/examples/lpasswd +55 -0
  14. data/examples/objects/group.rb +13 -0
  15. data/examples/objects/ou.rb +4 -0
  16. data/examples/objects/user.rb +20 -0
  17. data/examples/ouadd +38 -0
  18. data/examples/useradd +45 -0
  19. data/examples/useradd-binary +50 -0
  20. data/examples/userdel +34 -0
  21. data/examples/userls +50 -0
  22. data/examples/usermod +42 -0
  23. data/examples/usermod-binary-add +47 -0
  24. data/examples/usermod-binary-add-time +51 -0
  25. data/examples/usermod-binary-del +48 -0
  26. data/examples/usermod-lang-add +43 -0
  27. data/lib/active_ldap.rb +213 -214
  28. data/lib/active_ldap/adapter/base.rb +461 -0
  29. data/lib/active_ldap/adapter/ldap.rb +232 -0
  30. data/lib/active_ldap/adapter/ldap_ext.rb +69 -0
  31. data/lib/active_ldap/adapter/net_ldap.rb +288 -0
  32. data/lib/active_ldap/adapter/net_ldap_ext.rb +29 -0
  33. data/lib/active_ldap/association/belongs_to.rb +3 -1
  34. data/lib/active_ldap/association/belongs_to_many.rb +5 -6
  35. data/lib/active_ldap/association/has_many.rb +9 -17
  36. data/lib/active_ldap/association/has_many_wrap.rb +4 -5
  37. data/lib/active_ldap/attributes.rb +4 -0
  38. data/lib/active_ldap/base.rb +201 -56
  39. data/lib/active_ldap/configuration.rb +11 -1
  40. data/lib/active_ldap/connection.rb +15 -9
  41. data/lib/active_ldap/distinguished_name.rb +246 -0
  42. data/lib/active_ldap/ldap_error.rb +74 -0
  43. data/lib/active_ldap/object_class.rb +9 -5
  44. data/lib/active_ldap/schema.rb +50 -9
  45. data/lib/active_ldap/validations.rb +11 -13
  46. data/rails/plugin/active_ldap/generators/scaffold_al/scaffold_al_generator.rb +7 -0
  47. data/rails/plugin/active_ldap/generators/scaffold_al/templates/ldap.yml +21 -0
  48. data/rails/plugin/active_ldap/init.rb +10 -4
  49. data/test/al-test-utils.rb +46 -3
  50. data/test/run-test.rb +16 -4
  51. data/test/test-unit-ext/always-show-result.rb +28 -0
  52. data/test/test-unit-ext/priority.rb +163 -0
  53. data/test/test_adapter.rb +81 -0
  54. data/test/test_attributes.rb +8 -1
  55. data/test/test_base.rb +132 -3
  56. data/test/test_base_per_instance.rb +14 -3
  57. data/test/test_connection.rb +19 -0
  58. data/test/test_dn.rb +161 -0
  59. data/test/test_find.rb +24 -0
  60. data/test/test_object_class.rb +15 -2
  61. data/test/test_schema.rb +108 -1
  62. metadata +111 -41
  63. data/lib/active_ldap/adaptor/base.rb +0 -29
  64. data/lib/active_ldap/adaptor/ldap.rb +0 -466
  65. data/lib/active_ldap/ldap.rb +0 -113
@@ -0,0 +1,461 @@
1
+ require 'active_ldap/schema'
2
+ require 'active_ldap/ldap_error'
3
+
4
+ module ActiveLdap
5
+ module Adapter
6
+ class Base
7
+ VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :timeout,
8
+ :retry_on_timeout, :retry_limit,
9
+ :retry_wait, :bind_dn, :password,
10
+ :password_block, :try_sasl,
11
+ :sasl_mechanisms, :sasl_quiet,
12
+ :allow_anonymous, :store_password]
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
22
+
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
31
+
32
+ def disconnect!(options={})
33
+ return if @connection.nil?
34
+ unbind(options)
35
+ @connection = nil
36
+ end
37
+
38
+ def rebind(options={})
39
+ unbind(options) if bound?
40
+ connect(options)
41
+ end
42
+
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
70
+
71
+ def bind_as_anonymous(options={})
72
+ @logger.info {"Attempting anonymous authentication"}
73
+ operation(options) do
74
+ yield
75
+ end
76
+ end
77
+
78
+ def connecting?
79
+ not @connection.nil?
80
+ end
81
+
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
108
+
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
116
+
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
145
+
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
160
+
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
180
+
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
192
+
193
+ private
194
+ def prepare_connection(options)
195
+ end
196
+
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
205
+
206
+ def need_credential_sasl_mechanism?(mechanism)
207
+ not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism)
208
+ end
209
+
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
234
+
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
245
+
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
272
+
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
291
+
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
327
+
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
339
+
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
349
+
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
366
+
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
379
+
380
+ def collection?(object)
381
+ !object.is_a?(String) and object.respond_to?(:each)
382
+ end
383
+
384
+ LOGICAL_OPERATORS = [:and, :or, :&, :|]
385
+ def filter_logical_operator?(operator)
386
+ LOGICAL_OPERATORS.include?(operator)
387
+ end
388
+
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
398
+
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
407
+
408
+ # Attempts to reconnect up to the number of times allowed
409
+ # If forced, try once then fail with ConnectionError if not connected.
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
446
+
447
+ def reconnect_if_need(options={})
448
+ reconnect(options) if !connecting? and can_reconnect?(options)
449
+ end
450
+
451
+ # Determine if we have exceed the retry limit or not.
452
+ # True is reconnecting is allowed - False if not.
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
459
+ end
460
+ end
461
+ end