ruby-activeldap 0.4.1
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/lib/activeldap/associations.rb +122 -0
- data/lib/activeldap/base.rb +1094 -0
- data/lib/activeldap/configuration.rb +25 -0
- data/lib/activeldap/schema2.rb +141 -0
- data/lib/activeldap.rb +889 -0
- metadata +53 -0
@@ -0,0 +1,1094 @@
|
|
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 Will Drewry <will@alum.bu.edu>
|
5
|
+
#
|
6
|
+
# == Summary
|
7
|
+
# ActiveLDAP lets you read and update LDAP entries in a completely object
|
8
|
+
# oriented fashion, even handling attributes with multiple names seamlessly.
|
9
|
+
# It was inspired by ActiveRecord so extending it to deal with custom
|
10
|
+
# LDAP schemas is as effortless as knowing the 'ou' of the objects, and the
|
11
|
+
# primary key. (fix this up some)
|
12
|
+
#
|
13
|
+
# == Example
|
14
|
+
# irb> require 'activeldap'
|
15
|
+
# > true
|
16
|
+
# irb> user = ActiveLDAP::User.new("drewry")
|
17
|
+
# > #<ActiveLDAP::User:0x402e...
|
18
|
+
# irb> user.cn
|
19
|
+
# > "foo"
|
20
|
+
# irb> user.commonname
|
21
|
+
# > "foo"
|
22
|
+
# irb> user.cn = "Will Drewry"
|
23
|
+
# > "Will Drewry"
|
24
|
+
# irb> user.cn
|
25
|
+
# > "Will Drewry"
|
26
|
+
# irb> user.validate
|
27
|
+
# > nil
|
28
|
+
# irb> user.write
|
29
|
+
#
|
30
|
+
#
|
31
|
+
|
32
|
+
require 'ldap'
|
33
|
+
require 'ldap/schema'
|
34
|
+
require 'log4r'
|
35
|
+
|
36
|
+
module ActiveLDAP
|
37
|
+
# OO-interface to LDAP assuming pam/nss_ldap-style comanization with Active specifics
|
38
|
+
# Each subclass does a ldapsearch for the matching entry.
|
39
|
+
# If no exact match, raise an error.
|
40
|
+
# If match, change all LDAP attributes in accessor attributes on the object.
|
41
|
+
# -- these are ACTUALLY populated from schema - see subschema.rb example
|
42
|
+
# -- @conn.schema().each{|k,vs| vs.each{|v| print("#{k}: #{v}\n")}}
|
43
|
+
# -- extract objectClasses from match and populate
|
44
|
+
# Multiple entries become lists.
|
45
|
+
# If this isn't read-only then lists become multiple entries, etc.
|
46
|
+
|
47
|
+
# AttributeEmpty
|
48
|
+
#
|
49
|
+
# An exception raised when a required attribute is found to be empty
|
50
|
+
class AttributeEmpty < RuntimeError
|
51
|
+
end
|
52
|
+
|
53
|
+
# DeleteError
|
54
|
+
#
|
55
|
+
# An exception raised when an ActiveLDAP delete action fails
|
56
|
+
class DeleteError < RuntimeError
|
57
|
+
end
|
58
|
+
|
59
|
+
# WriteError
|
60
|
+
#
|
61
|
+
# An exception raised when an ActiveLDAP write action fails
|
62
|
+
class WriteError < RuntimeError
|
63
|
+
end
|
64
|
+
|
65
|
+
# AuthenticationError
|
66
|
+
#
|
67
|
+
# An exception raised when user authentication fails
|
68
|
+
class AuthenticationError < RuntimeError
|
69
|
+
end
|
70
|
+
|
71
|
+
# ConnectionError
|
72
|
+
#
|
73
|
+
# An exception raised when the LDAP conenction fails
|
74
|
+
class ConnectionError < RuntimeError
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Base
|
79
|
+
#
|
80
|
+
# Base is the primary class which contains all of the core
|
81
|
+
# ActiveLDAP functionality. It is meant to only ever be subclassed
|
82
|
+
# by extension classes.
|
83
|
+
class Base
|
84
|
+
# Parsed schema structures
|
85
|
+
attr_reader :must, :may
|
86
|
+
attr_accessor :logger
|
87
|
+
|
88
|
+
# All class-wide variables
|
89
|
+
@@config = nil # Container for current connection settings
|
90
|
+
@@schema = nil # LDAP server's schema
|
91
|
+
@@conn = nil # LDAP connection
|
92
|
+
|
93
|
+
# Driver generator
|
94
|
+
#
|
95
|
+
# TODO add type checking
|
96
|
+
# This let's you call this method to create top-level extension object. This
|
97
|
+
# is really just a proof of concept and has not truly useful purpose.
|
98
|
+
# example: Base.create_object(:class => "user", :dnattr => "uid", :classes => ['top'])
|
99
|
+
#
|
100
|
+
def Base.create_object(config={})
|
101
|
+
# Just upcase the first letter of the new class name
|
102
|
+
str = config[:class]
|
103
|
+
class_name = str[0].chr.upcase + str[1..-1]
|
104
|
+
|
105
|
+
attr = config[:dnattr] # "uid"
|
106
|
+
prefix = config[:base] # "ou=People"
|
107
|
+
# [ 'top', 'posixAccount' ]
|
108
|
+
classes_array = config[:classes] || []
|
109
|
+
# [ [ :groups, {:class_name => "Group", :foreign_key => "memberUid"}] ]
|
110
|
+
belongs_to_array = config[:belongs_to] || []
|
111
|
+
# [ [ :members, {:class_name => "User", :foreign_key => "uid", :local_key => "memberUid"}] ]
|
112
|
+
has_many_array = config[:has_many] || []
|
113
|
+
|
114
|
+
raise TypeError, ":objectclasses must be an array" unless classes_array.respond_to? :size
|
115
|
+
raise TypeError, ":belongs_to must be an array" unless belongs_to_array.respond_to? :size
|
116
|
+
raise TypeError, ":has_many must be an array" unless has_many_array.respond_to? :size
|
117
|
+
|
118
|
+
# Build classes array
|
119
|
+
classes = '['
|
120
|
+
classes_array.map! {|x| x = "'#{x}'"}
|
121
|
+
classes << classes_array.join(', ')
|
122
|
+
classes << ']'
|
123
|
+
|
124
|
+
# Build belongs_to
|
125
|
+
belongs_to = []
|
126
|
+
if belongs_to_array.size > 0
|
127
|
+
belongs_to_array.each do |bt|
|
128
|
+
line = [ "belongs_to :#{bt[0]}" ]
|
129
|
+
bt[1].keys.each do |key|
|
130
|
+
line << ":#{key} => '#{bt[1][key]}'"
|
131
|
+
end
|
132
|
+
belongs_to << line.join(', ')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Build has_many
|
137
|
+
has_many = []
|
138
|
+
if has_many_array.size > 0
|
139
|
+
has_many_array.each do |hm|
|
140
|
+
line = [ "has_many :#{hm[0]}" ]
|
141
|
+
hm[1].keys.each do |key|
|
142
|
+
line << ":#{key} => '#{hm[1][key]}'"
|
143
|
+
end
|
144
|
+
has_many << line.join(', ')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
self.class.module_eval <<-"end_eval"
|
149
|
+
class ::#{class_name} < ActiveLDAP::Base
|
150
|
+
ldap_mapping :dnattr => "#{attr}, :prefix => "#{prefix}", :classes => "#{classes}"
|
151
|
+
#{belongs_to.join("\n")}
|
152
|
+
#{has_many.join("\n")}
|
153
|
+
end
|
154
|
+
end_eval
|
155
|
+
end
|
156
|
+
|
157
|
+
# Connect and bind to LDAP creating a class variable for use by all ActiveLDAP
|
158
|
+
# objects.
|
159
|
+
#
|
160
|
+
# == +config+
|
161
|
+
# +config+ must be a hash that may contain any of the following fields:
|
162
|
+
# :user, :password_block, :logger, :host, :port, :base, :bind_format, :try_sasl, :allow_anonymous
|
163
|
+
# :user specifies the username to bind with.
|
164
|
+
# :bind_format specifies the string to substitute the username into on bind. e.g. uid=%s,ou=People,dc=example,dc=com. Overrides @@bind_format.
|
165
|
+
# :password_block specifies a Proc object that will yield a String to be used as the password when called.
|
166
|
+
# :logger specifies a preconfigured Log4r::Logger to be used for all logging
|
167
|
+
# :host overrides the configuration.rb @@host setting with the LDAP server hostname
|
168
|
+
# :port overrides the configuration.rb @@port setting for the LDAP server port
|
169
|
+
# :base overwrites Base.base - this affects EVERYTHING
|
170
|
+
# :try_sasl indicates that a SASL bind should be attempted when binding to the server (default: false)
|
171
|
+
# :allow_anonymous indicates that a true anonymous bind is allowed when trying to bind to the server (default: true)
|
172
|
+
def Base.connect(config={}) # :user, :password_block, :logger
|
173
|
+
# Process config
|
174
|
+
# Class options
|
175
|
+
## These will be replace by configuration.rb defaults if defined
|
176
|
+
@@config = {}
|
177
|
+
@@config[:host] = config[:host] || @@host
|
178
|
+
@@config[:port] = config[:port] || @@port
|
179
|
+
if config[:base]
|
180
|
+
Base.class_eval <<-"end_eval"
|
181
|
+
def Base.base
|
182
|
+
'#{config[:base]}'
|
183
|
+
end
|
184
|
+
end_eval
|
185
|
+
end
|
186
|
+
@@config[:bind_format] = config[:bind_format] || @@bind_format
|
187
|
+
|
188
|
+
@@logger = config[:logger] || nil
|
189
|
+
# Setup default logger to console
|
190
|
+
if @@logger.nil?
|
191
|
+
@@logger = Log4r::Logger.new('activeldap')
|
192
|
+
@@logger.level = Log4r::FATAL
|
193
|
+
Log4r::StderrOutputter.new 'console'
|
194
|
+
@@logger.add('console')
|
195
|
+
end
|
196
|
+
|
197
|
+
# Method options
|
198
|
+
user = nil
|
199
|
+
password_block = nil
|
200
|
+
@@config[:allow_anonymous] = true
|
201
|
+
@@config[:try_sasl] = false
|
202
|
+
|
203
|
+
@@config[:user] = config[:user] || user
|
204
|
+
@@config[:allow_anonymous] = config[:allow_anonymous] if config.has_key? :allow_anonymous
|
205
|
+
@@config[:try_sasl] = config[:try_sasl]
|
206
|
+
@@config[:password_block] = config[:password_block] if config.has_key? :password_block
|
207
|
+
|
208
|
+
# Setup bind credentials
|
209
|
+
@@config[:user] = ENV['USER'] unless @@config[:user]
|
210
|
+
|
211
|
+
# Connect to LDAP
|
212
|
+
begin
|
213
|
+
# SSL using START_TLS
|
214
|
+
@@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], true)
|
215
|
+
rescue
|
216
|
+
@@logger.warn "Warning: Failed to connect using TLS!"
|
217
|
+
begin
|
218
|
+
@@logger.warn "Warning: Attempting SSL connection . . ."
|
219
|
+
@@conn = LDAP::SSLConn.new(@@config[:host], @@config[:port], false)
|
220
|
+
# HACK: Load the schema here because otherwise you can't tell if the
|
221
|
+
# HACK: SSLConn is a real SSL connection.
|
222
|
+
@@schema = @@conn.schema() if @@schema.nil?
|
223
|
+
rescue
|
224
|
+
@@logger.warn "Warning: Attempting unencrypted connection . . ."
|
225
|
+
@@conn = LDAP::Conn.new(@@config[:host], @@config[:port])
|
226
|
+
end
|
227
|
+
end
|
228
|
+
@@logger.debug "Connected to #{@@config[:host]}:#{@@config[:port]}."
|
229
|
+
|
230
|
+
# Enforce LDAPv3
|
231
|
+
@@conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
|
232
|
+
|
233
|
+
# Authenticate
|
234
|
+
do_bind
|
235
|
+
|
236
|
+
# Load Schema (if not straight SSL...)
|
237
|
+
begin
|
238
|
+
@@schema = @@conn.schema() if @@schema.nil?
|
239
|
+
rescue => detail
|
240
|
+
raise ConnectionError, "#{detail.exception} - LDAP connection failure, or server does not support schema queries."
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
# Cleanly return
|
245
|
+
return nil
|
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
|
+
@@conn = nil
|
253
|
+
end
|
254
|
+
|
255
|
+
# Return the LDAP connection object currently in use
|
256
|
+
def Base.connection
|
257
|
+
return @@conn
|
258
|
+
end
|
259
|
+
|
260
|
+
# Set the LDAP connection avoiding Base.connect or multiplexing connections
|
261
|
+
def Base.connection=(conn)
|
262
|
+
@@conn = conn
|
263
|
+
end
|
264
|
+
|
265
|
+
# Return the schema object
|
266
|
+
def Base.schema
|
267
|
+
@@schema
|
268
|
+
end
|
269
|
+
|
270
|
+
# search
|
271
|
+
#
|
272
|
+
# Wraps Ruby/LDAP connection.search to make it easier to search for specific
|
273
|
+
# data without cracking open Base.connection
|
274
|
+
def Base.search(config={})
|
275
|
+
unless Base.connection
|
276
|
+
if @@config
|
277
|
+
ActiveLDAP::Base.connect(@@config)
|
278
|
+
else
|
279
|
+
ActiveLDAP::Base.connect
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
config[:filter] = 'objectClass=*' unless config.has_key? :filter
|
284
|
+
config[:attrs] = [] unless config.has_key? :attrs
|
285
|
+
config[:scope] = LDAP::LDAP_SCOPE_SUBTREE unless config.has_key? :scope
|
286
|
+
config[:base] = base() unless config.has_key? :base
|
287
|
+
|
288
|
+
values = []
|
289
|
+
config[:attrs] = config[:attrs].to_a # just in case
|
290
|
+
|
291
|
+
begin
|
292
|
+
@@conn.search(config[:base], config[:scope], config[:filter], config[:attrs]) do |m|
|
293
|
+
res = {}
|
294
|
+
res['dn'] = [m.dn.dup] # For consistency with the below
|
295
|
+
m.attrs.each do |attr|
|
296
|
+
if config[:attrs].member? attr or config[:attrs].empty?
|
297
|
+
res[attr] = m.vals(attr).dup
|
298
|
+
end
|
299
|
+
end
|
300
|
+
values.push(res)
|
301
|
+
end
|
302
|
+
rescue RuntimeError => detail
|
303
|
+
@@logger.debug "No matches for #{config[:filter]} and attrs #{config[:attrs]}"
|
304
|
+
# Do nothing on failure
|
305
|
+
end
|
306
|
+
return values
|
307
|
+
end
|
308
|
+
|
309
|
+
# find
|
310
|
+
#
|
311
|
+
# Finds the first match for value where |value| is the value of some
|
312
|
+
# |field|, or the wildcard match. This is only useful for derived classes.
|
313
|
+
# usage: Subclass.find(:attribute => "cn", :value => "some*val", :objects => true)
|
314
|
+
# Subclass.find('some*val')
|
315
|
+
#
|
316
|
+
def Base.find(config = {})
|
317
|
+
unless Base.connection
|
318
|
+
if @@config
|
319
|
+
ActiveLDAP::Base.connect(@@config)
|
320
|
+
else
|
321
|
+
ActiveLDAP::Base.connect
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
if self.class == Class
|
326
|
+
klass = self.ancestors[0].to_s.split(':').last
|
327
|
+
real_klass = self.ancestors[0]
|
328
|
+
else
|
329
|
+
klass = self.class.to_s.split(':').last
|
330
|
+
real_klass = self.class
|
331
|
+
end
|
332
|
+
|
333
|
+
# Allow a single string argument
|
334
|
+
attr = dnattr()
|
335
|
+
objects = false
|
336
|
+
val = config
|
337
|
+
# Or a hash
|
338
|
+
if config.respond_to?"has_key?"
|
339
|
+
attr = config[:attribute] || dnattr()
|
340
|
+
val = config[:value] || '*'
|
341
|
+
objects = config[:objects]
|
342
|
+
end
|
343
|
+
|
344
|
+
matches = []
|
345
|
+
|
346
|
+
begin
|
347
|
+
# Get some attributes
|
348
|
+
@@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
|
349
|
+
# Extract the dnattr value
|
350
|
+
dnval = m.dn.split(/,/)[0].split(/=/)[1]
|
351
|
+
|
352
|
+
if objects
|
353
|
+
return eval("#{real_klass}.new(dnval)")
|
354
|
+
else
|
355
|
+
return dnval
|
356
|
+
end
|
357
|
+
end
|
358
|
+
rescue RuntimeError => detail
|
359
|
+
@@logger.debug "No matches for #{attr}=#{val}"
|
360
|
+
# Do nothing on failure
|
361
|
+
end
|
362
|
+
return nil
|
363
|
+
end
|
364
|
+
private_class_method :find
|
365
|
+
|
366
|
+
|
367
|
+
# find_all
|
368
|
+
#
|
369
|
+
# Finds all matches for value where |value| is the value of some
|
370
|
+
# |field|, or the wildcard match. This is only useful for derived classes.
|
371
|
+
def Base.find_all(config = {})
|
372
|
+
unless Base.connection
|
373
|
+
if @@config
|
374
|
+
ActiveLDAP::Base.connect(@@config)
|
375
|
+
else
|
376
|
+
ActiveLDAP::Base.connect
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
if self.class == Class
|
381
|
+
real_klass = self.ancestors[0]
|
382
|
+
else
|
383
|
+
real_klass = self.class
|
384
|
+
end
|
385
|
+
|
386
|
+
# Allow a single string argument
|
387
|
+
attr = dnattr()
|
388
|
+
objects = false
|
389
|
+
val = config
|
390
|
+
# Or a hash
|
391
|
+
if config.respond_to?"has_key?"
|
392
|
+
attr = config[:attribute] || dnattr()
|
393
|
+
val = config[:value] || '*'
|
394
|
+
objects = config[:objects]
|
395
|
+
end
|
396
|
+
|
397
|
+
matches = []
|
398
|
+
|
399
|
+
begin
|
400
|
+
# Get some attributes
|
401
|
+
@@conn.search(base(), LDAP::LDAP_SCOPE_SUBTREE, "(#{attr}=#{val})") do |m|
|
402
|
+
# Extract the dnattr value
|
403
|
+
dnval = m.dn.split(/,/)[0].split(/=/)[1]
|
404
|
+
|
405
|
+
if objects
|
406
|
+
matches.push(eval("#{real_klass}.new(dnval)"))
|
407
|
+
else
|
408
|
+
matches.push(dnval)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
rescue RuntimeError => detail
|
412
|
+
#p @@conn.err2string(@@conn.err)
|
413
|
+
@@logger.debug "No matches for #{attr}=#{val}"
|
414
|
+
# Do nothing on failure
|
415
|
+
end
|
416
|
+
return matches
|
417
|
+
end
|
418
|
+
private_class_method :find_all
|
419
|
+
|
420
|
+
# Base.base
|
421
|
+
#
|
422
|
+
# This method when included into Base provides
|
423
|
+
# an inheritable, overwritable configuration setting
|
424
|
+
#
|
425
|
+
# This should be a string with the base of the
|
426
|
+
# ldap server such as 'dc=example,dc=com', and
|
427
|
+
# it should be overwritten by including
|
428
|
+
# configuration.rb into this class.
|
429
|
+
# When subclassing, the specified prefix will be concatenated.
|
430
|
+
def Base.base
|
431
|
+
'dc=example,dc=com'
|
432
|
+
end
|
433
|
+
|
434
|
+
# Base.dnattr
|
435
|
+
#
|
436
|
+
# This is a placeholder for the class method that will
|
437
|
+
# be overridden on calling ldap_mapping in a subclass.
|
438
|
+
# Using a class method allows for clean inheritance from
|
439
|
+
# classes that already have a ldap_mapping.
|
440
|
+
def Base.dnattr
|
441
|
+
''
|
442
|
+
end
|
443
|
+
|
444
|
+
# Base.required_classes
|
445
|
+
#
|
446
|
+
# This method when included into Base provides
|
447
|
+
# an inheritable, overwritable configuration setting
|
448
|
+
#
|
449
|
+
# The value should be the minimum required objectClasses
|
450
|
+
# to make an object in the LDAP server, or an empty array [].
|
451
|
+
# This should be overwritten by configuration.rb.
|
452
|
+
# Note that subclassing does not cause concatenation of
|
453
|
+
# arrays to occurs.
|
454
|
+
def Base.required_classes
|
455
|
+
[]
|
456
|
+
end
|
457
|
+
|
458
|
+
### All instance methods, etc
|
459
|
+
|
460
|
+
# new
|
461
|
+
#
|
462
|
+
# Creates a new instance of Base initializing all class and
|
463
|
+
# instance variables. initialize2() must be called to complete
|
464
|
+
# all initialization.
|
465
|
+
# defines local defaults. See examples
|
466
|
+
# If multiple values exist for dnattr, the first one put here will be authoritative
|
467
|
+
# TODO: Add support for relative distinguished names
|
468
|
+
def initialize(val='')
|
469
|
+
if val.class != String
|
470
|
+
raise TypeError, "Object key must be a String"
|
471
|
+
end
|
472
|
+
# Try a default connection if none made explicitly
|
473
|
+
unless Base.connection
|
474
|
+
# Use @@config if it has been prepopulated and the conn is down.
|
475
|
+
if @@config
|
476
|
+
ActiveLDAP::Base.connect(@@config)
|
477
|
+
else
|
478
|
+
ActiveLDAP::Base.connect
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
@data = {} # where the r/w entry data is stored
|
483
|
+
@data.default = []
|
484
|
+
@ldap_data = {} # original ldap entry data
|
485
|
+
@ldap_data.default = []
|
486
|
+
@attr_methods = {} # list of valid method calls for attributes used for dereferencing
|
487
|
+
@must = {} # list of schema required attributes
|
488
|
+
@may = {} # list of schema optional attributes
|
489
|
+
@sup = {} # list of schema supplemental attributes
|
490
|
+
|
491
|
+
# Break val apart if it is a dn
|
492
|
+
if val.match(/^#{dnattr()}=([^,=]+),#{base()}$/i)
|
493
|
+
val = $1
|
494
|
+
elsif val.match(/[=,]/)
|
495
|
+
@@logger.info "initialize: Changing val from '#{val}' to '' because it doesn't match the DN."
|
496
|
+
val = ''
|
497
|
+
end
|
498
|
+
|
499
|
+
# Do a search - if it exists, pull all data and parse schema, if not, just set the hierarchical data
|
500
|
+
if val.empty?
|
501
|
+
@exists = false
|
502
|
+
# Setup what should eb authoritative
|
503
|
+
@dn = "#{dnattr()}=#{val},#{base()}"
|
504
|
+
send(:objectClass=, required_classes())
|
505
|
+
else # do a search then
|
506
|
+
# Search for the existing entry
|
507
|
+
begin
|
508
|
+
# Get some attributes
|
509
|
+
Base.connection.search("#{dnattr()}=#{val},#{base()}", LDAP::LDAP_SCOPE_SUBTREE, "objectClass=*") do |m|
|
510
|
+
# Save DN
|
511
|
+
@dn = m.dn
|
512
|
+
# Load up data into tmp
|
513
|
+
m.attrs.each do |attr|
|
514
|
+
# Load with subtypes just like @data
|
515
|
+
safe_attr, value = make_subtypes(attr, m.vals(attr).dup)
|
516
|
+
@ldap_data[safe_attr] = value
|
517
|
+
end
|
518
|
+
end
|
519
|
+
@exists = true
|
520
|
+
# Populate schema data
|
521
|
+
send(:objectClass=, @ldap_data['objectClass'])
|
522
|
+
|
523
|
+
# Populate real data now that we have the schema with aliases
|
524
|
+
@ldap_data.each do |pair|
|
525
|
+
send(:attribute_method=, pair[0], pair[1])
|
526
|
+
end
|
527
|
+
|
528
|
+
rescue LDAP::ResultError
|
529
|
+
@exists = false
|
530
|
+
# Create what should be the authoritative DN
|
531
|
+
@dn = "#{dnattr()}=#{val},#{base()}"
|
532
|
+
send(:objectClass=, required_classes())
|
533
|
+
|
534
|
+
# Setup dn attribute (later rdn too!)
|
535
|
+
attr_sym = "#{dnattr()}=".to_sym
|
536
|
+
send(attr_sym, val)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end # initialize2
|
540
|
+
|
541
|
+
# Hide new in Base
|
542
|
+
private_class_method :new
|
543
|
+
|
544
|
+
# attributes
|
545
|
+
#
|
546
|
+
# Return attribute methods so that a program can determine available
|
547
|
+
# attributes dynamically without schema awareness
|
548
|
+
def attributes
|
549
|
+
return @attr_methods.keys
|
550
|
+
end
|
551
|
+
|
552
|
+
# objectClass=
|
553
|
+
#
|
554
|
+
# objectClass= special case for updating appropriately
|
555
|
+
# This updates the objectClass entry in @data. It also
|
556
|
+
# updating all required and allowed attributes while
|
557
|
+
# removing defined attributes that are no longer valid
|
558
|
+
# given the new objectclasses.
|
559
|
+
def objectClass=(val)
|
560
|
+
if val.class != Array
|
561
|
+
raise TypeError, 'objectClass must be an Array'
|
562
|
+
end
|
563
|
+
|
564
|
+
val.each do |klass|
|
565
|
+
unless klass.class == String
|
566
|
+
raise TypeError, "Value in array is not a String. (#{klass.class})"
|
567
|
+
end
|
568
|
+
unless Base.schema.names("objectClasses").member? klass
|
569
|
+
# TODO MAKE OWN EXCEPTION
|
570
|
+
raise RuntimeError, "objectClass '#{klass}' unknown to LDAP server"
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
# make sure this doesn't drop any of the required objectclasses
|
575
|
+
required_classes().each do |oc|
|
576
|
+
unless val.member? oc
|
577
|
+
raise "'#{oc}' must be a defined objectClass for class '#{self.class}'"
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Set the actual objectClass data
|
582
|
+
define_attribute_methods('objectClass')
|
583
|
+
@data['objectClass'] = val.uniq
|
584
|
+
|
585
|
+
# Build |data| from schema
|
586
|
+
# clear attr_method mapping first
|
587
|
+
@attr_methods = {}
|
588
|
+
@must = {}
|
589
|
+
@may = {}
|
590
|
+
@sup = {}
|
591
|
+
val.each do |objc|
|
592
|
+
# Setup dependencies for validation pre-save
|
593
|
+
@must[objc] = Base.schema.attr('objectClasses', objc, 'MUST')
|
594
|
+
@may[objc] = Base.schema.attr('objectClasses', objc, 'MAY')
|
595
|
+
@sup[objc] = Base.schema.attr('objectClasses', objc, 'SUP')
|
596
|
+
@must[objc] = [] if @must[objc].nil?
|
597
|
+
@may[objc] = [] if @may[objc].nil?
|
598
|
+
@sup[objc] = [] if @sup[objc].nil?
|
599
|
+
|
600
|
+
# setup the alias/attr method mapping for @must
|
601
|
+
@must[objc].each do |attr|
|
602
|
+
# Update attr_method with appropriate
|
603
|
+
define_attribute_methods(attr)
|
604
|
+
end
|
605
|
+
|
606
|
+
# setup the alias/attr method mapping for @may
|
607
|
+
@may[objc].each do |attr|
|
608
|
+
define_attribute_methods(attr)
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
# Delete all now invalid attributes given the new objectClasses
|
613
|
+
@data.keys.each do |key|
|
614
|
+
# If it's not a proper aliased attribute, drop it
|
615
|
+
unless @attr_methods.has_key? key
|
616
|
+
@data.delete(key)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
# exists?
|
622
|
+
#
|
623
|
+
# Return whether the entry exists in LDAP or not
|
624
|
+
def exists?
|
625
|
+
return @exists
|
626
|
+
end
|
627
|
+
|
628
|
+
# dn
|
629
|
+
#
|
630
|
+
# Return the authoritative dn
|
631
|
+
def dn
|
632
|
+
return @dn.dup
|
633
|
+
end
|
634
|
+
|
635
|
+
# validate
|
636
|
+
#
|
637
|
+
# Basic validation:
|
638
|
+
# - Verify that every 'MUST' specified in the schema has a value defined
|
639
|
+
# - Enforcement of undefined attributes is handled in the objectClass= method
|
640
|
+
def validate
|
641
|
+
@must.each do |oc|
|
642
|
+
oc[1].each do |req_attr|
|
643
|
+
# TODO: should I ever check if key exists? bug if it doesnt...
|
644
|
+
deref = @attr_methods[req_attr]
|
645
|
+
if @data[deref] == []
|
646
|
+
raise AttributeEmpty,
|
647
|
+
"objectClass '#{oc[0]}' requires attribute '#{Base.schema.attribute_aliases(req_attr).join(', ')}'"
|
648
|
+
end
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
|
654
|
+
# delete
|
655
|
+
#
|
656
|
+
# Delete this entry from LDAP
|
657
|
+
def delete
|
658
|
+
begin
|
659
|
+
@@conn.delete(@dn)
|
660
|
+
@exists = false
|
661
|
+
rescue LDAP::ResultError => detail
|
662
|
+
raise DeleteError, "Failed to delete LDAP entry: '#{@dn}'"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
|
667
|
+
# write
|
668
|
+
#
|
669
|
+
# Write and validate this object into LDAP
|
670
|
+
# either adding or replacing attributes
|
671
|
+
# TODO: Binary data support
|
672
|
+
# TODO: Relative DN support
|
673
|
+
def write
|
674
|
+
# Validate against the objectClass requirements
|
675
|
+
validate
|
676
|
+
|
677
|
+
# Put all changes into one change entry to ensure
|
678
|
+
# automatic rollback upon failure.
|
679
|
+
entry = []
|
680
|
+
if @exists
|
681
|
+
# Cycle through all attrs to determine action
|
682
|
+
action = {}
|
683
|
+
|
684
|
+
replaceable = []
|
685
|
+
|
686
|
+
# Expand subtypes to real ldap_data entries
|
687
|
+
# We can't reuse @ldap_data because an exception would leave
|
688
|
+
# an object in an unknown state
|
689
|
+
ldap_data = @ldap_data.dup
|
690
|
+
ldap_data.keys.each do |key|
|
691
|
+
ldap_data[key].each do |value|
|
692
|
+
if value.class == Hash
|
693
|
+
suffix, real_value = extract_subtypes(value)
|
694
|
+
if ldap_data.has_key? key + suffix
|
695
|
+
ldap_data[key + suffix].push(real_value)
|
696
|
+
else
|
697
|
+
ldap_data[key + suffix] = real_value
|
698
|
+
end
|
699
|
+
ldap_data[key].delete(value)
|
700
|
+
end
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
# Expand subtypes to real data entries, but leave @data alone
|
705
|
+
data = @data.dup
|
706
|
+
data.keys.each do |key|
|
707
|
+
data[key].each do |value|
|
708
|
+
if value.class == Hash
|
709
|
+
suffix, real_value = extract_subtypes(value)
|
710
|
+
if data.has_key? key + suffix
|
711
|
+
data[key + suffix].push(real_value)
|
712
|
+
else
|
713
|
+
data[key + suffix] = real_value
|
714
|
+
end
|
715
|
+
data[key].delete(value)
|
716
|
+
end
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
# Now that all the subtypes will be treated as unique attributes
|
721
|
+
# we can see what's changed and add anything that is brand-spankin'
|
722
|
+
# new.
|
723
|
+
ldap_data.each do |pair|
|
724
|
+
suffix = ''
|
725
|
+
binary = 0
|
726
|
+
|
727
|
+
name, *suffix_a = pair[0].split(/;/)
|
728
|
+
suffix = ';'+ suffix_a.join(';') if suffix_a.size > 0
|
729
|
+
name = @attr_methods[name]
|
730
|
+
name = pair[0].split(/;/)[0] if name.nil? # for objectClass, or removed vals
|
731
|
+
value = data[name+suffix]
|
732
|
+
|
733
|
+
# Detect subtypes and account for them
|
734
|
+
binary = LDAP::LDAP_MOD_BVALUES if suffix.match(/;binary(;|$)/)
|
735
|
+
|
736
|
+
replaceable.push(name+suffix)
|
737
|
+
if pair[1] != value
|
738
|
+
# Create mod entries
|
739
|
+
if not value.empty?
|
740
|
+
# Ditched delete then replace because attribs with no equality match rules
|
741
|
+
# will fails
|
742
|
+
@@logger.debug("updating attribute of existing entry: #{name+suffix}: #{value.inspect}")
|
743
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, value))
|
744
|
+
else
|
745
|
+
# Since some types do not have equality matching rules, delete doesn't work
|
746
|
+
# Replacing with nothing is equivalent.
|
747
|
+
@@logger.debug("removing attribute from existing entry: #{name+suffix}")
|
748
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_REPLACE|binary, name + suffix, []))
|
749
|
+
end
|
750
|
+
end
|
751
|
+
end
|
752
|
+
data.each do |pair|
|
753
|
+
suffix = ''
|
754
|
+
binary = 0
|
755
|
+
|
756
|
+
name, *suffix_a = pair[0].split(/;/)
|
757
|
+
suffix = ';' + suffix_a.join(';') if suffix_a.size > 0
|
758
|
+
name = @attr_methods[name]
|
759
|
+
name = pair[0].split(/;/)[0] if name.nil? # for obj class or removed vals
|
760
|
+
value = pair[1]
|
761
|
+
|
762
|
+
if not replaceable.member? name+suffix
|
763
|
+
# Detect subtypes and account for them
|
764
|
+
binary = LDAP::LDAP_MOD_BVALUES if suffix.match(/;binary(;|$)/)
|
765
|
+
@@logger.debug("adding attribute to existing entry: #{name+suffix}: #{value.inspect}")
|
766
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, name + suffix, value)) unless value.empty?
|
767
|
+
end
|
768
|
+
end
|
769
|
+
begin
|
770
|
+
@@conn.modify(@dn, entry)
|
771
|
+
rescue => detail
|
772
|
+
raise WriteError, "Could not update LDAP entry: #{detail}"
|
773
|
+
end
|
774
|
+
else # add everything!
|
775
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, @attr_methods[dnattr()],
|
776
|
+
data[@attr_methods[dnattr()]]))
|
777
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD, 'objectClass',
|
778
|
+
data[@attr_methods['objectClass']]))
|
779
|
+
@data.each do |pair|
|
780
|
+
if pair[1].size > 0 and pair[0] != 'objectClass' and pair[0] != @attr_methods[dnattr()]
|
781
|
+
# Detect subtypes and account for them
|
782
|
+
binary = LDAP::LDAP_MOD_BVALUES if pair[0].match(/;binary(;|$)/)
|
783
|
+
@@logger.debug("adding attribute to new entry: #{name+suffix}: #{value.inspect}")
|
784
|
+
entry.push(LDAP.mod(LDAP::LDAP_MOD_ADD|binary, pair[0], pair[1]))
|
785
|
+
end
|
786
|
+
end
|
787
|
+
begin
|
788
|
+
@@conn.add(@dn, entry)
|
789
|
+
@exists = true
|
790
|
+
rescue LDAP::ResultError => detail
|
791
|
+
raise WriteError, "Could not add LDAP entry: #{detail}"
|
792
|
+
end
|
793
|
+
end
|
794
|
+
@ldap_data = @data.dup
|
795
|
+
end
|
796
|
+
|
797
|
+
|
798
|
+
# method_missing
|
799
|
+
#
|
800
|
+
# If a given method matches an attribute or an attribute alias
|
801
|
+
# then call the appropriate method.
|
802
|
+
# TODO: Determine if it would be better to define each allowed method
|
803
|
+
# using class_eval instead of using method_missing. This would
|
804
|
+
# give tab completion in irb.
|
805
|
+
def method_missing(name, *args)
|
806
|
+
key = name.to_s
|
807
|
+
case key
|
808
|
+
when /^(\S+)=$/
|
809
|
+
real_key = $1
|
810
|
+
if @attr_methods.has_key? real_key
|
811
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size != 1
|
812
|
+
return send(:attribute_method=, real_key, args[0])
|
813
|
+
end
|
814
|
+
else
|
815
|
+
if @attr_methods.has_key? key
|
816
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
|
817
|
+
return attribute_method(key, *args)
|
818
|
+
end
|
819
|
+
end
|
820
|
+
raise NoMethodError, "undefined method `#{key}' for #{self}"
|
821
|
+
end
|
822
|
+
|
823
|
+
|
824
|
+
private
|
825
|
+
|
826
|
+
# Enforce typing:
|
827
|
+
# Hashes are for subtypes
|
828
|
+
# Arrays are for multiple entries
|
829
|
+
def attribute_input_handler(attr, value)
|
830
|
+
if attr.nil?
|
831
|
+
raise RuntimeError, 'attr argument must not be nil.'
|
832
|
+
end
|
833
|
+
binary = Base.schema.binary? attr
|
834
|
+
single = Base.schema.single_value? attr
|
835
|
+
case value.class.to_s
|
836
|
+
when 'Array'
|
837
|
+
if single and value.size > 1
|
838
|
+
raise TypeError, "This attribute can only have a single value"
|
839
|
+
end
|
840
|
+
value.map! do |entry|
|
841
|
+
entry = entry.to_s unless entry.class == Hash
|
842
|
+
entry = attribute_input_handler(attr, entry)[0]
|
843
|
+
end
|
844
|
+
when 'Hash'
|
845
|
+
if value.keys.size > 1
|
846
|
+
raise TypeError, "Hashes must have one key-value pair only."
|
847
|
+
end
|
848
|
+
unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
|
849
|
+
@@logger.warn("unknown subtype did not match lang-* or binary: #{value.keys[0]}")
|
850
|
+
end
|
851
|
+
# Contents MUST be a String or an Array
|
852
|
+
if value.keys[0] != 'binary' and binary
|
853
|
+
suffix, real_value = extract_subtypes(value)
|
854
|
+
value = make_subtypes(name + suffix + ';binary', real_value)
|
855
|
+
end
|
856
|
+
value = [value]
|
857
|
+
when 'String'
|
858
|
+
if binary
|
859
|
+
value = {'binary' => value}
|
860
|
+
end
|
861
|
+
return [value]
|
862
|
+
else
|
863
|
+
value = [value.to_s]
|
864
|
+
end
|
865
|
+
return value
|
866
|
+
end
|
867
|
+
|
868
|
+
# make_subtypes
|
869
|
+
#
|
870
|
+
# Makes the Hashized value from the full attributename
|
871
|
+
# e.g. userCertificate;binary => "some_bin"
|
872
|
+
# becomes userCertificate => {"binary" => "some_bin"}
|
873
|
+
def make_subtypes(attr, value)
|
874
|
+
return [attr, value] unless attr.match(/;/)
|
875
|
+
|
876
|
+
ret_attr, *subtypes = attr.split(/;/)
|
877
|
+
return [ret_attr, [make_subtypes_helper(subtypes, value)]]
|
878
|
+
end
|
879
|
+
|
880
|
+
# make_subtypes_helper
|
881
|
+
#
|
882
|
+
# This is a recursive function for building
|
883
|
+
# nested hashed from multi-subtyped values
|
884
|
+
def make_subtypes_helper(subtypes, value)
|
885
|
+
return value if subtypes.size == 0
|
886
|
+
return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)}
|
887
|
+
end
|
888
|
+
|
889
|
+
# extract_subtypes
|
890
|
+
#
|
891
|
+
# Extracts all of the subtypes from a given set of nested hashes
|
892
|
+
# and returns the attribute suffix and the final true value
|
893
|
+
def extract_subtypes(value)
|
894
|
+
subtype = ''
|
895
|
+
ret_val = value
|
896
|
+
if value.class == Hash
|
897
|
+
subtype = ';' + value.keys[0]
|
898
|
+
ret_val = value[value.keys[0]]
|
899
|
+
subsubtype = ''
|
900
|
+
if ret_val.class == Hash
|
901
|
+
subsubtype, ret_val = extract_subtypes(ret_val)
|
902
|
+
end
|
903
|
+
subtype += subsubtype
|
904
|
+
end
|
905
|
+
ret_val = [ret_val] unless ret_val.class == Array
|
906
|
+
return subtype, ret_val
|
907
|
+
end
|
908
|
+
|
909
|
+
|
910
|
+
# Wrapper all bind activity
|
911
|
+
def Base.do_bind()
|
912
|
+
bind_dn = @@config[:bind_format] % [@@config[:user]]
|
913
|
+
if @@config[:password_block]
|
914
|
+
password = @@config[:password_block].call
|
915
|
+
@@config[:password_block] = Proc.new { password }
|
916
|
+
end
|
917
|
+
|
918
|
+
# Rough bind loop:
|
919
|
+
# Attempt 1: SASL if available
|
920
|
+
# Attempt 2: SIMPLE with credentials if password block
|
921
|
+
# Attempt 3: SIMPLE ANONYMOUS if 1 and 2 fail (or pwblock returns '')
|
922
|
+
auth = false
|
923
|
+
auth = do_sasl_bind(bind_dn) if @@config[:try_sasl]
|
924
|
+
auth = do_simple_bind(bind_dn) unless auth
|
925
|
+
auth = do_anonymous_bind(bind_dn) if not auth and @@config[:allow_anonymous]
|
926
|
+
|
927
|
+
unless auth
|
928
|
+
raise AuthenticationError, "All authentication mechanisms failed"
|
929
|
+
end
|
930
|
+
return auth
|
931
|
+
end
|
932
|
+
|
933
|
+
|
934
|
+
# Base.do_anonymous_bind
|
935
|
+
#
|
936
|
+
# Bind to LDAP with the given DN, but with no password. (anonymous!)
|
937
|
+
def Base.do_anonymous_bind(bind_dn)
|
938
|
+
@@logger.info "Attempting anonymous authentication"
|
939
|
+
begin
|
940
|
+
@@conn.bind()
|
941
|
+
return true
|
942
|
+
rescue
|
943
|
+
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
|
944
|
+
@@logger.warn "Warning: Anonymous authentication failed."
|
945
|
+
return false
|
946
|
+
end
|
947
|
+
end
|
948
|
+
|
949
|
+
# Base.do_simple_bind
|
950
|
+
#
|
951
|
+
# Bind to LDAP with the given DN and password_block.call()
|
952
|
+
def Base.do_simple_bind(bind_dn)
|
953
|
+
return false unless @@config[:password_block].respond_to? :call
|
954
|
+
begin
|
955
|
+
@@conn.bind(bind_dn, @@config[:password_block].call())
|
956
|
+
return true
|
957
|
+
rescue
|
958
|
+
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
|
959
|
+
@@logger.warn "Warning: SIMPLE authentication failed."
|
960
|
+
return false
|
961
|
+
end
|
962
|
+
end
|
963
|
+
|
964
|
+
# Base.do_sasl_bind
|
965
|
+
#
|
966
|
+
# Bind to LDAP with the given DN using any available SASL methods
|
967
|
+
def Base.do_sasl_bind(bind_dn)
|
968
|
+
# Get all SASL mechanisms
|
969
|
+
mechanisms = @@conn.root_dse[0]['supportedSASLMechanisms']
|
970
|
+
# Use GSSAPI if available
|
971
|
+
# Currently only GSSAPI is supported with Ruby/LDAP from
|
972
|
+
# http://caliban.com/files/redhat/RPMS/i386/ruby-ldap-0.8.2-4.i386.rpm
|
973
|
+
# TODO: Investigate further SASL support
|
974
|
+
if mechanisms.respond_to? :member? and mechanisms.member? 'GSSAPI'
|
975
|
+
begin
|
976
|
+
@@conn.sasl_bind(bind_dn, 'GSSAPI')
|
977
|
+
return true
|
978
|
+
rescue
|
979
|
+
@@logger.debug "LDAP Error: #{@@conn.err2string(@@conn.err)}"
|
980
|
+
@@logger.warn "Warning: SASL GSSAPI authentication failed."
|
981
|
+
return false
|
982
|
+
end
|
983
|
+
end
|
984
|
+
return false
|
985
|
+
end
|
986
|
+
|
987
|
+
# base
|
988
|
+
#
|
989
|
+
# Returns the value of self.class.base
|
990
|
+
# This is just syntactic sugar
|
991
|
+
def base
|
992
|
+
self.class.base
|
993
|
+
end
|
994
|
+
|
995
|
+
# required_classes
|
996
|
+
#
|
997
|
+
# Returns the value of self.class.required_classes
|
998
|
+
# This is just syntactic sugar
|
999
|
+
def required_classes
|
1000
|
+
self.class.required_classes
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
# dnattr
|
1004
|
+
#
|
1005
|
+
# Returns the value of self.class.dnattr
|
1006
|
+
# This is just syntactic sugar
|
1007
|
+
def dnattr
|
1008
|
+
self.class.dnattr
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
# attribute_method
|
1012
|
+
#
|
1013
|
+
# Return the value of the attribute called by method_missing?
|
1014
|
+
def attribute_method(method, arrays = false)
|
1015
|
+
attr = @attr_methods[method]
|
1016
|
+
|
1017
|
+
# Return a copy of the stored data
|
1018
|
+
return @data[attr].dup if arrays
|
1019
|
+
return array_of(@data[attr].dup, false)
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
|
1023
|
+
# attribute_method=
|
1024
|
+
#
|
1025
|
+
# Set the value of the attribute called by method_missing?
|
1026
|
+
def attribute_method=(method, arg)
|
1027
|
+
# Copy input
|
1028
|
+
begin
|
1029
|
+
value = arg.dup
|
1030
|
+
rescue TypeError
|
1031
|
+
value = arg.to_s
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
# Get the attr and clean up the input
|
1035
|
+
attr = @attr_methods[method]
|
1036
|
+
value = attribute_input_handler(attr, value)
|
1037
|
+
|
1038
|
+
# Assign the value
|
1039
|
+
@data[attr] = value
|
1040
|
+
|
1041
|
+
# Return a copy of what got saved
|
1042
|
+
return @data[attr].dup
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
|
1046
|
+
# define_attribute_methods
|
1047
|
+
#
|
1048
|
+
# Make a method entry for _every_ alias of a valid attribute and map it
|
1049
|
+
# onto the first attribute passed in.
|
1050
|
+
def define_attribute_methods(attr)
|
1051
|
+
if @attr_methods.has_key? attr
|
1052
|
+
return
|
1053
|
+
end
|
1054
|
+
aliases = Base.schema.attribute_aliases(attr)
|
1055
|
+
aliases.each do |ali|
|
1056
|
+
@attr_methods[ali] = attr
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
# array_of
|
1061
|
+
#
|
1062
|
+
# Returns the array form of a value, or not an array if
|
1063
|
+
# false is passed in.
|
1064
|
+
def array_of(value, to_a = true)
|
1065
|
+
if to_a
|
1066
|
+
case value.class.to_s
|
1067
|
+
when 'Array'
|
1068
|
+
return value
|
1069
|
+
when 'Hash'
|
1070
|
+
return [value]
|
1071
|
+
else
|
1072
|
+
return [value.to_s]
|
1073
|
+
end
|
1074
|
+
else
|
1075
|
+
case value.class.to_s
|
1076
|
+
when 'Array'
|
1077
|
+
return nil if value.size == 0
|
1078
|
+
return value[0] if value.size == 1
|
1079
|
+
return value
|
1080
|
+
when 'Hash'
|
1081
|
+
return value
|
1082
|
+
else
|
1083
|
+
return value.to_s
|
1084
|
+
end
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
end # Base
|
1089
|
+
|
1090
|
+
end # ActiveLDAP
|
1091
|
+
|
1092
|
+
|
1093
|
+
|
1094
|
+
|