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