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.
- checksums.yaml +7 -0
- data/.yardopts +6 -0
- data/COPYING +340 -0
- data/Gemfile +12 -0
- data/LICENSE +59 -0
- data/README.textile +140 -0
- data/TODO +32 -0
- data/benchmark/README.md +64 -0
- data/benchmark/bench-backend.rb +247 -0
- data/benchmark/bench-instantiate.rb +98 -0
- data/benchmark/config.yaml.sample +5 -0
- data/doc/text/development.textile +54 -0
- data/doc/text/news.textile +811 -0
- data/doc/text/rails.textile +144 -0
- data/doc/text/tutorial.textile +1010 -0
- 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 +53 -0
- data/examples/userdel +34 -0
- data/examples/userls +50 -0
- data/examples/usermod +42 -0
- data/examples/usermod-binary-add +50 -0
- data/examples/usermod-binary-add-time +54 -0
- data/examples/usermod-binary-del +48 -0
- data/examples/usermod-lang-add +43 -0
- data/lib/active_ldap.rb +85 -0
- data/lib/active_ldap/action_controller/ldap_benchmarking.rb +55 -0
- data/lib/active_ldap/acts/tree.rb +78 -0
- data/lib/active_ldap/adapter/base.rb +707 -0
- data/lib/active_ldap/adapter/jndi.rb +184 -0
- data/lib/active_ldap/adapter/jndi_connection.rb +185 -0
- data/lib/active_ldap/adapter/ldap.rb +290 -0
- data/lib/active_ldap/adapter/ldap_ext.rb +105 -0
- data/lib/active_ldap/adapter/net_ldap.rb +309 -0
- data/lib/active_ldap/adapter/net_ldap_ext.rb +23 -0
- data/lib/active_ldap/association/belongs_to.rb +47 -0
- data/lib/active_ldap/association/belongs_to_many.rb +58 -0
- data/lib/active_ldap/association/children.rb +21 -0
- data/lib/active_ldap/association/collection.rb +105 -0
- data/lib/active_ldap/association/has_many.rb +31 -0
- data/lib/active_ldap/association/has_many_utils.rb +44 -0
- data/lib/active_ldap/association/has_many_wrap.rb +75 -0
- data/lib/active_ldap/association/proxy.rb +107 -0
- data/lib/active_ldap/associations.rb +205 -0
- data/lib/active_ldap/attribute_methods.rb +23 -0
- data/lib/active_ldap/attribute_methods/before_type_cast.rb +24 -0
- data/lib/active_ldap/attribute_methods/dirty.rb +43 -0
- data/lib/active_ldap/attribute_methods/query.rb +31 -0
- data/lib/active_ldap/attribute_methods/read.rb +44 -0
- data/lib/active_ldap/attribute_methods/write.rb +38 -0
- data/lib/active_ldap/attributes.rb +176 -0
- data/lib/active_ldap/base.rb +1410 -0
- data/lib/active_ldap/callbacks.rb +71 -0
- data/lib/active_ldap/command.rb +49 -0
- data/lib/active_ldap/compatible.rb +44 -0
- data/lib/active_ldap/configuration.rb +147 -0
- data/lib/active_ldap/connection.rb +299 -0
- data/lib/active_ldap/distinguished_name.rb +291 -0
- data/lib/active_ldap/entry_attribute.rb +78 -0
- data/lib/active_ldap/escape.rb +12 -0
- data/lib/active_ldap/get_text.rb +20 -0
- data/lib/active_ldap/get_text/parser.rb +161 -0
- data/lib/active_ldap/helper.rb +92 -0
- data/lib/active_ldap/human_readable.rb +133 -0
- data/lib/active_ldap/ldap_error.rb +74 -0
- data/lib/active_ldap/ldif.rb +930 -0
- data/lib/active_ldap/log_subscriber.rb +50 -0
- data/lib/active_ldap/object_class.rb +95 -0
- data/lib/active_ldap/operations.rb +624 -0
- data/lib/active_ldap/persistence.rb +100 -0
- data/lib/active_ldap/populate.rb +53 -0
- data/lib/active_ldap/railtie.rb +43 -0
- data/lib/active_ldap/railties/controller_runtime.rb +48 -0
- data/lib/active_ldap/schema.rb +701 -0
- data/lib/active_ldap/schema/syntaxes.rb +422 -0
- data/lib/active_ldap/timeout.rb +75 -0
- data/lib/active_ldap/timeout_stub.rb +17 -0
- data/lib/active_ldap/user_password.rb +99 -0
- data/lib/active_ldap/validations.rb +200 -0
- data/lib/active_ldap/version.rb +3 -0
- data/lib/active_ldap/xml.rb +139 -0
- data/lib/rails/generators/active_ldap/model/USAGE +18 -0
- data/lib/rails/generators/active_ldap/model/model_generator.rb +47 -0
- data/lib/rails/generators/active_ldap/model/templates/model_active_ldap.rb +3 -0
- data/lib/rails/generators/active_ldap/scaffold/scaffold_generator.rb +14 -0
- data/lib/rails/generators/active_ldap/scaffold/templates/ldap.yml +19 -0
- data/po/en/active-ldap.po +4029 -0
- data/po/ja/active-ldap.po +4060 -0
- data/test/add-phonetic-attribute-options-to-slapd.ldif +10 -0
- data/test/al-test-utils.rb +428 -0
- data/test/command.rb +111 -0
- data/test/config.yaml.sample +6 -0
- data/test/fixtures/lower_case_object_class_schema.rb +802 -0
- data/test/run-test.rb +34 -0
- data/test/test_acts_as_tree.rb +60 -0
- data/test/test_adapter.rb +121 -0
- data/test/test_associations.rb +701 -0
- data/test/test_attributes.rb +117 -0
- data/test/test_base.rb +1214 -0
- data/test/test_base_per_instance.rb +61 -0
- data/test/test_bind.rb +62 -0
- data/test/test_callback.rb +31 -0
- data/test/test_configuration.rb +40 -0
- data/test/test_connection.rb +82 -0
- data/test/test_connection_per_class.rb +112 -0
- data/test/test_connection_per_dn.rb +112 -0
- data/test/test_dirty.rb +98 -0
- data/test/test_dn.rb +172 -0
- data/test/test_find.rb +176 -0
- data/test/test_groupadd.rb +50 -0
- data/test/test_groupdel.rb +46 -0
- data/test/test_groupls.rb +107 -0
- data/test/test_groupmod.rb +51 -0
- data/test/test_ldif.rb +1890 -0
- data/test/test_load.rb +133 -0
- data/test/test_lpasswd.rb +75 -0
- data/test/test_object_class.rb +74 -0
- data/test/test_persistence.rb +131 -0
- data/test/test_reflection.rb +175 -0
- data/test/test_schema.rb +559 -0
- data/test/test_syntax.rb +444 -0
- data/test/test_user.rb +217 -0
- data/test/test_user_password.rb +108 -0
- data/test/test_useradd-binary.rb +62 -0
- data/test/test_useradd.rb +57 -0
- data/test/test_userdel.rb +48 -0
- data/test/test_userls.rb +91 -0
- data/test/test_usermod-binary-add-time.rb +65 -0
- data/test/test_usermod-binary-add.rb +64 -0
- data/test/test_usermod-binary-del.rb +66 -0
- data/test/test_usermod-lang-add.rb +59 -0
- data/test/test_usermod.rb +58 -0
- data/test/test_validation.rb +274 -0
- 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
|