ruby-activeldap 0.7.4 → 0.8.0
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 +375 -0
- data/COPYING +340 -0
- data/LICENSE +58 -0
- data/Manifest.txt +33 -0
- data/README +63 -0
- data/Rakefile +37 -0
- data/TODO +31 -0
- data/benchmark/bench-al.rb +152 -0
- data/lib/{activeldap.rb → active_ldap.rb} +280 -263
- data/lib/active_ldap/adaptor/base.rb +29 -0
- data/lib/active_ldap/adaptor/ldap.rb +466 -0
- data/lib/active_ldap/association/belongs_to.rb +38 -0
- data/lib/active_ldap/association/belongs_to_many.rb +40 -0
- data/lib/active_ldap/association/collection.rb +80 -0
- data/lib/active_ldap/association/has_many.rb +48 -0
- data/lib/active_ldap/association/has_many_wrap.rb +56 -0
- data/lib/active_ldap/association/proxy.rb +89 -0
- data/lib/active_ldap/associations.rb +162 -0
- data/lib/active_ldap/attributes.rb +199 -0
- data/lib/active_ldap/base.rb +1343 -0
- data/lib/active_ldap/callbacks.rb +19 -0
- data/lib/active_ldap/command.rb +46 -0
- data/lib/active_ldap/configuration.rb +96 -0
- data/lib/active_ldap/connection.rb +137 -0
- data/lib/{activeldap → active_ldap}/ldap.rb +1 -1
- data/lib/active_ldap/object_class.rb +70 -0
- data/lib/active_ldap/schema.rb +258 -0
- data/lib/{activeldap → active_ldap}/timeout.rb +0 -0
- data/lib/{activeldap → active_ldap}/timeout_stub.rb +0 -0
- data/lib/active_ldap/user_password.rb +92 -0
- data/lib/active_ldap/validations.rb +78 -0
- data/rails/plugin/active_ldap/README +54 -0
- data/rails/plugin/active_ldap/init.rb +6 -0
- data/test/TODO +2 -0
- data/test/al-test-utils.rb +337 -0
- data/test/command.rb +62 -0
- data/test/config.yaml +8 -0
- data/test/config.yaml.sample +6 -0
- data/test/run-test.rb +17 -0
- data/test/test-unit-ext.rb +2 -0
- data/test/test_associations.rb +334 -0
- data/test/test_attributes.rb +71 -0
- data/test/test_base.rb +345 -0
- data/test/test_base_per_instance.rb +32 -0
- data/test/test_bind.rb +53 -0
- data/test/test_callback.rb +35 -0
- data/test/test_connection.rb +38 -0
- data/test/test_connection_per_class.rb +50 -0
- data/test/test_find.rb +36 -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_lpasswd.rb +75 -0
- data/test/test_object_class.rb +32 -0
- data/test/test_reflection.rb +173 -0
- data/test/test_schema.rb +166 -0
- data/test/test_user.rb +209 -0
- data/test/test_user_password.rb +93 -0
- data/test/test_useradd-binary.rb +59 -0
- data/test/test_useradd.rb +55 -0
- data/test/test_userdel.rb +48 -0
- data/test/test_userls.rb +86 -0
- data/test/test_usermod-binary-add-time.rb +62 -0
- data/test/test_usermod-binary-add.rb +61 -0
- data/test/test_usermod-binary-del.rb +64 -0
- data/test/test_usermod-lang-add.rb +57 -0
- data/test/test_usermod.rb +56 -0
- data/test/test_validation.rb +38 -0
- metadata +94 -21
- data/lib/activeldap/associations.rb +0 -170
- data/lib/activeldap/base.rb +0 -1456
- data/lib/activeldap/configuration.rb +0 -59
- data/lib/activeldap/schema2.rb +0 -217
data/lib/activeldap/base.rb
DELETED
@@ -1,1456 +0,0 @@
|
|
1
|
-
# === ActiveLDAP - an OO-interface to LDAP objects inspired by ActiveRecord
|
2
|
-
# Author: Will Drewry <will@alum.bu.edu>
|
3
|
-
# License: See LICENSE and COPYING.txt
|
4
|
-
# Copyright 2004-2006 Will Drewry <will@alum.bu.edu>
|
5
|
-
# Some portions Copyright 2006 Google Inc
|
6
|
-
#
|
7
|
-
# == Summary
|
8
|
-
# ActiveLDAP lets you read and update LDAP entries in a completely object
|
9
|
-
# oriented fashion, even handling attributes with multiple names seamlessly.
|
10
|
-
# It was inspired by ActiveRecord so extending it to deal with custom
|
11
|
-
# LDAP schemas is as effortless as knowing the 'ou' of the objects, and the
|
12
|
-
# primary key. (fix this up some)
|
13
|
-
#
|
14
|
-
# == Example
|
15
|
-
# irb> require 'activeldap'
|
16
|
-
# > true
|
17
|
-
# irb> user = ActiveLDAP::User.new("drewry")
|
18
|
-
# > #<ActiveLDAP::User:0x402e...
|
19
|
-
# irb> user.cn
|
20
|
-
# > "foo"
|
21
|
-
# irb> user.commonname
|
22
|
-
# > "foo"
|
23
|
-
# irb> user.cn = "Will Drewry"
|
24
|
-
# > "Will Drewry"
|
25
|
-
# irb> user.cn
|
26
|
-
# > "Will Drewry"
|
27
|
-
# irb> user.validate
|
28
|
-
# > nil
|
29
|
-
# irb> user.write
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
33
|
-
require 'ldap'
|
34
|
-
require 'ldap/schema'
|
35
|
-
require 'log4r'
|
36
|
-
|
37
|
-
module ActiveLDAP
|
38
|
-
# OO-interface to LDAP assuming pam/nss_ldap-style organization with Active specifics
|
39
|
-
# Each subclass does a ldapsearch for the matching entry.
|
40
|
-
# If no exact match, raise an error.
|
41
|
-
# If match, change all LDAP attributes in accessor attributes on the object.
|
42
|
-
# -- these are ACTUALLY populated from schema - see subschema.rb example
|
43
|
-
# -- @conn.schema2().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
|
44
|
-
# -- extract objectClasses from match and populate
|
45
|
-
# Multiple entries become lists.
|
46
|
-
# If this isn't read-only then lists become multiple entries, etc.
|
47
|
-
|
48
|
-
# AttributeEmpty
|
49
|
-
#
|
50
|
-
# An exception raised when a required attribute is found to be empty
|
51
|
-
class AttributeEmpty < RuntimeError
|
52
|
-
end
|
53
|
-
|
54
|
-
# ConfigurationError
|
55
|
-
#
|
56
|
-
# An exception raised when there is a problem with Base.connect arguments
|
57
|
-
class ConfigurationError < RuntimeError
|
58
|
-
end
|
59
|
-
|
60
|
-
# DeleteError
|
61
|
-
#
|
62
|
-
# An exception raised when an ActiveLDAP delete action fails
|
63
|
-
class DeleteError < RuntimeError
|
64
|
-
end
|
65
|
-
|
66
|
-
# WriteError
|
67
|
-
#
|
68
|
-
# An exception raised when an ActiveLDAP write action fails
|
69
|
-
class WriteError < RuntimeError
|
70
|
-
end
|
71
|
-
|
72
|
-
# AuthenticationError
|
73
|
-
#
|
74
|
-
# An exception raised when user authentication fails
|
75
|
-
class AuthenticationError < RuntimeError
|
76
|
-
end
|
77
|
-
|
78
|
-
# ConnectionError
|
79
|
-
#
|
80
|
-
# An exception raised when the LDAP conenction fails
|
81
|
-
class ConnectionError < RuntimeError
|
82
|
-
end
|
83
|
-
|
84
|
-
# ObjectClassError
|
85
|
-
#
|
86
|
-
# An exception raised when an objectClass is not defined in the schema
|
87
|
-
class ObjectClassError < RuntimeError
|
88
|
-
end
|
89
|
-
|
90
|
-
# AttributeAssignmentError
|
91
|
-
#
|
92
|
-
# An exception raised when there is an issue assigning a value to
|
93
|
-
# an attribute
|
94
|
-
class AttributeAssignmentError < RuntimeError
|
95
|
-
end
|
96
|
-
|
97
|
-
# TimeoutError
|
98
|
-
#
|
99
|
-
# An exception raised when a connection action fails due to a timeout
|
100
|
-
class TimeoutError < RuntimeError
|
101
|
-
end
|
102
|
-
|
103
|
-
# Base
|
104
|
-
#
|
105
|
-
# Base is the primary class which contains all of the core
|
106
|
-
# ActiveLDAP functionality. It is meant to only ever be subclassed
|
107
|
-
# by extension classes.
|
108
|
-
class Base
|
109
|
-
# Parsed schema structures
|
110
|
-
attr_reader :must, :may
|
111
|
-
attr_accessor :logger
|
112
|
-
|
113
|
-
# All class-wide variables
|
114
|
-
@@config = nil # Container for current connection settings
|
115
|
-
@@schema = nil # LDAP server's schema
|
116
|
-
@@conn = nil # LDAP connection
|
117
|
-
@@reconnect_attempts = 0 # Number of reconnects attempted
|
118
|
-
|
119
|
-
# Driver generator
|
120
|
-
#
|
121
|
-
# TODO add type checking
|
122
|
-
# This let's you call this method to create top-level extension object. This
|
123
|
-
# is really just a proof of concept and has not truly useful purpose.
|
124
|
-
# example: Base.create_object(:class => "user", :dnattr => "uid", :classes => ['top'])
|
125
|
-
#
|
126
|
-
# THIS METHOD IS DANGEROUS. INPUT IS NOT SANITIZED.
|
127
|
-
def Base.create_object(config={})
|
128
|
-
# Just upcase the first letter of the new class name
|
129
|
-
str = config[:class]
|
130
|
-
class_name = str[0].chr.upcase + str[1..-1]
|
131
|
-
|
132
|
-
attr = config[:dnattr] # "uid"
|
133
|
-
prefix = config[:base] # "ou=People"
|
134
|
-
# [ 'top', 'posixAccount' ]
|
135
|
-
classes_array = config[:classes] || []
|
136
|
-
# [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ]
|
137
|
-
belongs_to_array = config[:belongs_to] || []
|
138
|
-
# [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ]
|
139
|
-
has_many_array = config[:has_many] || []
|
140
|
-
|
141
|
-
raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size
|
142
|
-
raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size
|
143
|
-
raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size
|
144
|
-
|
145
|
-
# Build classes array
|
146
|
-
classes = '['
|
147
|
-
classes_array.map! {|x| x = "'#{x}'"}
|
148
|
-
classes << classes_array.join(', ')
|
149
|
-
classes << ']'
|
150
|
-
|
151
|
-
# Build belongs_to
|
152
|
-
belongs_to = []
|
153
|
-
if belongs_to_array.size > 0
|
154
|
-
belongs_to_array.each do |bt|
|
155
|
-
line = [ "belongs_to :#{bt[0]}" ]
|
156
|
-
bt[1].keys.each do |key|
|
157
|
-
line << ":#{key} => '#{bt[1][key]}'"
|
158
|
-
end
|
159
|
-
belongs_to << line.join(', ')
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Build has_many
|
164
|
-
has_many = []
|
165
|
-
if has_many_array.size > 0
|
166
|
-
has_many_array.each do |hm|
|
167
|
-
line = [ "has_many :#{hm[0]}" ]
|
168
|
-
hm[1].keys.each do |key|
|
169
|
-
line << ":#{key} => '#{hm[1][key]}'"
|
170
|
-
end
|
171
|
-
has_many << line.join(', ')
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
self.class.module_eval <<-"end_eval"
|
176
|
-
class ::#{class_name} < ActiveLDAP::Base
|
177
|
-
ldap_mapping :dnattr => "#{attr}", :prefix => "#{prefix}", :classes => #{classes}
|
178
|
-
#{belongs_to.join("\n")}
|
179
|
-
#{has_many.join("\n")}
|
180
|
-
end
|
181
|
-
end_eval
|
182
|
-
end
|
183
|
-
|
184
|
-
# Connect and bind to LDAP creating a class variable for use by all ActiveLDAP
|
185
|
-
# objects.
|
186
|
-
#
|
187
|
-
# == +config+
|
188
|
-
# +config+ must be a hash that may contain any of the following fields:
|
189
|
-
# :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous
|
190
|
-
# :user specifies the username to bind with.
|
191
|
-
# :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=dataspill,dc=org. Overrides @@bind_format.
|
192
|
-
# :password_block specifies a Proc object that will yield a String to be used as the password when called.
|
193
|
-
# :logger specifies a preconfigured Log4r::Logger to be used for all logging
|
194
|
-
# :host sets the LDAP server hostname
|
195
|
-
# :port sets the LDAP server port
|
196
|
-
# :base overwrites Base.base - this affects EVERYTHING
|
197
|
-
# :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false)
|
198
|
-
# :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
|
199
|
-
# :retries - indicates the number of attempts to reconnect that will be undertaken when a stale connection occurs. -1 means infinite.
|
200
|
-
# :sasl_quiet - if true, sets @sasl_quiet on the Ruby/LDAP connection
|
201
|
-
# :method - whether to use :ssl, :tls, or :plain (unencrypted)
|
202
|
-
# :retry_wait - seconds to wait before retrying a connection
|
203
|
-
# :ldap_scope - dictates how to find objects. ONELEVEL by default to avoid dn_attr collisions across OUs. Think before changing.
|
204
|
-
# :return_objects - indicates whether find/find_all will return objects or just the distinguished name attribute value of the matches
|
205
|
-
# :timeout - time in seconds - defaults to disabled. This CAN interrupt search() requests. Be warned.
|
206
|
-
# :retry_on_timeout - whether to reconnect when timeouts occur. Defaults to true
|
207
|
-
# See lib/configuration.rb for defaults for each option
|
208
|
-
def Base.connect(config={})
|
209
|
-
# Process config
|
210
|
-
# Class options
|
211
|
-
## These will be replace by configuration.rb defaults if defined
|
212
|
-
@@config = DEFAULT_CONFIG.dup
|
213
|
-
config.keys.each do |key|
|
214
|
-
case key
|
215
|
-
when :base
|
216
|
-
# Scrub before inserting
|
217
|
-
base = config[:base].gsub(/['}{#]/, '')
|
218
|
-
Base.class_eval("def Base.base();'#{base}';end")
|
219
|
-
when :ldap_scope
|
220
|
-
if config[:ldap_scope].class != Fixnum
|
221
|
-
raise ConfigurationError, ':ldap_scope must be a Fixnum'
|
222
|
-
end
|
223
|
-
Base.class_eval("def Base.ldap_scope();#{config[:ldap_scope]};end")
|
224
|
-
else
|
225
|
-
@@config[key] = config[key]
|
226
|
-
end
|
227
|
-
end
|
228
|
-
# Assign a easier name for the logger
|
229
|
-
@@logger = @@config[:logger] || nil
|
230
|
-
# Setup default logger to console
|
231
|
-
if @@logger.nil?
|
232
|
-
@@logger = Log4r::Logger.new('activeldap')
|
233
|
-
@@logger.level = Log4r::OFF
|
234
|
-
Log4r::StderrOutputter.new 'console'
|
235
|
-
@@logger.add('console')
|
236
|
-
end
|
237
|
-
|
238
|
-
# Reset for the new connection
|
239
|
-
@@reconnect_attempts = 0
|
240
|
-
|
241
|
-
# Make the connection.
|
242
|
-
do_connect()
|
243
|
-
|
244
|
-
# Make irb users happy with a 'true'
|
245
|
-
return true
|
246
|
-
end # Base.connect
|
247
|
-
|
248
|
-
# Base.close
|
249
|
-
# This method deletes the LDAP connection object.
|
250
|
-
# This does NOT reset any overridden values from a Base.connect call.
|
251
|
-
def Base.close
|
252
|
-
begin
|
253
|
-
@@conn.unbind unless @@conn.nil?
|
254
|
-
rescue
|
255
|
-
# Doesn't matter.
|
256
|
-
end
|
257
|
-
@@conn = nil
|
258
|
-
# Make sure it is cleaned up
|
259
|
-
# This causes Ruby/LDAP memory corruption.
|
260
|
-
# ObjectSpace.garbage_collect
|
261
|
-
end
|
262
|
-
|
263
|
-
# Return the LDAP connection object currently in use
|
264
|
-
# Alternately execute a command against the connection
|
265
|
-
# object "safely" using a given block. Use the given
|
266
|
-
# "errmsg" for any error conditions.
|
267
|
-
def Base.connection(exc=RuntimeError.new('unknown error'), try_reconnect = true)
|
268
|
-
# Block was given! Let's safely provide access.
|
269
|
-
if block_given?
|
270
|
-
begin
|
271
|
-
Timeout.alarm(@@config[:timeout]) do
|
272
|
-
begin
|
273
|
-
yield @@conn
|
274
|
-
rescue => e
|
275
|
-
# Raise an LDAP error instead of RuntimeError or whatever
|
276
|
-
|
277
|
-
raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0
|
278
|
-
# Else reraise
|
279
|
-
|
280
|
-
raise e
|
281
|
-
end
|
282
|
-
end
|
283
|
-
rescue Timeout::Error => e
|
284
|
-
@@logger.error('Requested action timed out.')
|
285
|
-
retry if try_reconnect and @@config[:retry_on_timeout] and Base.reconnect()
|
286
|
-
message = e.message
|
287
|
-
message = exc.message unless exc.nil?
|
288
|
-
@@logger.error(message)
|
289
|
-
raise TimeoutError, message
|
290
|
-
rescue LDAP::ServerDown, LDAP::ResultError, RuntimeError => e
|
291
|
-
@@logger.error("#{e.class} exception occurred in connection block")
|
292
|
-
@@logger.error("Exception message: #{e.message}")
|
293
|
-
@@logger.error("Exception backtrace: #{e.backtrace}")
|
294
|
-
@@logger.error(exc.message) unless exc.nil?
|
295
|
-
retry if try_reconnect and Base.reconnect()
|
296
|
-
raise exc unless exc.nil?
|
297
|
-
return nil
|
298
|
-
rescue LDAP::UndefinedType => e
|
299
|
-
@@logger.error("#{e.class} exception occurred in connection block")
|
300
|
-
@@logger.error("Exception message: #{e.message}")
|
301
|
-
@@logger.error("Exception backtrace: #{e.backtrace}")
|
302
|
-
# Do not retry - not a connection error
|
303
|
-
raise exc unless exc.nil?
|
304
|
-
return nil
|
305
|
-
# Catch all - to be remedied later
|
306
|
-
rescue => e
|
307
|
-
@@logger.error("#{e.class} exception occurred in connection block")
|
308
|
-
@@logger.error("Exception message: #{e.message}")
|
309
|
-
@@logger.error("Exception backtrace: #{e.backtrace}")
|
310
|
-
@@logger.error("Error in catch all: please send debug log to ActiveLDAP author")
|
311
|
-
@@logger.error(exc.message) unless exc.nil?
|
312
|
-
raise exc unless exc.nil?
|
313
|
-
return nil
|
314
|
-
end
|
315
|
-
end
|
316
|
-
return @@conn
|
317
|
-
end
|
318
|
-
|
319
|
-
# Set the LDAP connection avoiding Base.connect or multiplexing connections
|
320
|
-
def Base.connection=(conn)
|
321
|
-
@@conn = conn
|
322
|
-
end
|
323
|
-
|
324
|
-
# Determine if we have exceed the retry limit or not.
|
325
|
-
# True is reconnecting is allowed - False if not.
|
326
|
-
def Base.can_reconnect?
|
327
|
-
# Allow connect if we've never connected.
|
328
|
-
return true unless @@config
|
329
|
-
if @@reconnect_attempts < (@@config[:retries] - 1) or
|
330
|
-
@@config[:retries] < 0
|
331
|
-
return true
|
332
|
-
end
|
333
|
-
return false
|
334
|
-
end
|
335
|
-
|
336
|
-
# Attempts to reconnect up to the number of times allowed
|
337
|
-
# If forced, try once then fail with ConnectionError if not connected.
|
338
|
-
def Base.reconnect(force=false)
|
339
|
-
unless @@config
|
340
|
-
@@logger.error('Ignoring force: Base.reconnect called before Base.connect') if force
|
341
|
-
|
342
|
-
Base.connect
|
343
|
-
return true
|
344
|
-
end
|
345
|
-
not_connected = true
|
346
|
-
while not_connected
|
347
|
-
if Base.can_reconnect?
|
348
|
-
|
349
|
-
Base.close()
|
350
|
-
|
351
|
-
# Reset the attempts if this was forced.
|
352
|
-
@@reconnect_attempts = 0 if force
|
353
|
-
@@reconnect_attempts += 1 if @@config[:retries] >= 0
|
354
|
-
begin
|
355
|
-
do_connect()
|
356
|
-
not_connected = false
|
357
|
-
rescue => detail
|
358
|
-
@@logger.error("Reconnect to server failed: #{detail.exception}")
|
359
|
-
@@logger.error("Reconnect to server failed backtrace: #{detail.backtrace}")
|
360
|
-
# Do not loop if forced
|
361
|
-
raise ConnectionError, detail.message if force
|
362
|
-
end
|
363
|
-
else
|
364
|
-
# Raise a warning
|
365
|
-
raise ConnectionError, 'Giving up trying to reconnect to LDAP server.'
|
366
|
-
end
|
367
|
-
|
368
|
-
# Sleep before looping
|
369
|
-
sleep @@config[:retry_wait]
|
370
|
-
end
|
371
|
-
return true
|
372
|
-
end
|
373
|
-
|
374
|
-
# Return the schema object
|
375
|
-
def Base.schema
|
376
|
-
@@schema
|
377
|
-
end
|
378
|
-
|
379
|
-
# search
|
380
|
-
#
|
381
|
-
# Wraps Ruby/LDAP connection.search to make it easier to search for specific
|
382
|
-
# data without cracking open Base.connection
|
383
|
-
def Base.search(config={})
|
384
|
-
Base.reconnect if Base.connection.nil? and Base.can_reconnect?
|
385
|
-
|
386
|
-
config[:filter] = 'objectClass=*' unless config.has_key? :filter
|
387
|
-
config[:attrs] = [] unless config.has_key? :attrs
|
388
|
-
config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope
|
389
|
-
config[:base] = base() unless config.has_key? :base
|
390
|
-
|
391
|
-
values = []
|
392
|
-
config[:attrs] = config[:attrs].to_a # just in case
|
393
|
-
|
394
|
-
result = Base.connection() do |conn|
|
395
|
-
conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
|
396
|
-
res = {}
|
397
|
-
res['dn'] = [m.dn.dup] # For consistency with the below
|
398
|
-
m.attrs.each do |attr|
|
399
|
-
if config[:attrs].member? attr or config[:attrs].empty?
|
400
|
-
res[attr] = m.vals(attr).dup
|
401
|
-
end
|
402
|
-
end
|
403
|
-
values.push(res)
|
404
|
-
end
|
405
|
-
end
|
406
|
-
if result.nil?
|
407
|
-
# Do nothing on failure
|
408
|
-
|
409
|
-
end
|
410
|
-
return values
|
411
|
-
end
|
412
|
-
|
413
|
-
# find
|
414
|
-
#
|
415
|
-
# Finds the first match for value where |value| is the value of some
|
416
|
-
# |field|, or the wildcard match. This is only useful for derived classes.
|
417
|
-
# usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
|
418
|
-
# Subclass.find('some*val')
|
419
|
-
#
|
420
|
-
def Base.find(config='*')
|
421
|
-
Base.reconnect if Base.connection.nil? and Base.can_reconnect?
|
422
|
-
|
423
|
-
if self.class == Class
|
424
|
-
klass = self.ancestors[0].to_s.split(':').last
|
425
|
-
real_klass = self.ancestors[0]
|
426
|
-
else
|
427
|
-
klass = self.class.to_s.split(':').last
|
428
|
-
real_klass = self.class
|
429
|
-
end
|
430
|
-
|
431
|
-
# Allow a single string argument
|
432
|
-
attr = dnattr()
|
433
|
-
objects = @@config[:return_objects]
|
434
|
-
val = config
|
435
|
-
# Or a hash
|
436
|
-
if config.respond_to?(:has_key?)
|
437
|
-
attr = config[:attribute] || dnattr()
|
438
|
-
val = config[:value] || '*'
|
439
|
-
objects = config[:objects] unless config[:objects].nil?
|
440
|
-
end
|
441
|
-
|
442
|
-
Base.connection(ConnectionError.new("Failed in #{self.class}#find(#{config.inspect})")) do |conn|
|
443
|
-
# Get some attributes
|
444
|
-
conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m|
|
445
|
-
# Extract the dnattr value
|
446
|
-
dnval = m.dn.split(/,/)[0].split(/=/)[1]
|
447
|
-
|
448
|
-
if objects
|
449
|
-
return real_klass.new(m)
|
450
|
-
else
|
451
|
-
return dnval
|
452
|
-
end
|
453
|
-
end
|
454
|
-
end
|
455
|
-
# If we're here, there were no results
|
456
|
-
return nil
|
457
|
-
end
|
458
|
-
private_class_method :find
|
459
|
-
|
460
|
-
# find_all
|
461
|
-
#
|
462
|
-
# Finds all matches for value where |value| is the value of some
|
463
|
-
# |field|, or the wildcard match. This is only useful for derived classes.
|
464
|
-
def Base.find_all(config='*')
|
465
|
-
Base.reconnect if Base.connection.nil? and Base.can_reconnect?
|
466
|
-
|
467
|
-
if self.class == Class
|
468
|
-
real_klass = self.ancestors[0]
|
469
|
-
else
|
470
|
-
real_klass = self.class
|
471
|
-
end
|
472
|
-
|
473
|
-
# Allow a single string argument
|
474
|
-
val = config
|
475
|
-
attr = dnattr()
|
476
|
-
objects = @@config[:return_objects]
|
477
|
-
# Or a hash
|
478
|
-
if config.respond_to?(:has_key?)
|
479
|
-
val = config[:value] || '*'
|
480
|
-
attr = config[:attribute] || dnattr()
|
481
|
-
objects = config[:objects] unless config[:objects].nil?
|
482
|
-
end
|
483
|
-
|
484
|
-
matches = []
|
485
|
-
Base.connection(ConnectionError.new("Failed in #{self.class}#find_all(#{config.inspect})")) do |conn|
|
486
|
-
# Get some attributes
|
487
|
-
conn.search(base(), ldap_scope(), "(#{attr}=#{val})") do |m|
|
488
|
-
# Extract the dnattr value
|
489
|
-
dnval = m.dn.split(/,/)[0].split(/=/)[1]
|
490
|
-
|
491
|
-
if objects
|
492
|
-
matches.push(real_klass.new(m))
|
493
|
-
else
|
494
|
-
matches.push(dnval)
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
498
|
-
return matches
|
499
|
-
end
|
500
|
-
private_class_method :find_all
|
501
|
-
|
502
|
-
# Base.base
|
503
|
-
#
|
504
|
-
# This method when included into Base provides
|
505
|
-
# an inheritable, overwritable configuration setting
|
506
|
-
#
|
507
|
-
# This should be a string with the base of the
|
508
|
-
# ldap server such as 'dc=example,dc=com', and
|
509
|
-
# it should be overwritten by including
|
510
|
-
# configuration.rb into this class.
|
511
|
-
# When subclassing, the specified prefix will be concatenated.
|
512
|
-
def Base.base
|
513
|
-
'dc=localdomain'
|
514
|
-
end
|
515
|
-
|
516
|
-
# Base.dnattr
|
517
|
-
#
|
518
|
-
# This is a placeholder for the class method that will
|
519
|
-
# be overridden on calling ldap_mapping in a subclass.
|
520
|
-
# Using a class method allows for clean inheritance from
|
521
|
-
# classes that already have a ldap_mapping.
|
522
|
-
def Base.dnattr
|
523
|
-
''
|
524
|
-
end
|
525
|
-
|
526
|
-
# Base.required_classes
|
527
|
-
#
|
528
|
-
# This method when included into Base provides
|
529
|
-
# an inheritable, overwritable configuration setting
|
530
|
-
#
|
531
|
-
# The value should be the minimum required objectClasses
|
532
|
-
# to make an object in the LDAP server, or an empty array [].
|
533
|
-
# This should be overwritten by configuration.rb.
|
534
|
-
# Note that subclassing does not cause concatenation of
|
535
|
-
# arrays to occurs.
|
536
|
-
def Base.required_classes
|
537
|
-
[]
|
538
|
-
end
|
539
|
-
|
540
|
-
# Base.ldap_scope
|
541
|
-
#
|
542
|
-
# This method when included into Base provides
|
543
|
-
# an inheritable, overwritable configuration setting
|
544
|
-
#
|
545
|
-
# This value should be the default LDAP scope behavior
|
546
|
-
# desired.
|
547
|
-
def Base.ldap_scope
|
548
|
-
LDAP::LDAP_SCOPE_ONELEVEL
|
549
|
-
end
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
### All instance methods, etc
|
554
|
-
|
555
|
-
# new
|
556
|
-
#
|
557
|
-
# Creates a new instance of Base initializing all class and all
|
558
|
-
# initialization. Defines local defaults. See examples If multiple values
|
559
|
-
# exist for dnattr, the first one put here will be authoritative
|
560
|
-
# TODO: Add # support for relative distinguished names
|
561
|
-
# val can be a dn attribute value, a full DN, or a LDAP::Entry. The use
|
562
|
-
# with a LDAP::Entry is primarily meant for internal use by find and
|
563
|
-
# find_all.
|
564
|
-
#
|
565
|
-
def initialize(val)
|
566
|
-
@exists = false
|
567
|
-
# Make sure we're connected
|
568
|
-
Base.reconnect if Base.connection.nil? and Base.can_reconnect?
|
569
|
-
|
570
|
-
if val.class == LDAP::Entry
|
571
|
-
# Call import, which is basically initialize
|
572
|
-
# without accessing LDAP.
|
573
|
-
|
574
|
-
import(val)
|
575
|
-
return
|
576
|
-
end
|
577
|
-
if val.class != String
|
578
|
-
raise TypeError, "Object key must be a String"
|
579
|
-
end
|
580
|
-
|
581
|
-
@data = {} # where the r/w entry data is stored
|
582
|
-
@ldap_data = {} # original ldap entry data
|
583
|
-
@attr_methods = {} # list of valid method calls for attributes used for dereferencing
|
584
|
-
@last_oc = false # for use in other methods for "caching"
|
585
|
-
if dnattr().empty?
|
586
|
-
raise ConfigurationError, "dnattr() not set for this class: #{self.class}"
|
587
|
-
end
|
588
|
-
|
589
|
-
# Extract dnattr if val looks like a dn
|
590
|
-
if val.match(/^#{dnattr()}=([^,=]+),/i)
|
591
|
-
val = $1
|
592
|
-
elsif val.match(/[=,]/)
|
593
|
-
@@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN."
|
594
|
-
val = ''
|
595
|
-
end
|
596
|
-
|
597
|
-
# Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data
|
598
|
-
if val.class != String or val.empty?
|
599
|
-
raise TypeError, 'a dn attribute String must be supplied ' +
|
600
|
-
'on initialization'
|
601
|
-
else
|
602
|
-
# Create what should be the authoritative DN
|
603
|
-
@dn = "#{dnattr()}=#{val},#{base()}"
|
604
|
-
|
605
|
-
# Search for the existing entry
|
606
|
-
Base.connection(ConnectionError.new("Failed in #{self.class}#new(#{val.inspect})")) do |conn|
|
607
|
-
# Get some attributes
|
608
|
-
conn.search(base(), ldap_scope(), "(#{dnattr()}=#{val})") do |m|
|
609
|
-
@exists = true
|
610
|
-
# Save DN
|
611
|
-
@dn = m.dn
|
612
|
-
# Load up data into tmp
|
613
|
-
|
614
|
-
m.attrs.each do |attr|
|
615
|
-
# Load with subtypes just like @data
|
616
|
-
|
617
|
-
safe_attr, value = make_subtypes(attr, m.vals(attr).dup)
|
618
|
-
|
619
|
-
# Add subtype to any existing values
|
620
|
-
if @ldap_data.has_key? safe_attr
|
621
|
-
value.each do |v|
|
622
|
-
@ldap_data[safe_attr].push(v)
|
623
|
-
end
|
624
|
-
else
|
625
|
-
@ldap_data[safe_attr] = value
|
626
|
-
end
|
627
|
-
end
|
628
|
-
end
|
629
|
-
end
|
630
|
-
end
|
631
|
-
|
632
|
-
# Do the actual object setup work.
|
633
|
-
if @exists
|
634
|
-
# Make sure the server uses objectClass and not objectclass
|
635
|
-
unless @ldap_data.has_key?('objectClass')
|
636
|
-
real_objc = @ldap_data.grep(/^objectclass$/i)
|
637
|
-
if real_objc.size == 1
|
638
|
-
@ldap_data['objectClass'] = @ldap_data[real_objc]
|
639
|
-
@ldap_data.delete(real_objc)
|
640
|
-
else
|
641
|
-
raise AttributeEmpty, 'objectClass was not sent by LDAP server!'
|
642
|
-
end
|
643
|
-
end
|
644
|
-
|
645
|
-
# Populate schema data
|
646
|
-
send(:apply_objectclass, @ldap_data['objectClass'])
|
647
|
-
|
648
|
-
# Populate real data now that we have the schema with aliases
|
649
|
-
@ldap_data.each do |pair|
|
650
|
-
real_attr = @attr_methods[pair[0]]
|
651
|
-
|
652
|
-
if real_attr.nil?
|
653
|
-
@@logger.error("Unable to resolve attribute value #{pair[0].inspect}. " +
|
654
|
-
"Unpredictable behavior likely!")
|
655
|
-
end
|
656
|
-
@data[real_attr] = pair[1].dup
|
657
|
-
|
658
|
-
end
|
659
|
-
else
|
660
|
-
send(:apply_objectclass, required_classes())
|
661
|
-
|
662
|
-
# Setup dn attribute (later rdn too!)
|
663
|
-
real_dnattr = @attr_methods[dnattr()]
|
664
|
-
@data[real_dnattr] = val
|
665
|
-
|
666
|
-
end
|
667
|
-
end # initialize
|
668
|
-
|
669
|
-
# Hide new in Base
|
670
|
-
private_class_method :new
|
671
|
-
|
672
|
-
# attributes
|
673
|
-
#
|
674
|
-
# Return attribute methods so that a program can determine available
|
675
|
-
# attributes dynamically without schema awareness
|
676
|
-
def attributes
|
677
|
-
|
678
|
-
send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
|
679
|
-
return @attr_methods.keys.map {|x|x.downcase}.uniq
|
680
|
-
end
|
681
|
-
|
682
|
-
# exists?
|
683
|
-
#
|
684
|
-
# Return whether the entry exists in LDAP or not
|
685
|
-
def exists?
|
686
|
-
|
687
|
-
return @exists
|
688
|
-
end
|
689
|
-
|
690
|
-
# dn
|
691
|
-
#
|
692
|
-
# Return the authoritative dn
|
693
|
-
def dn
|
694
|
-
|
695
|
-
return @dn.dup
|
696
|
-
end
|
697
|
-
|
698
|
-
# validate
|
699
|
-
#
|
700
|
-
# Basic validation:
|
701
|
-
# - Verify that every 'MUST' specified in the schema has a value defined
|
702
|
-
# - Enforcement of undefined attributes is handled in the objectClass= method
|
703
|
-
# Must call enforce_types() first before enforcement can be guaranteed
|
704
|
-
def validate
|
705
|
-
|
706
|
-
# Clean up attr values, etc
|
707
|
-
send(:enforce_types)
|
708
|
-
|
709
|
-
# Validate objectclass settings
|
710
|
-
@data['objectClass'].each do |klass|
|
711
|
-
unless klass.class == String
|
712
|
-
raise TypeError, "Value in objectClass array is not a String. (#{klass.class}:#{klass.inspect})"
|
713
|
-
end
|
714
|
-
unless Base.schema.names("objectClasses").member? klass
|
715
|
-
raise ObjectClassError, "objectClass '#{klass}' unknown to LDAP server."
|
716
|
-
end
|
717
|
-
end
|
718
|
-
|
719
|
-
# make sure this doesn't drop any of the required objectclasses
|
720
|
-
required_classes().each do |oc|
|
721
|
-
unless @data['objectClass'].member? oc.to_s
|
722
|
-
raise ObjectClassError, "'#{oc}' must be a defined objectClass for class '#{self.class}' as set in the ldap_mapping"
|
723
|
-
end
|
724
|
-
end
|
725
|
-
|
726
|
-
# Make sure all MUST attributes have a value
|
727
|
-
@data['objectClass'].each do |objc|
|
728
|
-
@must.each do |req_attr|
|
729
|
-
# Downcase to ensure we catch schema problems
|
730
|
-
deref = @attr_methods[req_attr.downcase]
|
731
|
-
# Set default if it wasn't yet set.
|
732
|
-
@data[deref] = [] if @data[deref].nil?
|
733
|
-
# Check for missing requirements.
|
734
|
-
if @data[deref].empty?
|
735
|
-
raise AttributeEmpty,
|
736
|
-
"objectClass '#{objc}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'"
|
737
|
-
end
|
738
|
-
end
|
739
|
-
end
|
740
|
-
|
741
|
-
end
|
742
|
-
|
743
|
-
# delete
|
744
|
-
#
|
745
|
-
# Delete this entry from LDAP
|
746
|
-
def delete
|
747
|
-
|
748
|
-
Base.connection(DeleteError.new(
|
749
|
-
"Failed to delete LDAP entry: '#{@dn}'")) do |conn|
|
750
|
-
conn.delete(@dn)
|
751
|
-
@exists = false
|
752
|
-
end
|
753
|
-
end
|
754
|
-
|
755
|
-
|
756
|
-
# write
|
757
|
-
#
|
758
|
-
# Write and validate this object into LDAP
|
759
|
-
# either adding or replacing attributes
|
760
|
-
# TODO: Binary data support
|
761
|
-
# TODO: Relative DN support
|
762
|
-
def write
|
763
|
-
|
764
|
-
# Validate against the objectClass requirements
|
765
|
-
validate
|
766
|
-
|
767
|
-
# Put all changes into one change entry to ensure
|
768
|
-
# automatic rollback upon failure.
|
769
|
-
entry = []
|
770
|
-
|
771
|
-
|
772
|
-
# Expand subtypes to real ldap_data entries
|
773
|
-
# We can't reuse @ldap_data because an exception would leave
|
774
|
-
# an object in an unknown state
|
775
|
-
|
776
|
-
ldap_data = Marshal.load(Marshal.dump(@ldap_data))
|
777
|
-
|
778
|
-
|
779
|
-
ldap_data.keys.each do |key|
|
780
|
-
ldap_data[key].each do |value|
|
781
|
-
if value.class == Hash
|
782
|
-
suffix, real_value = extract_subtypes(value)
|
783
|
-
if ldap_data.has_key? key + suffix
|
784
|
-
ldap_data[key + suffix].push(real_value)
|
785
|
-
else
|
786
|
-
ldap_data[key + suffix] = real_value
|
787
|
-
end
|
788
|
-
ldap_data[key].delete(value)
|
789
|
-
end
|
790
|
-
end
|
791
|
-
end
|
792
|
-
|
793
|
-
|
794
|
-
# Expand subtypes to real data entries, but leave @data alone
|
795
|
-
|
796
|
-
data = Marshal.load(Marshal.dump(@data))
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
bad_attrs = @data.keys - (@must+@may)
|
801
|
-
bad_attrs.each do |removeme|
|
802
|
-
data.delete(removeme)
|
803
|
-
end
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
data.keys.each do |key|
|
809
|
-
data[key].each do |value|
|
810
|
-
if value.class == Hash
|
811
|
-
suffix, real_value = extract_subtypes(value)
|
812
|
-
if data.has_key? key + suffix
|
813
|
-
data[key + suffix].push(real_value)
|
814
|
-
else
|
815
|
-
data[key + suffix] = real_value
|
816
|
-
end
|
817
|
-
data[key].delete(value)
|
818
|
-
end
|
819
|
-
end
|
820
|
-
end
|
821
|
-
|
822
|
-
|
823
|
-
if @exists
|
824
|
-
# Cycle through all attrs to determine action
|
825
|
-
action = {}
|
826
|
-
|
827
|
-
replaceable = []
|
828
|
-
# Now that all the subtypes will be treated as unique attributes
|
829
|
-
# we can see what's changed and add anything that is brand-spankin'
|
830
|
-
# new.
|
831
|
-
|
832
|
-
ldap_data.each do |pair|
|
833
|
-
suffix = ''
|
834
|
-
binary = 0
|
835
|
-
|
836
|
-
name, *suffix_a = pair[0].split(/;/)
|
837
|
-
suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0
|
838
|
-
name = @attr_methods[name]
|
839
|
-
name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals
|
840
|
-
value = data[name+suffix]
|
841
|
-
# If it doesn't exist, don't freak out.
|
842
|
-
value = [] if value.nil?
|
843
|
-
|
844
|
-
# Detect subtypes and account for them
|
845
|
-
binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
|
846
|
-
|
847
|
-
replaceable.push(name+suffix)
|
848
|
-
if pair[1] != value
|
849
|
-
# Create mod entries
|
850
|
-
if not value.empty?
|
851
|
-
# Ditched delete then replace because attribs with no equality match rules
|
852
|
-
# will fails
|
853
|
-
|
854
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value))
|
855
|
-
else
|
856
|
-
# Since some types do not have equality matching rules, delete doesn't work
|
857
|
-
# Replacing with nothing is equivalent.
|
858
|
-
|
859
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, []))
|
860
|
-
end
|
861
|
-
end
|
862
|
-
end
|
863
|
-
|
864
|
-
|
865
|
-
data.each do |pair|
|
866
|
-
suffix = ''
|
867
|
-
binary = 0
|
868
|
-
|
869
|
-
name, *suffix_a = pair[0].split(/;/)
|
870
|
-
suffix = ';' + suffix_a.join(';') if suffix_a.size > 0
|
871
|
-
name = @attr_methods[name]
|
872
|
-
name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals
|
873
|
-
value = pair[1]
|
874
|
-
# Make sure to change this to an Array if there was mistake earlier.
|
875
|
-
value = [] if value.nil?
|
876
|
-
|
877
|
-
if not replaceable.member? name+suffix
|
878
|
-
# Detect subtypes and account for them
|
879
|
-
binary = LDAP::LDAP_MOD_BVALUES if Base.schema.binary? name
|
880
|
-
|
881
|
-
# REPLACE will function like ADD, but doesn't hit EQUALITY problems
|
882
|
-
# TODO: Added equality(attr) to Schema2
|
883
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value)) unless value.empty?
|
884
|
-
end
|
885
|
-
end
|
886
|
-
|
887
|
-
Base.connection(WriteError.new(
|
888
|
-
"Failed to modify: '#{entry}'")) do |conn|
|
889
|
-
|
890
|
-
conn.modify(@dn, entry)
|
891
|
-
|
892
|
-
end
|
893
|
-
else # add everything!
|
894
|
-
|
895
|
-
|
896
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()],
|
897
|
-
data[@attr_methods[dnattr()]]))
|
898
|
-
|
899
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass',
|
900
|
-
data[@attr_methods['objectClass']]))
|
901
|
-
data.each do |pair|
|
902
|
-
if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()]
|
903
|
-
# Detect subtypes and account for them
|
904
|
-
if Base.schema.binary? pair[0].split(/;/)[0]
|
905
|
-
binary = LDAP::LDAP_MOD_BVALUES
|
906
|
-
else
|
907
|
-
binary = 0
|
908
|
-
end
|
909
|
-
|
910
|
-
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
|
911
|
-
end
|
912
|
-
end
|
913
|
-
Base.connection(WriteError.new(
|
914
|
-
"Failed to add: '#{entry}'")) do |conn|
|
915
|
-
|
916
|
-
conn.add(@dn, entry)
|
917
|
-
|
918
|
-
@exists = true
|
919
|
-
end
|
920
|
-
end
|
921
|
-
|
922
|
-
@ldap_data = Marshal.load(Marshal.dump(data))
|
923
|
-
# Delete items disallowed by objectclasses.
|
924
|
-
# They should have been removed from ldap.
|
925
|
-
|
926
|
-
bad_attrs.each do |removeme|
|
927
|
-
@ldap_data.delete(removeme)
|
928
|
-
end
|
929
|
-
|
930
|
-
|
931
|
-
end
|
932
|
-
|
933
|
-
# method_missing
|
934
|
-
#
|
935
|
-
# If a given method matches an attribute or an attribute alias
|
936
|
-
# then call the appropriate method.
|
937
|
-
# TODO: Determine if it would be better to define each allowed method
|
938
|
-
# using class_eval instead of using method_missing. This would
|
939
|
-
# give tab completion in irb.
|
940
|
-
def method_missing(name, *args)
|
941
|
-
|
942
|
-
|
943
|
-
# dynamically update the available attributes without requiring an
|
944
|
-
# explicit call. The cache 'last_oc' saves a lot of cpu time.
|
945
|
-
if @data['objectClass'] != @last_oc
|
946
|
-
|
947
|
-
send(:apply_objectclass, @data['objectClass'])
|
948
|
-
end
|
949
|
-
key = name.to_s
|
950
|
-
case key
|
951
|
-
when /^(\S+)=$/
|
952
|
-
real_key = $1
|
953
|
-
|
954
|
-
if @attr_methods.has_key? real_key
|
955
|
-
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1
|
956
|
-
|
957
|
-
return send(:attribute_method=, real_key, args[0])
|
958
|
-
end
|
959
|
-
else
|
960
|
-
|
961
|
-
if @attr_methods.has_key? key
|
962
|
-
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
|
963
|
-
return attribute_method(key, *args)
|
964
|
-
end
|
965
|
-
end
|
966
|
-
raise NoMethodError, "undefined method `#{key}' for #{self}"
|
967
|
-
end
|
968
|
-
|
969
|
-
# Add available attributes to the methods
|
970
|
-
alias_method :__methods, :methods
|
971
|
-
def methods
|
972
|
-
return __methods + attributes()
|
973
|
-
end
|
974
|
-
|
975
|
-
|
976
|
-
private
|
977
|
-
|
978
|
-
# import(LDAP::Entry)
|
979
|
-
#
|
980
|
-
# Overwrites an existing entry (usually called by new)
|
981
|
-
# with the data given in the data given in LDAP::Entry.
|
982
|
-
#
|
983
|
-
def import(entry=nil)
|
984
|
-
|
985
|
-
if entry.class != LDAP::Entry
|
986
|
-
raise TypeError, "argument must be a LDAP::Entry"
|
987
|
-
end
|
988
|
-
|
989
|
-
@data = {} # where the r/w entry data is stored
|
990
|
-
@ldap_data = {} # original ldap entry data
|
991
|
-
@attr_methods = {} # list of valid method calls for attributes used for dereferencing
|
992
|
-
|
993
|
-
# Get some attributes
|
994
|
-
@dn = entry.dn
|
995
|
-
entry.attrs.each do |attr|
|
996
|
-
# Load with subtypes just like @data
|
997
|
-
|
998
|
-
safe_attr, value = make_subtypes(attr, entry.vals(attr).dup)
|
999
|
-
|
1000
|
-
# Add subtype to any existing values
|
1001
|
-
if @ldap_data.has_key? safe_attr
|
1002
|
-
value.each do |v|
|
1003
|
-
@ldap_data[safe_attr].push(v)
|
1004
|
-
end
|
1005
|
-
else
|
1006
|
-
@ldap_data[safe_attr] = value
|
1007
|
-
end
|
1008
|
-
end
|
1009
|
-
# Assume if we are importing it that it exists
|
1010
|
-
@exists = true
|
1011
|
-
# Populate schema data
|
1012
|
-
send(:apply_objectclass, @ldap_data['objectClass'])
|
1013
|
-
|
1014
|
-
# Populate real data now that we have the schema with aliases
|
1015
|
-
@ldap_data.each do |pair|
|
1016
|
-
real_attr = @attr_methods[pair[0]]
|
1017
|
-
|
1018
|
-
@data[real_attr] = pair[1].dup
|
1019
|
-
|
1020
|
-
end
|
1021
|
-
end # import
|
1022
|
-
|
1023
|
-
# enforce_types
|
1024
|
-
#
|
1025
|
-
# enforce_types applies your changes without attempting to write to LDAP. This means that
|
1026
|
-
# if you set userCertificate to somebinary value, it will wrap it up correctly.
|
1027
|
-
def enforce_types
|
1028
|
-
|
1029
|
-
send(:apply_objectclass, @data['objectClass']) if @data['objectClass'] != @last_oc
|
1030
|
-
# Enforce attribute value formatting
|
1031
|
-
@data.keys.each do |key|
|
1032
|
-
@data[key] = attribute_input_handler(key, @data[key])
|
1033
|
-
end
|
1034
|
-
|
1035
|
-
return true
|
1036
|
-
end
|
1037
|
-
|
1038
|
-
# apply_objectclass
|
1039
|
-
#
|
1040
|
-
# objectClass= special case for updating appropriately
|
1041
|
-
# This updates the objectClass entry in @data. It also
|
1042
|
-
# updating all required and allowed attributes while
|
1043
|
-
# removing defined attributes that are no longer valid
|
1044
|
-
# given the new objectclasses.
|
1045
|
-
def apply_objectclass(val)
|
1046
|
-
|
1047
|
-
new_oc = val
|
1048
|
-
new_oc = [val] if new_oc.class != Array
|
1049
|
-
if defined?(@last_oc).nil?
|
1050
|
-
@last_oc = false
|
1051
|
-
end
|
1052
|
-
return new_oc if @last_oc == new_oc
|
1053
|
-
|
1054
|
-
# Store for caching purposes
|
1055
|
-
@last_oc = new_oc.dup
|
1056
|
-
|
1057
|
-
# Set the actual objectClass data
|
1058
|
-
define_attribute_methods('objectClass')
|
1059
|
-
@data['objectClass'] = new_oc.uniq
|
1060
|
-
|
1061
|
-
# Build |data| from schema
|
1062
|
-
# clear attr_method mapping first
|
1063
|
-
@attr_methods = {}
|
1064
|
-
@must = []
|
1065
|
-
@may = []
|
1066
|
-
new_oc.each do |objc|
|
1067
|
-
# get all attributes for the class
|
1068
|
-
attributes = Base.schema.class_attributes(objc.to_s)
|
1069
|
-
@must += attributes[:must]
|
1070
|
-
@may += attributes[:may]
|
1071
|
-
end
|
1072
|
-
@must.uniq!
|
1073
|
-
@may.uniq!
|
1074
|
-
(@must+@may).each do |attr|
|
1075
|
-
# Update attr_method with appropriate
|
1076
|
-
define_attribute_methods(attr)
|
1077
|
-
end
|
1078
|
-
end
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
# Enforce typing:
|
1083
|
-
# Hashes are for subtypes
|
1084
|
-
# Arrays are for multiple entries
|
1085
|
-
def attribute_input_handler(attr, value)
|
1086
|
-
|
1087
|
-
if attr.nil?
|
1088
|
-
raise RuntimeError, 'The first argument, attr, must not be nil. Please report this as a bug!'
|
1089
|
-
end
|
1090
|
-
binary = Base.schema.binary_required? attr
|
1091
|
-
single = Base.schema.single_value? attr
|
1092
|
-
case value.class.to_s
|
1093
|
-
when 'Array'
|
1094
|
-
if single and value.size > 1
|
1095
|
-
raise TypeError, "Attribute #{attr} can only have a single value"
|
1096
|
-
end
|
1097
|
-
value.map! do |entry|
|
1098
|
-
if entry.class != Hash
|
1099
|
-
|
1100
|
-
entry = entry.to_s
|
1101
|
-
end
|
1102
|
-
entry = attribute_input_handler(attr, entry)[0]
|
1103
|
-
end
|
1104
|
-
when 'Hash'
|
1105
|
-
if value.keys.size > 1
|
1106
|
-
raise TypeError, "Hashes must have one key-value pair only."
|
1107
|
-
end
|
1108
|
-
unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
|
1109
|
-
@@logger.warn("unknown subtype did not match lang-* or binary: #{value.keys[0]}")
|
1110
|
-
end
|
1111
|
-
# Contents MUST be a String or an Array
|
1112
|
-
if value.keys[0] != 'binary' and binary
|
1113
|
-
suffix, real_value = extract_subtypes(value)
|
1114
|
-
value = make_subtypes(name + suffix + ';binary', real_value)
|
1115
|
-
end
|
1116
|
-
value = [value]
|
1117
|
-
when 'String'
|
1118
|
-
if binary
|
1119
|
-
value = {'binary' => value}
|
1120
|
-
end
|
1121
|
-
return [value]
|
1122
|
-
else
|
1123
|
-
value = [value.to_s]
|
1124
|
-
end
|
1125
|
-
return value
|
1126
|
-
end
|
1127
|
-
|
1128
|
-
# make_subtypes
|
1129
|
-
#
|
1130
|
-
# Makes the Hashized value from the full attributename
|
1131
|
-
# e.g. userCertificate;binary => "some_bin"
|
1132
|
-
# becomes userCertificate => {"binary" => "some_bin"}
|
1133
|
-
def make_subtypes(attr, value)
|
1134
|
-
|
1135
|
-
return [attr, value] unless attr.match(/;/)
|
1136
|
-
|
1137
|
-
ret_attr, *subtypes = attr.split(/;/)
|
1138
|
-
return [ret_attr, [make_subtypes_helper(subtypes, value)]]
|
1139
|
-
end
|
1140
|
-
|
1141
|
-
# make_subtypes_helper
|
1142
|
-
#
|
1143
|
-
# This is a recursive function for building
|
1144
|
-
# nested hashed from multi-subtyped values
|
1145
|
-
def make_subtypes_helper(subtypes, value)
|
1146
|
-
|
1147
|
-
return value if subtypes.size == 0
|
1148
|
-
return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)}
|
1149
|
-
end
|
1150
|
-
|
1151
|
-
# extract_subtypes
|
1152
|
-
#
|
1153
|
-
# Extracts all of the subtypes from a given set of nested hashes
|
1154
|
-
# and returns the attribute suffix and the final true value
|
1155
|
-
def extract_subtypes(value)
|
1156
|
-
|
1157
|
-
subtype = ''
|
1158
|
-
ret_val = value
|
1159
|
-
if value.class == Hash
|
1160
|
-
subtype = ';' + value.keys[0]
|
1161
|
-
ret_val = value[value.keys[0]]
|
1162
|
-
subsubtype = ''
|
1163
|
-
if ret_val.class == Hash
|
1164
|
-
subsubtype, ret_val = extract_subtypes(ret_val)
|
1165
|
-
end
|
1166
|
-
subtype += subsubtype
|
1167
|
-
end
|
1168
|
-
ret_val = [ret_val] unless ret_val.class == Array
|
1169
|
-
return subtype, ret_val
|
1170
|
-
end
|
1171
|
-
|
1172
|
-
|
1173
|
-
# Performs the actually connection. This separate so that it may
|
1174
|
-
# be called to refresh stale connections.
|
1175
|
-
def Base.do_connect()
|
1176
|
-
begin
|
1177
|
-
case @@config[:method]
|
1178
|
-
when :ssl
|
1179
|
-
@@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
|
1180
|
-
when :tls
|
1181
|
-
@@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true)
|
1182
|
-
when :plain
|
1183
|
-
@@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
|
1184
|
-
else
|
1185
|
-
raise ConfigurationError,"#{@@config[:method]} is not one of the available connect methods :ssl, :tls, or :plain"
|
1186
|
-
end
|
1187
|
-
rescue ConfigurationError => e
|
1188
|
-
# Pass through
|
1189
|
-
raise e
|
1190
|
-
rescue => e
|
1191
|
-
@@logger.error("Failed to connect using #{@@config[:method]}")
|
1192
|
-
raise e
|
1193
|
-
end
|
1194
|
-
|
1195
|
-
# Enforce LDAPv3
|
1196
|
-
Base.connection(nil, false) do |conn|
|
1197
|
-
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
|
1198
|
-
end
|
1199
|
-
|
1200
|
-
# Authenticate
|
1201
|
-
do_bind
|
1202
|
-
|
1203
|
-
# Retrieve the schema. We need this to automagically determine attributes
|
1204
|
-
exc = ConnectionError.new("Unable to retrieve schema from server (#{@@config[:method]})")
|
1205
|
-
Base.connection(exc, false) do |conn|
|
1206
|
-
@@schema = @@conn.schema2() if @@schema.nil?
|
1207
|
-
end
|
1208
|
-
|
1209
|
-
end
|
1210
|
-
|
1211
|
-
# Wrapper all bind activity
|
1212
|
-
def Base.do_bind()
|
1213
|
-
bind_dn = @@config[:bind_format] % [@@config[:user]]
|
1214
|
-
# Rough bind loop:
|
1215
|
-
# Attempt 1: SASL if available
|
1216
|
-
# Attempt 2: SIMPLE with credentials if password block
|
1217
|
-
# Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
|
1218
|
-
if @@config[:try_sasl] and do_sasl_bind(bind_dn)
|
1219
|
-
@@logger.info('Bound SASL')
|
1220
|
-
elsif do_simple_bind(bind_dn)
|
1221
|
-
@@logger.info('Bound simple')
|
1222
|
-
elsif @@config[:allow_anonymous] and do_anonymous_bind(bind_dn)
|
1223
|
-
@@logger.info('Bound anonymous')
|
1224
|
-
else
|
1225
|
-
raise *LDAP::err2exception(@@conn.err) if @@conn.err != 0
|
1226
|
-
raise AuthenticationError, 'All authentication methods exhausted.'
|
1227
|
-
end
|
1228
|
-
|
1229
|
-
return @@conn.bound?
|
1230
|
-
end
|
1231
|
-
private_class_method :do_bind
|
1232
|
-
|
1233
|
-
# Base.do_anonymous_bind
|
1234
|
-
#
|
1235
|
-
# Bind to LDAP with the given DN, but with no password. (anonymous!)
|
1236
|
-
def Base.do_anonymous_bind(bind_dn)
|
1237
|
-
@@logger.info "Attempting anonymous authentication"
|
1238
|
-
Base.connection(nil, false) do |conn|
|
1239
|
-
conn.bind()
|
1240
|
-
return true
|
1241
|
-
end
|
1242
|
-
return false
|
1243
|
-
end
|
1244
|
-
private_class_method :do_anonymous_bind
|
1245
|
-
|
1246
|
-
# Base.do_simple_bind
|
1247
|
-
#
|
1248
|
-
# Bind to LDAP with the given DN and password
|
1249
|
-
def Base.do_simple_bind(bind_dn)
|
1250
|
-
# Bail if we have no password or password block
|
1251
|
-
if @@config[:password_block].nil? and @@config[:password].nil?
|
1252
|
-
return false
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
# TODO: Give a warning to reconnect users with password clearing
|
1256
|
-
# Get the passphrase for the first time, or anew if we aren't storing
|
1257
|
-
password = ''
|
1258
|
-
if not @@config[:password].nil?
|
1259
|
-
password = @@config[:password]
|
1260
|
-
elsif not @@config[:password_block].nil?
|
1261
|
-
unless @@config[:password_block].respond_to?(:call)
|
1262
|
-
@@logger.error('Skipping simple bind: ' +
|
1263
|
-
':password_block not nil or Proc object. Ignoring.')
|
1264
|
-
return false
|
1265
|
-
end
|
1266
|
-
password = @@config[:password_block].call
|
1267
|
-
else
|
1268
|
-
@@logger.error('Skipping simple bind: ' +
|
1269
|
-
':password_block and :password options are empty.')
|
1270
|
-
return false
|
1271
|
-
end
|
1272
|
-
|
1273
|
-
# Store the password for quick reference later
|
1274
|
-
if @@config[:store_password]
|
1275
|
-
@@config[:password] = password
|
1276
|
-
elsif @@config[:store_password] == false
|
1277
|
-
@@config[:password] = nil
|
1278
|
-
end
|
1279
|
-
|
1280
|
-
Base.connection(nil, false) do |conn|
|
1281
|
-
conn.bind(bind_dn, password)
|
1282
|
-
return true
|
1283
|
-
end
|
1284
|
-
return false
|
1285
|
-
end
|
1286
|
-
private_class_method :do_simple_bind
|
1287
|
-
|
1288
|
-
# Base.do_sasl_bind
|
1289
|
-
#
|
1290
|
-
# Bind to LDAP with the given DN using any available SASL methods
|
1291
|
-
def Base.do_sasl_bind(bind_dn)
|
1292
|
-
# Get all SASL mechanisms
|
1293
|
-
#
|
1294
|
-
mechanisms = []
|
1295
|
-
exc = ConnectionError.new('Root DSE query failed')
|
1296
|
-
Base.connection(exc, false) do |conn|
|
1297
|
-
mechanisms = conn.root_dse[0]['supportedSASLMechanisms']
|
1298
|
-
end
|
1299
|
-
# Use GSSAPI if available
|
1300
|
-
# Currently only GSSAPI is supported with Ruby/LDAP from
|
1301
|
-
# http://caliban.org/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
|
1302
|
-
# TODO: Investigate further SASL support
|
1303
|
-
if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
|
1304
|
-
Base.connection(nil, false) do |conn|
|
1305
|
-
conn.sasl_quiet = @@config[:sasl_quiet] if @@config.has_key?(:sasl_quiet)
|
1306
|
-
conn.sasl_bind(bind_dn, 'GSSAPI')
|
1307
|
-
return true
|
1308
|
-
end
|
1309
|
-
end
|
1310
|
-
return false
|
1311
|
-
end
|
1312
|
-
private_class_method :do_sasl_bind
|
1313
|
-
|
1314
|
-
# base
|
1315
|
-
#
|
1316
|
-
# Returns the value of self.class.base
|
1317
|
-
# This is just syntactic sugar
|
1318
|
-
def base
|
1319
|
-
|
1320
|
-
self.class.base
|
1321
|
-
end
|
1322
|
-
|
1323
|
-
# ldap_scope
|
1324
|
-
#
|
1325
|
-
# Returns the value of self.class.ldap_scope
|
1326
|
-
# This is just syntactic sugar
|
1327
|
-
def ldap_scope
|
1328
|
-
|
1329
|
-
self.class.ldap_scope
|
1330
|
-
end
|
1331
|
-
|
1332
|
-
# required_classes
|
1333
|
-
#
|
1334
|
-
# Returns the value of self.class.required_classes
|
1335
|
-
# This is just syntactic sugar
|
1336
|
-
def required_classes
|
1337
|
-
|
1338
|
-
self.class.required_classes
|
1339
|
-
end
|
1340
|
-
|
1341
|
-
# dnattr
|
1342
|
-
#
|
1343
|
-
# Returns the value of self.class.dnattr
|
1344
|
-
# This is just syntactic sugar
|
1345
|
-
def dnattr
|
1346
|
-
|
1347
|
-
self.class.dnattr
|
1348
|
-
end
|
1349
|
-
|
1350
|
-
# attribute_method
|
1351
|
-
#
|
1352
|
-
# Return the value of the attribute called by method_missing?
|
1353
|
-
def attribute_method(method, not_array = false)
|
1354
|
-
|
1355
|
-
attr = @attr_methods[method]
|
1356
|
-
|
1357
|
-
# Set the default value to empty if attr is not set.
|
1358
|
-
@data[attr] = [] if @data[attr].nil?
|
1359
|
-
|
1360
|
-
# Return a copy of the stored data
|
1361
|
-
return array_of(@data[attr].dup, false) if not_array
|
1362
|
-
return @data[attr]
|
1363
|
-
end
|
1364
|
-
|
1365
|
-
|
1366
|
-
# attribute_method=
|
1367
|
-
#
|
1368
|
-
# Set the value of the attribute called by method_missing?
|
1369
|
-
def attribute_method=(method, value)
|
1370
|
-
|
1371
|
-
# Get the attr and clean up the input
|
1372
|
-
attr = @attr_methods[method]
|
1373
|
-
|
1374
|
-
|
1375
|
-
# Check if it is the DN attribute
|
1376
|
-
if dnattr() == attr
|
1377
|
-
raise AttributeAssignmentError, 'cannot modify the DN attribute value'
|
1378
|
-
end
|
1379
|
-
|
1380
|
-
# Enforce LDAP-pleasing values
|
1381
|
-
|
1382
|
-
real_value = value
|
1383
|
-
# Squash empty values
|
1384
|
-
if value.class == Array
|
1385
|
-
real_value = value.collect {|c| if c == ''; []; else c; end }.flatten
|
1386
|
-
end
|
1387
|
-
real_value = [] if real_value.nil?
|
1388
|
-
real_value = [] if real_value == ''
|
1389
|
-
real_value = [real_value] if real_value.class == String
|
1390
|
-
real_value = [real_value.to_s] if real_value.class == Fixnum
|
1391
|
-
# NOTE: Hashes are allowed for subtyping.
|
1392
|
-
|
1393
|
-
# Assign the value
|
1394
|
-
@data[attr] = real_value
|
1395
|
-
|
1396
|
-
# Return the passed in value
|
1397
|
-
|
1398
|
-
return @data[attr]
|
1399
|
-
end
|
1400
|
-
|
1401
|
-
|
1402
|
-
# define_attribute_methods
|
1403
|
-
#
|
1404
|
-
# Make a method entry for _every_ alias of a valid attribute and map it
|
1405
|
-
# onto the first attribute passed in.
|
1406
|
-
def define_attribute_methods(attr)
|
1407
|
-
|
1408
|
-
if @attr_methods.has_key? attr
|
1409
|
-
return
|
1410
|
-
end
|
1411
|
-
aliases = Base.schema.attribute_aliases(attr)
|
1412
|
-
aliases.each do |ali|
|
1413
|
-
|
1414
|
-
@attr_methods[ali] = attr
|
1415
|
-
|
1416
|
-
@attr_methods[ali.downcase] = attr
|
1417
|
-
end
|
1418
|
-
|
1419
|
-
end
|
1420
|
-
|
1421
|
-
# array_of
|
1422
|
-
#
|
1423
|
-
# Returns the array form of a value, or not an array if
|
1424
|
-
# false is passed in.
|
1425
|
-
def array_of(value, to_a = true)
|
1426
|
-
|
1427
|
-
if to_a
|
1428
|
-
case value.class.to_s
|
1429
|
-
when 'Array'
|
1430
|
-
return value
|
1431
|
-
when 'Hash'
|
1432
|
-
return [value]
|
1433
|
-
else
|
1434
|
-
return [value.to_s]
|
1435
|
-
end
|
1436
|
-
else
|
1437
|
-
case value.class.to_s
|
1438
|
-
when 'Array'
|
1439
|
-
return nil if value.size == 0
|
1440
|
-
return value[0] if value.size == 1
|
1441
|
-
return value
|
1442
|
-
when 'Hash'
|
1443
|
-
return value
|
1444
|
-
else
|
1445
|
-
return value.to_s
|
1446
|
-
end
|
1447
|
-
end
|
1448
|
-
end
|
1449
|
-
|
1450
|
-
end # Base
|
1451
|
-
|
1452
|
-
end # ActiveLDAP
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|