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.
- data/CHANGES +5 -0
- data/Manifest.txt +91 -25
- data/README +22 -0
- data/Rakefile +41 -8
- data/TODO +1 -6
- data/examples/config.yaml.example +5 -0
- data/examples/example.der +0 -0
- data/examples/example.jpg +0 -0
- data/examples/groupadd +41 -0
- data/examples/groupdel +35 -0
- data/examples/groupls +49 -0
- data/examples/groupmod +42 -0
- data/examples/lpasswd +55 -0
- data/examples/objects/group.rb +13 -0
- data/examples/objects/ou.rb +4 -0
- data/examples/objects/user.rb +20 -0
- data/examples/ouadd +38 -0
- data/examples/useradd +45 -0
- data/examples/useradd-binary +50 -0
- data/examples/userdel +34 -0
- data/examples/userls +50 -0
- data/examples/usermod +42 -0
- data/examples/usermod-binary-add +47 -0
- data/examples/usermod-binary-add-time +51 -0
- data/examples/usermod-binary-del +48 -0
- data/examples/usermod-lang-add +43 -0
- data/lib/active_ldap.rb +213 -214
- data/lib/active_ldap/adapter/base.rb +461 -0
- data/lib/active_ldap/adapter/ldap.rb +232 -0
- data/lib/active_ldap/adapter/ldap_ext.rb +69 -0
- data/lib/active_ldap/adapter/net_ldap.rb +288 -0
- data/lib/active_ldap/adapter/net_ldap_ext.rb +29 -0
- data/lib/active_ldap/association/belongs_to.rb +3 -1
- data/lib/active_ldap/association/belongs_to_many.rb +5 -6
- data/lib/active_ldap/association/has_many.rb +9 -17
- data/lib/active_ldap/association/has_many_wrap.rb +4 -5
- data/lib/active_ldap/attributes.rb +4 -0
- data/lib/active_ldap/base.rb +201 -56
- data/lib/active_ldap/configuration.rb +11 -1
- data/lib/active_ldap/connection.rb +15 -9
- data/lib/active_ldap/distinguished_name.rb +246 -0
- data/lib/active_ldap/ldap_error.rb +74 -0
- data/lib/active_ldap/object_class.rb +9 -5
- data/lib/active_ldap/schema.rb +50 -9
- data/lib/active_ldap/validations.rb +11 -13
- data/rails/plugin/active_ldap/generators/scaffold_al/scaffold_al_generator.rb +7 -0
- data/rails/plugin/active_ldap/generators/scaffold_al/templates/ldap.yml +21 -0
- data/rails/plugin/active_ldap/init.rb +10 -4
- data/test/al-test-utils.rb +46 -3
- data/test/run-test.rb +16 -4
- data/test/test-unit-ext/always-show-result.rb +28 -0
- data/test/test-unit-ext/priority.rb +163 -0
- data/test/test_adapter.rb +81 -0
- data/test/test_attributes.rb +8 -1
- data/test/test_base.rb +132 -3
- data/test/test_base_per_instance.rb +14 -3
- data/test/test_connection.rb +19 -0
- data/test/test_dn.rb +161 -0
- data/test/test_find.rb +24 -0
- data/test/test_object_class.rb +15 -2
- data/test/test_schema.rb +108 -1
- metadata +111 -41
- data/lib/active_ldap/adaptor/base.rb +0 -29
- data/lib/active_ldap/adaptor/ldap.rb +0 -466
- 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
|