ruby-activeldap 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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