powerhome-activeldap 3.2.3

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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +6 -0
  3. data/COPYING +340 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +59 -0
  6. data/README.textile +140 -0
  7. data/TODO +32 -0
  8. data/benchmark/README.md +64 -0
  9. data/benchmark/bench-backend.rb +247 -0
  10. data/benchmark/bench-instantiate.rb +98 -0
  11. data/benchmark/config.yaml.sample +5 -0
  12. data/doc/text/development.textile +54 -0
  13. data/doc/text/news.textile +811 -0
  14. data/doc/text/rails.textile +144 -0
  15. data/doc/text/tutorial.textile +1010 -0
  16. data/examples/config.yaml.example +5 -0
  17. data/examples/example.der +0 -0
  18. data/examples/example.jpg +0 -0
  19. data/examples/groupadd +41 -0
  20. data/examples/groupdel +35 -0
  21. data/examples/groupls +49 -0
  22. data/examples/groupmod +42 -0
  23. data/examples/lpasswd +55 -0
  24. data/examples/objects/group.rb +13 -0
  25. data/examples/objects/ou.rb +4 -0
  26. data/examples/objects/user.rb +20 -0
  27. data/examples/ouadd +38 -0
  28. data/examples/useradd +45 -0
  29. data/examples/useradd-binary +53 -0
  30. data/examples/userdel +34 -0
  31. data/examples/userls +50 -0
  32. data/examples/usermod +42 -0
  33. data/examples/usermod-binary-add +50 -0
  34. data/examples/usermod-binary-add-time +54 -0
  35. data/examples/usermod-binary-del +48 -0
  36. data/examples/usermod-lang-add +43 -0
  37. data/lib/active_ldap.rb +85 -0
  38. data/lib/active_ldap/action_controller/ldap_benchmarking.rb +55 -0
  39. data/lib/active_ldap/acts/tree.rb +78 -0
  40. data/lib/active_ldap/adapter/base.rb +707 -0
  41. data/lib/active_ldap/adapter/jndi.rb +184 -0
  42. data/lib/active_ldap/adapter/jndi_connection.rb +185 -0
  43. data/lib/active_ldap/adapter/ldap.rb +290 -0
  44. data/lib/active_ldap/adapter/ldap_ext.rb +105 -0
  45. data/lib/active_ldap/adapter/net_ldap.rb +309 -0
  46. data/lib/active_ldap/adapter/net_ldap_ext.rb +23 -0
  47. data/lib/active_ldap/association/belongs_to.rb +47 -0
  48. data/lib/active_ldap/association/belongs_to_many.rb +58 -0
  49. data/lib/active_ldap/association/children.rb +21 -0
  50. data/lib/active_ldap/association/collection.rb +105 -0
  51. data/lib/active_ldap/association/has_many.rb +31 -0
  52. data/lib/active_ldap/association/has_many_utils.rb +44 -0
  53. data/lib/active_ldap/association/has_many_wrap.rb +75 -0
  54. data/lib/active_ldap/association/proxy.rb +107 -0
  55. data/lib/active_ldap/associations.rb +205 -0
  56. data/lib/active_ldap/attribute_methods.rb +23 -0
  57. data/lib/active_ldap/attribute_methods/before_type_cast.rb +24 -0
  58. data/lib/active_ldap/attribute_methods/dirty.rb +43 -0
  59. data/lib/active_ldap/attribute_methods/query.rb +31 -0
  60. data/lib/active_ldap/attribute_methods/read.rb +44 -0
  61. data/lib/active_ldap/attribute_methods/write.rb +38 -0
  62. data/lib/active_ldap/attributes.rb +176 -0
  63. data/lib/active_ldap/base.rb +1410 -0
  64. data/lib/active_ldap/callbacks.rb +71 -0
  65. data/lib/active_ldap/command.rb +49 -0
  66. data/lib/active_ldap/compatible.rb +44 -0
  67. data/lib/active_ldap/configuration.rb +147 -0
  68. data/lib/active_ldap/connection.rb +299 -0
  69. data/lib/active_ldap/distinguished_name.rb +291 -0
  70. data/lib/active_ldap/entry_attribute.rb +78 -0
  71. data/lib/active_ldap/escape.rb +12 -0
  72. data/lib/active_ldap/get_text.rb +20 -0
  73. data/lib/active_ldap/get_text/parser.rb +161 -0
  74. data/lib/active_ldap/helper.rb +92 -0
  75. data/lib/active_ldap/human_readable.rb +133 -0
  76. data/lib/active_ldap/ldap_error.rb +74 -0
  77. data/lib/active_ldap/ldif.rb +930 -0
  78. data/lib/active_ldap/log_subscriber.rb +50 -0
  79. data/lib/active_ldap/object_class.rb +95 -0
  80. data/lib/active_ldap/operations.rb +624 -0
  81. data/lib/active_ldap/persistence.rb +100 -0
  82. data/lib/active_ldap/populate.rb +53 -0
  83. data/lib/active_ldap/railtie.rb +43 -0
  84. data/lib/active_ldap/railties/controller_runtime.rb +48 -0
  85. data/lib/active_ldap/schema.rb +701 -0
  86. data/lib/active_ldap/schema/syntaxes.rb +422 -0
  87. data/lib/active_ldap/timeout.rb +75 -0
  88. data/lib/active_ldap/timeout_stub.rb +17 -0
  89. data/lib/active_ldap/user_password.rb +99 -0
  90. data/lib/active_ldap/validations.rb +200 -0
  91. data/lib/active_ldap/version.rb +3 -0
  92. data/lib/active_ldap/xml.rb +139 -0
  93. data/lib/rails/generators/active_ldap/model/USAGE +18 -0
  94. data/lib/rails/generators/active_ldap/model/model_generator.rb +47 -0
  95. data/lib/rails/generators/active_ldap/model/templates/model_active_ldap.rb +3 -0
  96. data/lib/rails/generators/active_ldap/scaffold/scaffold_generator.rb +14 -0
  97. data/lib/rails/generators/active_ldap/scaffold/templates/ldap.yml +19 -0
  98. data/po/en/active-ldap.po +4029 -0
  99. data/po/ja/active-ldap.po +4060 -0
  100. data/test/add-phonetic-attribute-options-to-slapd.ldif +10 -0
  101. data/test/al-test-utils.rb +428 -0
  102. data/test/command.rb +111 -0
  103. data/test/config.yaml.sample +6 -0
  104. data/test/fixtures/lower_case_object_class_schema.rb +802 -0
  105. data/test/run-test.rb +34 -0
  106. data/test/test_acts_as_tree.rb +60 -0
  107. data/test/test_adapter.rb +121 -0
  108. data/test/test_associations.rb +701 -0
  109. data/test/test_attributes.rb +117 -0
  110. data/test/test_base.rb +1214 -0
  111. data/test/test_base_per_instance.rb +61 -0
  112. data/test/test_bind.rb +62 -0
  113. data/test/test_callback.rb +31 -0
  114. data/test/test_configuration.rb +40 -0
  115. data/test/test_connection.rb +82 -0
  116. data/test/test_connection_per_class.rb +112 -0
  117. data/test/test_connection_per_dn.rb +112 -0
  118. data/test/test_dirty.rb +98 -0
  119. data/test/test_dn.rb +172 -0
  120. data/test/test_find.rb +176 -0
  121. data/test/test_groupadd.rb +50 -0
  122. data/test/test_groupdel.rb +46 -0
  123. data/test/test_groupls.rb +107 -0
  124. data/test/test_groupmod.rb +51 -0
  125. data/test/test_ldif.rb +1890 -0
  126. data/test/test_load.rb +133 -0
  127. data/test/test_lpasswd.rb +75 -0
  128. data/test/test_object_class.rb +74 -0
  129. data/test/test_persistence.rb +131 -0
  130. data/test/test_reflection.rb +175 -0
  131. data/test/test_schema.rb +559 -0
  132. data/test/test_syntax.rb +444 -0
  133. data/test/test_user.rb +217 -0
  134. data/test/test_user_password.rb +108 -0
  135. data/test/test_useradd-binary.rb +62 -0
  136. data/test/test_useradd.rb +57 -0
  137. data/test/test_userdel.rb +48 -0
  138. data/test/test_userls.rb +91 -0
  139. data/test/test_usermod-binary-add-time.rb +65 -0
  140. data/test/test_usermod-binary-add.rb +64 -0
  141. data/test/test_usermod-binary-del.rb +66 -0
  142. data/test/test_usermod-lang-add.rb +59 -0
  143. data/test/test_usermod.rb +58 -0
  144. data/test/test_validation.rb +274 -0
  145. metadata +379 -0
@@ -0,0 +1,707 @@
1
+ require 'benchmark'
2
+
3
+ require 'active_ldap/schema'
4
+ require 'active_ldap/entry_attribute'
5
+ require 'active_ldap/ldap_error'
6
+
7
+ module ActiveLdap
8
+ module Adapter
9
+ class Base
10
+ include GetTextSupport
11
+
12
+ VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :timeout,
13
+ :retry_on_timeout, :retry_limit,
14
+ :retry_wait, :bind_dn, :password,
15
+ :password_block, :try_sasl,
16
+ :sasl_mechanisms, :sasl_quiet,
17
+ :allow_anonymous, :store_password,
18
+ :scope, :sasl_options]
19
+
20
+ @@row_even = true
21
+
22
+ attr_reader :runtime
23
+ def initialize(configuration={})
24
+ @runtime = 0
25
+ @connection = nil
26
+ @disconnected = false
27
+ @bound = false
28
+ @bind_tried = false
29
+ @entry_attributes = {}
30
+ @configuration = configuration.dup
31
+ @logger = @configuration.delete(:logger)
32
+ @configuration.assert_valid_keys(VALID_ADAPTER_CONFIGURATION_KEYS)
33
+ VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
34
+ instance_variable_set("@#{name}", configuration[name])
35
+ end
36
+ @instrumenter = ActiveSupport::Notifications.instrumenter
37
+ end
38
+
39
+ def reset_runtime
40
+ runtime, @runtime = @runtime, 0
41
+ runtime
42
+ end
43
+
44
+ def connect(options={})
45
+ host = options[:host] || @host
46
+ method = options[:method] || @method || :plain
47
+ port = options[:port] || @port || ensure_port(method)
48
+ method = ensure_method(method)
49
+ @disconnected = false
50
+ @bound = false
51
+ @bind_tried = false
52
+ @connection, @uri, @with_start_tls = yield(host, port, method)
53
+ prepare_connection(options)
54
+ bind(options)
55
+ end
56
+
57
+ def disconnect!(options={})
58
+ unbind(options)
59
+ @connection = @uri = @with_start_tls = nil
60
+ @disconnected = true
61
+ end
62
+
63
+ def rebind(options={})
64
+ unbind(options) if bound?
65
+ connect(options)
66
+ end
67
+
68
+ def bind(options={})
69
+ @bind_tried = true
70
+
71
+ bind_dn = ensure_dn_string(options[:bind_dn] || @bind_dn)
72
+ try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl
73
+ if options.has_key?(:allow_anonymous)
74
+ allow_anonymous = options[:allow_anonymous]
75
+ else
76
+ allow_anonymous = @allow_anonymous
77
+ end
78
+ options = options.merge(:allow_anonymous => allow_anonymous)
79
+
80
+ # Rough bind loop:
81
+ # Attempt 1: SASL if available
82
+ # Attempt 2: SIMPLE with credentials if password block
83
+ # Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
84
+ if try_sasl and sasl_bind(bind_dn, options)
85
+ @logger.info {_('Bound to %s by SASL as %s') % [target, bind_dn]}
86
+ elsif simple_bind(bind_dn, options)
87
+ @logger.info {_('Bound to %s by simple as %s') % [target, bind_dn]}
88
+ elsif allow_anonymous and bind_as_anonymous(options)
89
+ @logger.info {_('Bound to %s as anonymous') % target}
90
+ else
91
+ message = yield if block_given?
92
+ message ||= _('All authentication methods for %s exhausted.') % target
93
+ raise AuthenticationError, message
94
+ end
95
+
96
+ @bound = true
97
+ @bound
98
+ end
99
+
100
+ def unbind(options={})
101
+ yield if @connection and (@bind_tried or bound?)
102
+ @bind_tried = @bound = false
103
+ end
104
+
105
+ def bind_as_anonymous(options={})
106
+ yield
107
+ end
108
+
109
+ def connecting?
110
+ !@connection.nil? and !@disconnected
111
+ end
112
+
113
+ def bound?
114
+ connecting? and @bound
115
+ end
116
+
117
+ def schema(options={})
118
+ @schema ||= operation(options) do
119
+ base = options[:base]
120
+ attrs = options[:attributes]
121
+
122
+ attrs ||= [
123
+ 'objectClasses',
124
+ 'attributeTypes',
125
+ 'matchingRules',
126
+ 'matchingRuleUse',
127
+ 'dITStructureRules',
128
+ 'dITContentRules',
129
+ 'nameForms',
130
+ 'ldapSyntaxes',
131
+ #'extendedAttributeInfo', # if we need RANGE-LOWER/UPPER.
132
+ ]
133
+ base ||= root_dse_values('subschemaSubentry', options)[0]
134
+ base ||= 'cn=schema'
135
+ schema = nil
136
+ search(:base => base,
137
+ :scope => :base,
138
+ :filter => '(objectClass=subschema)',
139
+ :attributes => attrs,
140
+ :limit => 1) do |dn, attributes|
141
+ schema = Schema.new(attributes)
142
+ end
143
+ schema || Schema.new([])
144
+ end
145
+ end
146
+
147
+ def naming_contexts
148
+ root_dse_values('namingContexts')
149
+ end
150
+
151
+ def entry_attribute(object_classes)
152
+ @entry_attributes[object_classes.uniq.sort] ||=
153
+ EntryAttribute.new(schema, object_classes)
154
+ end
155
+
156
+ def search(options={})
157
+ filter = parse_filter(options[:filter]) || 'objectClass=*'
158
+ attrs = options[:attributes] || []
159
+ scope = ensure_scope(options[:scope] || @scope)
160
+ base = options[:base]
161
+ limit = options[:limit] || 0
162
+ limit = nil if limit <= 0
163
+
164
+ attrs = attrs.to_a # just in case
165
+ base = ensure_dn_string(base)
166
+ begin
167
+ operation(options) do
168
+ yield(base, scope, filter, attrs, limit)
169
+ end
170
+ rescue LdapError::NoSuchObject, LdapError::InvalidDnSyntax
171
+ # Do nothing on failure
172
+ @logger.info do
173
+ args = [$!.class, $!.message, filter, attrs.inspect]
174
+ _("Ignore error %s(%s): filter %s: attributes: %s") % args
175
+ end
176
+ end
177
+ end
178
+
179
+ def delete(targets, options={})
180
+ targets = [targets] unless targets.is_a?(Array)
181
+ return if targets.empty?
182
+ begin
183
+ operation(options) do
184
+ targets.each do |target|
185
+ target = ensure_dn_string(target)
186
+ begin
187
+ yield(target)
188
+ rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
189
+ raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
190
+ end
191
+ end
192
+ end
193
+ rescue LdapError::NoSuchObject
194
+ raise EntryNotFound, _("No such entry: %s") % target
195
+ end
196
+ end
197
+
198
+ def add(dn, entries, options={})
199
+ dn = ensure_dn_string(dn)
200
+ begin
201
+ operation(options) do
202
+ yield(dn, entries)
203
+ end
204
+ rescue LdapError::NoSuchObject
205
+ raise EntryNotFound, _("No such entry: %s") % dn
206
+ rescue LdapError::InvalidDnSyntax
207
+ raise DistinguishedNameInvalid.new(dn)
208
+ rescue LdapError::AlreadyExists
209
+ raise EntryAlreadyExist, _("%s: %s") % [$!.message, dn]
210
+ rescue LdapError::StrongAuthRequired
211
+ raise StrongAuthenticationRequired, _("%s: %s") % [$!.message, dn]
212
+ rescue LdapError::ObjectClassViolation
213
+ raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
214
+ rescue LdapError::UnwillingToPerform
215
+ raise OperationNotPermitted, _("%s: %s") % [$!.message, dn]
216
+ end
217
+ end
218
+
219
+ def modify(dn, entries, options={})
220
+ dn = ensure_dn_string(dn)
221
+ begin
222
+ operation(options) do
223
+ begin
224
+ yield(dn, entries)
225
+ rescue LdapError::UnwillingToPerform, LdapError::InsufficientAccess
226
+ raise OperationNotPermitted, _("%s: %s") % [$!.message, target]
227
+ end
228
+ end
229
+ rescue LdapError::UndefinedType
230
+ raise
231
+ rescue LdapError::ObjectClassViolation
232
+ raise RequiredAttributeMissed, _("%s: %s") % [$!.message, dn]
233
+ end
234
+ end
235
+
236
+ def modify_rdn(dn, new_rdn, delete_old_rdn, new_superior, options={})
237
+ dn = ensure_dn_string(dn)
238
+ operation(options) do
239
+ yield(dn, new_rdn, delete_old_rdn, new_superior)
240
+ end
241
+ end
242
+
243
+ def log_info(name, runtime_in_seconds, info=nil)
244
+ return unless @logger
245
+ return unless @logger.debug?
246
+ message = "LDAP: #{name} (#{'%.1f' % (runtime_in_seconds * 1000)}ms)"
247
+ @logger.debug(format_log_entry(message, info))
248
+ end
249
+
250
+ private
251
+ def ensure_port(method)
252
+ if method == :ssl
253
+ URI::LDAPS::DEFAULT_PORT
254
+ else
255
+ URI::LDAP::DEFAULT_PORT
256
+ end
257
+ end
258
+
259
+ def prepare_connection(options)
260
+ end
261
+
262
+ def operation(options)
263
+ retried = false
264
+ options = options.dup
265
+ options[:try_reconnect] = true unless options.has_key?(:try_reconnect)
266
+ try_reconnect = false
267
+ begin
268
+ reconnect_if_need(options)
269
+ try_reconnect = options[:try_reconnect]
270
+ with_timeout(try_reconnect, options) do
271
+ yield
272
+ end
273
+ rescue ConnectionError
274
+ if try_reconnect and !retried
275
+ retried = true
276
+ @disconnected = true
277
+ retry
278
+ else
279
+ raise
280
+ end
281
+ end
282
+ end
283
+
284
+ def need_credential_sasl_mechanism?(mechanism)
285
+ not %(GSSAPI EXTERNAL ANONYMOUS).include?(mechanism)
286
+ end
287
+
288
+ def password(bind_dn, options={})
289
+ passwd = options[:password] || @password
290
+ return passwd if passwd
291
+
292
+ password_block = options[:password_block] || @password_block
293
+ # TODO: Give a warning to reconnect users with password clearing
294
+ # Get the passphrase for the first time, or anew if we aren't storing
295
+ if password_block.respond_to?(:call)
296
+ passwd = password_block.call(bind_dn)
297
+ else
298
+ @logger.error {_('password_block not nil or Proc object. Ignoring.')}
299
+ return nil
300
+ end
301
+
302
+ # Store the password for quick reference later
303
+ if options.has_key?(:store_password)
304
+ store_password = options[:store_password]
305
+ else
306
+ store_password = @store_password
307
+ end
308
+ @password = store_password ? passwd : nil
309
+
310
+ passwd
311
+ end
312
+
313
+ def with_timeout(try_reconnect=true, options={}, &block)
314
+ n_retries = 0
315
+ retry_limit = options[:retry_limit] || @retry_limit
316
+ begin
317
+ do_in_timeout(@timeout, &block)
318
+ rescue Timeout::Error => e
319
+ @logger.error {_('Requested action timed out.')}
320
+ if @retry_on_timeout and retry_limit < 0 and n_retries <= retry_limit
321
+ if connecting?
322
+ retry
323
+ elsif try_reconnect
324
+ retry if with_timeout(false, options) {reconnect(options)}
325
+ end
326
+ end
327
+ @logger.error {e.message}
328
+ raise TimeoutError, e.message
329
+ end
330
+ end
331
+
332
+ def do_in_timeout(timeout, &block)
333
+ Timeout.alarm(timeout, &block)
334
+ end
335
+
336
+ def sasl_bind(bind_dn, options={})
337
+ # Get all SASL mechanisms
338
+ mechanisms = operation(options) do
339
+ root_dse_values("supportedSASLMechanisms")
340
+ end
341
+
342
+ if options.has_key?(:sasl_quiet)
343
+ sasl_quiet = options[:sasl_quiet]
344
+ else
345
+ sasl_quiet = @sasl_quiet
346
+ end
347
+
348
+ sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
349
+ sasl_mechanisms.each do |mechanism|
350
+ next unless mechanisms.include?(mechanism)
351
+ return true if yield(bind_dn, mechanism, sasl_quiet)
352
+ end
353
+ false
354
+ end
355
+
356
+ def simple_bind(bind_dn, options={})
357
+ return false unless bind_dn
358
+
359
+ passwd = password(bind_dn, options)
360
+ return false unless passwd
361
+
362
+ if passwd.empty?
363
+ if options[:allow_anonymous]
364
+ @logger.info {_("Skip simple bind with empty password.")}
365
+ return false
366
+ else
367
+ raise AuthenticationError,
368
+ _("Can't use empty password for simple bind.")
369
+ end
370
+ end
371
+
372
+ begin
373
+ yield(bind_dn, passwd)
374
+ rescue LdapError::InvalidDnSyntax
375
+ raise DistinguishedNameInvalid.new(bind_dn)
376
+ rescue LdapError::InvalidCredentials
377
+ false
378
+ end
379
+ end
380
+
381
+ def parse_filter(filter, operator=nil)
382
+ return nil if filter.nil?
383
+ if !filter.is_a?(String) and !filter.respond_to?(:collect)
384
+ filter = filter.to_s
385
+ end
386
+
387
+ case filter
388
+ when String
389
+ parse_filter_string(filter)
390
+ when Hash
391
+ components = filter.sort_by {|k, v| k.to_s}.collect do |key, value|
392
+ construct_component(key, value, operator)
393
+ end
394
+ construct_filter(components, operator)
395
+ else
396
+ operator, components = normalize_array_filter(filter, operator)
397
+ components = construct_components(components, operator)
398
+ construct_filter(components, operator)
399
+ end
400
+ end
401
+
402
+ def parse_filter_string(filter)
403
+ if /\A\s*\z/.match(filter)
404
+ nil
405
+ else
406
+ if filter[0, 1] == "("
407
+ filter
408
+ else
409
+ "(#{filter})"
410
+ end
411
+ end
412
+ end
413
+
414
+ def normalize_array_filter(filter, operator=nil)
415
+ filter_operator, *components = filter
416
+ if filter_logical_operator?(filter_operator)
417
+ operator = filter_operator
418
+ else
419
+ components.unshift(filter_operator)
420
+ components = [components] unless filter_operator.is_a?(Array)
421
+ end
422
+ [operator, components]
423
+ end
424
+
425
+ def extract_filter_value_options(value)
426
+ options = {}
427
+ if value.is_a?(Array)
428
+ case value[0]
429
+ when Hash
430
+ options = value[0]
431
+ value = value[1]
432
+ when "=", "~=", "<=", ">="
433
+ options[:operator] = value[0]
434
+ if value.size > 2
435
+ value = value[1..-1]
436
+ else
437
+ value = value[1]
438
+ end
439
+ end
440
+ end
441
+ [value, options]
442
+ end
443
+
444
+ def construct_components(components, operator)
445
+ components.collect do |component|
446
+ if component.is_a?(Array)
447
+ if filter_logical_operator?(component[0])
448
+ parse_filter(component)
449
+ elsif component.size == 2
450
+ key, value = component
451
+ if value.is_a?(Hash)
452
+ parse_filter(value, key)
453
+ else
454
+ construct_component(key, value, operator)
455
+ end
456
+ else
457
+ construct_component(component[0], component[1..-1], operator)
458
+ end
459
+ elsif component.is_a?(Symbol)
460
+ assert_filter_logical_operator(component)
461
+ nil
462
+ else
463
+ parse_filter(component, operator)
464
+ end
465
+ end
466
+ end
467
+
468
+ def construct_component(key, value, operator=nil)
469
+ value, options = extract_filter_value_options(value)
470
+ comparison_operator = options[:operator] || "="
471
+ if collection?(value)
472
+ return nil if value.empty?
473
+ operator, value = normalize_array_filter(value, operator)
474
+ values = []
475
+ value.each do |val|
476
+ if collection?(val)
477
+ values.concat(val.collect {|v| [key, comparison_operator, v]})
478
+ else
479
+ values << [key, comparison_operator, val]
480
+ end
481
+ end
482
+ values[0] = values[0][1] if filter_logical_operator?(values[0][1])
483
+ parse_filter(values, operator)
484
+ else
485
+ [
486
+ "(",
487
+ escape_filter_key(key),
488
+ comparison_operator,
489
+ escape_filter_value(value, options),
490
+ ")"
491
+ ].join
492
+ end
493
+ end
494
+
495
+ def escape_filter_key(key)
496
+ escape_filter_value(key.to_s)
497
+ end
498
+
499
+ def escape_filter_value(value, options={})
500
+ case value
501
+ when Numeric, DN
502
+ value = value.to_s
503
+ when Time
504
+ value = Schema::GeneralizedTime.new.normalize_value(value)
505
+ end
506
+ value.gsub(/(?:[:()\\\0]|\*\*?)/) do |s|
507
+ if s == "*"
508
+ s
509
+ else
510
+ s = "*" if s == "**"
511
+ if s.respond_to?(:getbyte)
512
+ "\\%02X" % s.getbyte(0)
513
+ else
514
+ "\\%02X" % s[0]
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ def construct_filter(components, operator=nil)
521
+ operator = normalize_filter_logical_operator(operator)
522
+ components = components.compact
523
+ case components.size
524
+ when 0
525
+ nil
526
+ when 1
527
+ filter = components[0]
528
+ filter = "(!#{filter})" if operator == :not
529
+ filter
530
+ else
531
+ "(#{operator == :and ? '&' : '|'}#{components.join})"
532
+ end
533
+ end
534
+
535
+ def collection?(object)
536
+ !object.is_a?(String) and object.respond_to?(:each)
537
+ end
538
+
539
+ LOGICAL_OPERATORS = [:and, :or, :not, :&, :|]
540
+ def filter_logical_operator?(operator)
541
+ LOGICAL_OPERATORS.include?(operator)
542
+ end
543
+
544
+ def normalize_filter_logical_operator(operator)
545
+ assert_filter_logical_operator(operator)
546
+ case (operator || :and)
547
+ when :and, :&
548
+ :and
549
+ when :or, :|
550
+ :or
551
+ else
552
+ :not
553
+ end
554
+ end
555
+
556
+ def assert_filter_logical_operator(operator)
557
+ return if operator.nil?
558
+ unless filter_logical_operator?(operator)
559
+ raise ArgumentError,
560
+ _("invalid logical operator: %s: available operators: %s") %
561
+ [operator.inspect, LOGICAL_OPERATORS.inspect]
562
+ end
563
+ end
564
+
565
+ # Attempts to reconnect up to the number of times allowed
566
+ # If forced, try once then fail with ConnectionError if not connected.
567
+ def reconnect(options={})
568
+ options = options.dup
569
+ force = options[:force]
570
+ retry_limit = options[:retry_limit] || @retry_limit
571
+ retry_wait = options[:retry_wait] || @retry_wait
572
+ options[:reconnect_attempts] ||= 0
573
+
574
+ loop do
575
+ @logger.debug {_('Attempting to reconnect')}
576
+ disconnect!
577
+
578
+ # Reset the attempts if this was forced.
579
+ options[:reconnect_attempts] = 0 if force
580
+ options[:reconnect_attempts] += 1 if retry_limit >= 0
581
+ begin
582
+ connect(options)
583
+ break
584
+ rescue AuthenticationError
585
+ raise
586
+ rescue => detail
587
+ @logger.error do
588
+ _("Reconnect to server failed: %s\n" \
589
+ "Reconnect to server failed backtrace:\n" \
590
+ "%s") % [detail.exception, detail.backtrace.join("\n")]
591
+ end
592
+ # Do not loop if forced
593
+ raise ConnectionError, detail.message if force
594
+ end
595
+
596
+ unless can_reconnect?(options)
597
+ raise ConnectionError,
598
+ _('Giving up trying to reconnect to LDAP server.')
599
+ end
600
+
601
+ # Sleep before looping
602
+ sleep retry_wait
603
+ end
604
+
605
+ true
606
+ end
607
+
608
+ def reconnect_if_need(options={})
609
+ return if connecting?
610
+ with_timeout(false, options) do
611
+ reconnect(options)
612
+ end
613
+ end
614
+
615
+ # Determine if we have exceed the retry limit or not.
616
+ # True is reconnecting is allowed - False if not.
617
+ def can_reconnect?(options={})
618
+ retry_limit = options[:retry_limit] || @retry_limit
619
+ reconnect_attempts = options[:reconnect_attempts] || 0
620
+
621
+ retry_limit < 0 or reconnect_attempts <= retry_limit
622
+ end
623
+
624
+ def root_dse_values(key, options={})
625
+ dse = root_dse([key], options)
626
+ return [] if dse.nil?
627
+ normalized_key = key.downcase
628
+ dse.each do |_key, _value|
629
+ return _value if _key.downcase == normalized_key
630
+ end
631
+ []
632
+ end
633
+
634
+ def root_dse(attrs, options={})
635
+ found_attributes = nil
636
+ search(:base => "",
637
+ :scope => :base,
638
+ :attributes => attrs,
639
+ :limit => 1) do |dn, attributes|
640
+ found_attributes = attributes
641
+ end
642
+ found_attributes
643
+ end
644
+
645
+ def construct_uri(host, port, ssl)
646
+ protocol = ssl ? "ldaps" : "ldap"
647
+ URI.parse("#{protocol}://#{host}:#{port}").to_s
648
+ end
649
+
650
+ def target
651
+ return nil if @uri.nil?
652
+ if @with_start_tls
653
+ "#{@uri}(StartTLS)"
654
+ else
655
+ @uri
656
+ end
657
+ end
658
+
659
+ def log(name, info=nil)
660
+ if block_given?
661
+ result = nil
662
+ @instrumenter.instrument(
663
+ "log_info.active_ldap",
664
+ :info => info,
665
+ :name => name) { result = yield }
666
+ result
667
+ else
668
+ log_info(name, 0, info)
669
+ nil
670
+ end
671
+ rescue Exception
672
+ log_info("#{name}: FAILED", 0,
673
+ (info || {}).merge(:error => $!.class.name,
674
+ :error_message => $!.message))
675
+ raise
676
+ end
677
+
678
+ def format_log_entry(message, info=nil)
679
+ if ActiveLdap::Base.colorize_logging
680
+ if @@row_even
681
+ message_color, dump_color = "4;36;1", "0;1"
682
+ else
683
+ @@row_even = true
684
+ message_color, dump_color = "4;35;1", "0"
685
+ end
686
+ @@row_even = !@@row_even
687
+
688
+ log_entry = " \e[#{message_color}m#{message}\e[0m"
689
+ log_entry << ": \e[#{dump_color}m#{info.inspect}\e[0m" if info
690
+ log_entry
691
+ else
692
+ log_entry = message
693
+ log_entry += ": #{info.inspect}" if info
694
+ log_entry
695
+ end
696
+ end
697
+
698
+ def ensure_dn_string(dn)
699
+ if dn.is_a?(DN)
700
+ dn.to_s
701
+ else
702
+ dn
703
+ end
704
+ end
705
+ end
706
+ end
707
+ end