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.
Files changed (74) hide show
  1. data/CHANGES +375 -0
  2. data/COPYING +340 -0
  3. data/LICENSE +58 -0
  4. data/Manifest.txt +33 -0
  5. data/README +63 -0
  6. data/Rakefile +37 -0
  7. data/TODO +31 -0
  8. data/benchmark/bench-al.rb +152 -0
  9. data/lib/{activeldap.rb → active_ldap.rb} +280 -263
  10. data/lib/active_ldap/adaptor/base.rb +29 -0
  11. data/lib/active_ldap/adaptor/ldap.rb +466 -0
  12. data/lib/active_ldap/association/belongs_to.rb +38 -0
  13. data/lib/active_ldap/association/belongs_to_many.rb +40 -0
  14. data/lib/active_ldap/association/collection.rb +80 -0
  15. data/lib/active_ldap/association/has_many.rb +48 -0
  16. data/lib/active_ldap/association/has_many_wrap.rb +56 -0
  17. data/lib/active_ldap/association/proxy.rb +89 -0
  18. data/lib/active_ldap/associations.rb +162 -0
  19. data/lib/active_ldap/attributes.rb +199 -0
  20. data/lib/active_ldap/base.rb +1343 -0
  21. data/lib/active_ldap/callbacks.rb +19 -0
  22. data/lib/active_ldap/command.rb +46 -0
  23. data/lib/active_ldap/configuration.rb +96 -0
  24. data/lib/active_ldap/connection.rb +137 -0
  25. data/lib/{activeldap → active_ldap}/ldap.rb +1 -1
  26. data/lib/active_ldap/object_class.rb +70 -0
  27. data/lib/active_ldap/schema.rb +258 -0
  28. data/lib/{activeldap → active_ldap}/timeout.rb +0 -0
  29. data/lib/{activeldap → active_ldap}/timeout_stub.rb +0 -0
  30. data/lib/active_ldap/user_password.rb +92 -0
  31. data/lib/active_ldap/validations.rb +78 -0
  32. data/rails/plugin/active_ldap/README +54 -0
  33. data/rails/plugin/active_ldap/init.rb +6 -0
  34. data/test/TODO +2 -0
  35. data/test/al-test-utils.rb +337 -0
  36. data/test/command.rb +62 -0
  37. data/test/config.yaml +8 -0
  38. data/test/config.yaml.sample +6 -0
  39. data/test/run-test.rb +17 -0
  40. data/test/test-unit-ext.rb +2 -0
  41. data/test/test_associations.rb +334 -0
  42. data/test/test_attributes.rb +71 -0
  43. data/test/test_base.rb +345 -0
  44. data/test/test_base_per_instance.rb +32 -0
  45. data/test/test_bind.rb +53 -0
  46. data/test/test_callback.rb +35 -0
  47. data/test/test_connection.rb +38 -0
  48. data/test/test_connection_per_class.rb +50 -0
  49. data/test/test_find.rb +36 -0
  50. data/test/test_groupadd.rb +50 -0
  51. data/test/test_groupdel.rb +46 -0
  52. data/test/test_groupls.rb +107 -0
  53. data/test/test_groupmod.rb +51 -0
  54. data/test/test_lpasswd.rb +75 -0
  55. data/test/test_object_class.rb +32 -0
  56. data/test/test_reflection.rb +173 -0
  57. data/test/test_schema.rb +166 -0
  58. data/test/test_user.rb +209 -0
  59. data/test/test_user_password.rb +93 -0
  60. data/test/test_useradd-binary.rb +59 -0
  61. data/test/test_useradd.rb +55 -0
  62. data/test/test_userdel.rb +48 -0
  63. data/test/test_userls.rb +86 -0
  64. data/test/test_usermod-binary-add-time.rb +62 -0
  65. data/test/test_usermod-binary-add.rb +61 -0
  66. data/test/test_usermod-binary-del.rb +64 -0
  67. data/test/test_usermod-lang-add.rb +57 -0
  68. data/test/test_usermod.rb +56 -0
  69. data/test/test_validation.rb +38 -0
  70. metadata +94 -21
  71. data/lib/activeldap/associations.rb +0 -170
  72. data/lib/activeldap/base.rb +0 -1456
  73. data/lib/activeldap/configuration.rb +0 -59
  74. data/lib/activeldap/schema2.rb +0 -217
@@ -0,0 +1,199 @@
1
+ module ActiveLdap
2
+ module Attributes
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def attr_protected(*attributes)
9
+ targets = attributes.collect {|attr| attr.to_s} - protected_attributes
10
+ instance_variable_set("@attr_protected", targets)
11
+ end
12
+
13
+ def protected_attributes
14
+ ancestors[0..(ancestors.index(Base))].inject([]) do |result, ancestor|
15
+ result + ancestor.instance_eval {@attr_protected ||= []}
16
+ end
17
+ end
18
+
19
+ def normalize_attribute_name(name)
20
+ name.to_s.downcase
21
+ end
22
+
23
+ # Enforce typing:
24
+ # Hashes are for subtypes
25
+ # Arrays are for multiple entries
26
+ def normalize_attribute(name, value)
27
+ logger.debug {"stub: called normalize_attribute" +
28
+ "(#{name.inspect}, #{value.inspect})"}
29
+ if name.nil?
30
+ raise RuntimeError, 'The first argument, name, must not be nil. ' +
31
+ 'Please report this as a bug!'
32
+ end
33
+
34
+ name = normalize_attribute_name(name)
35
+ rubyish_class_name = Inflector.underscore(value.class.name)
36
+ handler = "normalize_attribute_value_of_#{rubyish_class_name}"
37
+ if respond_to?(handler, true)
38
+ [name, send(handler, name, value)]
39
+ else
40
+ [name, [value.to_s]]
41
+ end
42
+ end
43
+
44
+ def unnormalize_attributes(attributes)
45
+ result = {}
46
+ attributes.each do |name, values|
47
+ unnormalize_attribute(name, values, result)
48
+ end
49
+ result
50
+ end
51
+
52
+ def unnormalize_attribute(name, values, result={})
53
+ if values.empty?
54
+ result[name] = []
55
+ else
56
+ values.each do |value|
57
+ if value.is_a?(Hash)
58
+ suffix, real_value = extract_subtypes(value)
59
+ new_name = name + suffix
60
+ result[new_name] ||= []
61
+ result[new_name].concat(real_value)
62
+ else
63
+ result[name] ||= []
64
+ result[name] << value.dup
65
+ end
66
+ end
67
+ end
68
+ result
69
+ end
70
+
71
+ private
72
+ def normalize_attribute_value_of_array(name, value)
73
+ if value.size > 1 and schema.single_value?(name)
74
+ raise TypeError, "Attribute #{name} can only have a single value"
75
+ end
76
+ if value.empty?
77
+ schema.binary_required?(name) ? [{'binary' => value}] : value
78
+ else
79
+ value.collect do |entry|
80
+ normalize_attribute(name, entry)[1][0]
81
+ end
82
+ end
83
+ end
84
+
85
+ def normalize_attribute_value_of_hash(name, value)
86
+ if value.keys.size > 1
87
+ raise TypeError, "Hashes must have one key-value pair only."
88
+ end
89
+ unless value.keys[0].match(/^(lang-[a-z][a-z]*)|(binary)$/)
90
+ logger.warn {"unknown subtype did not match lang-* or binary:" +
91
+ "#{value.keys[0]}"}
92
+ end
93
+ # Contents MUST be a String or an Array
94
+ if !value.has_key?('binary') and schema.binary_required?(name)
95
+ suffix, real_value = extract_subtypes(value)
96
+ name, values = make_subtypes(name + suffix + ';binary', real_value)
97
+ values
98
+ else
99
+ [value]
100
+ end
101
+ end
102
+
103
+ def normalize_attribute_value_of_nil_class(name, value)
104
+ schema.binary_required?(name) ? [{'binary' => []}] : []
105
+ end
106
+
107
+ def normalize_attribute_value_of_string(name, value)
108
+ [schema.binary_required?(name) ? {'binary' => [value]} : value]
109
+ end
110
+
111
+ def normalize_attribute_value_of_date(name, value)
112
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
113
+ value.year, value.month, value.mday, 0, 0, 0,
114
+ '+0000')
115
+ normalize_attribute_value_of_string(name, new_value)
116
+ end
117
+
118
+ def normalize_attribute_value_of_time(name, value)
119
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
120
+ 0, 0, 0, value.hour, value.min, value.sec,
121
+ value.zone)
122
+ normalize_attribute_value_of_string(name, new_value)
123
+ end
124
+
125
+ def normalize_attribute_value_of_date_time(name, value)
126
+ new_value = sprintf('%.04d%.02d%.02d%.02d%.02d%.02d%s',
127
+ value.year, value.month, value.mday, value.hour,
128
+ value.min, value.sec, value.zone)
129
+ normalize_attribute_value_of_string(name, new_value)
130
+ end
131
+
132
+
133
+ # make_subtypes
134
+ #
135
+ # Makes the Hashized value from the full attributename
136
+ # e.g. userCertificate;binary => "some_bin"
137
+ # becomes userCertificate => {"binary" => "some_bin"}
138
+ def make_subtypes(attr, value)
139
+ logger.debug {"stub: called make_subtypes(#{attr.inspect}, " +
140
+ "#{value.inspect})"}
141
+ return [attr, value] unless attr.match(/;/)
142
+
143
+ ret_attr, *subtypes = attr.split(/;/)
144
+ return [ret_attr, [make_subtypes_helper(subtypes, value)]]
145
+ end
146
+
147
+ # make_subtypes_helper
148
+ #
149
+ # This is a recursive function for building
150
+ # nested hashed from multi-subtyped values
151
+ def make_subtypes_helper(subtypes, value)
152
+ logger.debug {"stub: called make_subtypes_helper" +
153
+ "(#{subtypes.inspect}, #{value.inspect})"}
154
+ return value if subtypes.size == 0
155
+ return {subtypes[0] => make_subtypes_helper(subtypes[1..-1], value)}
156
+ end
157
+
158
+ # extract_subtypes
159
+ #
160
+ # Extracts all of the subtypes from a given set of nested hashes
161
+ # and returns the attribute suffix and the final true value
162
+ def extract_subtypes(value)
163
+ logger.debug {"stub: called extract_subtypes(#{value.inspect})"}
164
+ subtype = ''
165
+ ret_val = value
166
+ if value.class == Hash
167
+ subtype = ';' + value.keys[0]
168
+ ret_val = value[value.keys[0]]
169
+ subsubtype = ''
170
+ if ret_val.class == Hash
171
+ subsubtype, ret_val = extract_subtypes(ret_val)
172
+ end
173
+ subtype += subsubtype
174
+ end
175
+ ret_val = [ret_val] unless ret_val.class == Array
176
+ return subtype, ret_val
177
+ end
178
+ end
179
+
180
+ private
181
+ def remove_attributes_protected_from_mass_assignment(targets)
182
+ needless_attributes = {}
183
+ (attributes_protected_by_default +
184
+ (self.class.protected_attributes || [])).each do |name|
185
+ needless_attributes[to_real_attribute_name(name)] = true
186
+ end
187
+
188
+ targets.collect do |key, value|
189
+ [to_real_attribute_name(key), value]
190
+ end.reject do |key, value|
191
+ key.nil? or needless_attributes[key]
192
+ end
193
+ end
194
+
195
+ def attributes_protected_by_default
196
+ [dn_attribute, 'objectClass']
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,1343 @@
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 'active_ldap'
16
+ # > true
17
+ # irb> user = ActiveLdap::User.new("drewry")
18
+ # > #<ActiveLdap::User:0x402e...
19
+ # irb> user.cn
20
+ # > "foo"
21
+ # irb> user.common_name
22
+ # > "foo"
23
+ # irb> user.cn = "Will Drewry"
24
+ # > "Will Drewry"
25
+ # irb> user.cn
26
+ # > "Will Drewry"
27
+ # irb> user.save
28
+ #
29
+ #
30
+
31
+ require 'English'
32
+
33
+ module ActiveLdap
34
+ # OO-interface to LDAP assuming pam/nss_ldap-style organization with
35
+ # Active specifics
36
+ # Each subclass does a ldapsearch for the matching entry.
37
+ # If no exact match, raise an error.
38
+ # If match, change all LDAP attributes in accessor attributes on the object.
39
+ # -- these are ACTUALLY populated from schema - see active_ldap/schema.rb
40
+ # example
41
+ # -- extract objectClasses from match and populate
42
+ # Multiple entries become lists.
43
+ # If this isn't read-only then lists become multiple entries, etc.
44
+
45
+ class Error < StandardError
46
+ end
47
+
48
+ # ConfigurationError
49
+ #
50
+ # An exception raised when there is a problem with Base.connect arguments
51
+ class ConfigurationError < Error
52
+ end
53
+
54
+ # DeleteError
55
+ #
56
+ # An exception raised when an ActiveLdap delete action fails
57
+ class DeleteError < Error
58
+ end
59
+
60
+ # SaveError
61
+ #
62
+ # An exception raised when an ActiveLdap save action failes
63
+ class SaveError < Error
64
+ end
65
+
66
+ # AuthenticationError
67
+ #
68
+ # An exception raised when user authentication fails
69
+ class AuthenticationError < Error
70
+ end
71
+
72
+ # ConnectionError
73
+ #
74
+ # An exception raised when the LDAP conenction fails
75
+ class ConnectionError < Error
76
+ end
77
+
78
+ # ObjectClassError
79
+ #
80
+ # An exception raised when an objectClass is not defined in the schema
81
+ class ObjectClassError < Error
82
+ end
83
+
84
+ # AttributeAssignmentError
85
+ #
86
+ # An exception raised when there is an issue assigning a value to
87
+ # an attribute
88
+ class AttributeAssignmentError < Error
89
+ end
90
+
91
+ # TimeoutError
92
+ #
93
+ # An exception raised when a connection action fails due to a timeout
94
+ class TimeoutError < Error
95
+ end
96
+
97
+ class EntryNotFound < Error
98
+ end
99
+
100
+ class EntryAlreadyExist < Error
101
+ end
102
+
103
+ class StrongAuthenticationRequired < Error
104
+ end
105
+
106
+ class DistinguishedNameInvalid < Error
107
+ attr_reader :dn
108
+ def initialize(dn)
109
+ @dn = dn
110
+ super("#{@dn} is invalid distinguished name (dn).")
111
+ end
112
+ end
113
+
114
+ class DistinguishedNameNotSetError < Error
115
+ end
116
+
117
+ class EntryNotSaved < Error
118
+ end
119
+
120
+ class RequiredObjectClassMissed < Error
121
+ end
122
+
123
+ class RequiredAttributeMissed < Error
124
+ end
125
+
126
+ class EntryInvalid < Error
127
+ end
128
+
129
+ class UnwillingToPerform < Error
130
+ end
131
+
132
+ class ConnectionNotEstablished < Error
133
+ end
134
+
135
+ class AdapterNotSpecified < Error
136
+ end
137
+
138
+ # Base
139
+ #
140
+ # Base is the primary class which contains all of the core
141
+ # ActiveLdap functionality. It is meant to only ever be subclassed
142
+ # by extension classes.
143
+ class Base
144
+ include Reloadable::Subclasses
145
+
146
+ VALID_LDAP_MAPPING_OPTIONS = [:dn_attribute, :prefix, :classes, :scope]
147
+
148
+ cattr_accessor :logger
149
+ cattr_accessor :configurations
150
+ @@configurations = {}
151
+
152
+ def self.class_local_attr_accessor(search_ancestors, *syms)
153
+ syms.flatten.each do |sym|
154
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
155
+ def self.#{sym}(search_superclasses=#{search_ancestors})
156
+ @#{sym} ||= nil
157
+ return @#{sym} if @#{sym}
158
+ if search_superclasses
159
+ target = superclass
160
+ value = nil
161
+ loop do
162
+ break nil unless target.respond_to?("#{sym}")
163
+ value = target.#{sym}
164
+ break if value
165
+ target = target.superclass
166
+ end
167
+ value
168
+ else
169
+ nil
170
+ end
171
+ end
172
+ def #{sym}; self.class.#{sym}; end
173
+ def self.#{sym}=(value); @#{sym} = value; end
174
+ def #{sym}=(value); self.class.#{sym} = value; end
175
+ EOS
176
+ end
177
+ end
178
+
179
+ class_local_attr_accessor false, :prefix, :base, :dn_attribute
180
+ class_local_attr_accessor true, :ldap_scope, :required_classes
181
+
182
+ class << self
183
+ # Hide new in Base
184
+ private :new
185
+ private :dn_attribute
186
+
187
+ # Connect and bind to LDAP creating a class variable for use by
188
+ # all ActiveLdap objects.
189
+ #
190
+ # == +config+
191
+ # +config+ must be a hash that may contain any of the following fields:
192
+ # :password_block, :logger, :host, :port, :base, :bind_dn,
193
+ # :try_sasl, :allow_anonymous
194
+ # :bind_dn specifies the DN to bind with.
195
+ # :password_block specifies a Proc object that will yield a String to
196
+ # be used as the password when called.
197
+ # :logger specifies a preconfigured Log4r::Logger to be used for all
198
+ # logging
199
+ # :host sets the LDAP server hostname
200
+ # :port sets the LDAP server port
201
+ # :base overwrites Base.base - this affects EVERYTHING
202
+ # :try_sasl indicates that a SASL bind should be attempted when binding
203
+ # to the server (default: false)
204
+ # :allow_anonymous indicates that a true anonymous bind is allowed when
205
+ # trying to bind to the server (default: true)
206
+ # :retries - indicates the number of attempts to reconnect that will be
207
+ # undertaken when a stale connection occurs. -1 means infinite.
208
+ # :sasl_quiet - if true, sets @sasl_quiet on the Ruby/LDAP connection
209
+ # :method - whether to use :ssl, :tls, or :plain (unencrypted)
210
+ # :retry_wait - seconds to wait before retrying a connection
211
+ # :ldap_scope - dictates how to find objects. ONELEVEL by default to
212
+ # avoid dn_attr collisions across OUs. Think before changing.
213
+ # :timeout - time in seconds - defaults to disabled. This CAN interrupt
214
+ # search() requests. Be warned.
215
+ # :retry_on_timeout - whether to reconnect when timeouts occur. Defaults
216
+ # to true
217
+ # See lib/configuration.rb for defaults for each option
218
+ def establish_connection(config=nil)
219
+ super
220
+ ensure_logger
221
+ connection.connect
222
+ # Make irb users happy with a 'true'
223
+ true
224
+ end
225
+
226
+ def create(attributes=nil, &block)
227
+ if attributes.is_a?(Array)
228
+ attributes.collect {|attrs| create(attrs, &block)}
229
+ else
230
+ object = new(attributes, &block)
231
+ object.save
232
+ object
233
+ end
234
+ end
235
+
236
+ def search(options={}, &block)
237
+ attr = options[:attribute]
238
+ value = options[:value] || '*'
239
+ filter = options[:filter]
240
+ prefix = options[:prefix]
241
+
242
+ value = value.first if value.is_a?(Array) and value.first.size == 1
243
+ if filter.nil? and !value.is_a?(String)
244
+ raise ArgumentError, "Search value must be a String"
245
+ end
246
+
247
+ _attr, value, _prefix = split_search_value(value)
248
+ attr ||= _attr || dn_attribute || "objectClass"
249
+ prefix ||= _prefix
250
+ filter ||= "(#{attr}=#{escape_filter_value(value, true)})"
251
+ _base = [prefix, base].compact.reject{|x| x.empty?}.join(",")
252
+ connection.search(:base => _base,
253
+ :scope => options[:scope] || ldap_scope,
254
+ :filter => filter,
255
+ :limit => options[:limit],
256
+ :attributes => options[:attributes]) do |dn, attrs|
257
+ attributes = {}
258
+ attrs.each do |key, value|
259
+ normalized_attr, normalized_value = make_subtypes(key, value)
260
+ attributes[normalized_attr] ||= []
261
+ attributes[normalized_attr].concat(normalized_value)
262
+ end
263
+ value = [dn, attributes]
264
+ value = yield(value) if block_given?
265
+ value
266
+ end
267
+ end
268
+
269
+ # This class function is used to setup all mappings between the subclass
270
+ # and ldap for use in activeldap
271
+ #
272
+ # Example:
273
+ # ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People',
274
+ # :classes => ['top', 'posixAccount'],
275
+ # :scope => :sub
276
+ def ldap_mapping(options={})
277
+ validate_ldap_mapping_options(options)
278
+ dn_attribute = options[:dn_attribute] || default_dn_attribute
279
+ prefix = options[:prefix] || default_prefix
280
+ classes = options[:classes]
281
+ scope = options[:scope]
282
+
283
+ self.dn_attribute = dn_attribute
284
+ self.prefix = prefix
285
+ self.ldap_scope = scope
286
+ self.required_classes = classes
287
+
288
+ public_class_method :new
289
+ public_class_method :dn_attribute
290
+ end
291
+
292
+ alias_method :base_inheritable, :base
293
+ # Base.base
294
+ #
295
+ # This method when included into Base provides
296
+ # an inheritable, overwritable configuration setting
297
+ #
298
+ # This should be a string with the base of the
299
+ # ldap server such as 'dc=example,dc=com', and
300
+ # it should be overwritten by including
301
+ # configuration.rb into this class.
302
+ # When subclassing, the specified prefix will be concatenated.
303
+ def base
304
+ _base = base_inheritable
305
+ _base = configuration[:base] if _base.nil? and configuration
306
+ _base ||= base_inheritable(true)
307
+ [prefix, _base].find_all do |component|
308
+ component and !component.empty?
309
+ end.join(",")
310
+ end
311
+
312
+ alias_method :ldap_scope_without_validation=, :ldap_scope=
313
+ def ldap_scope=(scope)
314
+ scope = scope.to_sym if scope.is_a?(String)
315
+ if scope.nil? or scope.is_a?(Symbol)
316
+ self.ldap_scope_without_validation = scope
317
+ else
318
+ raise ConfigurationError,
319
+ ":ldap_scope '#{scope.inspect}' must be a Symbol"
320
+ end
321
+ end
322
+
323
+ def dump(options={})
324
+ ldifs = []
325
+ options = {:base => base, :scope => ldap_scope}.merge(options)
326
+ connection.search(options) do |dn, attributes|
327
+ ldifs << to_ldif(dn, attributes)
328
+ end
329
+ ldifs.join("\n")
330
+ end
331
+
332
+ def to_ldif(dn, attributes)
333
+ connection.to_ldif(dn, unnormalize_attributes(attributes))
334
+ end
335
+
336
+ def load(ldifs)
337
+ connection.load(ldifs)
338
+ end
339
+
340
+ def destroy(targets, options={})
341
+ targets = [targets] unless targets.is_a?(Array)
342
+ targets.each do |target|
343
+ find(target, options).destroy
344
+ end
345
+ end
346
+
347
+ def destroy_all(filter=nil, options={})
348
+ targets = []
349
+ if filter.is_a?(Hash)
350
+ options = options.merge(filter)
351
+ filter = nil
352
+ end
353
+ options = options.merge(:filter => filter) if filter
354
+ find(:all, options).sort_by do |target|
355
+ target.dn.reverse
356
+ end.reverse.each do |target|
357
+ target.destroy
358
+ end
359
+ end
360
+
361
+ def delete(targets, options={})
362
+ targets = [targets] unless targets.is_a?(Array)
363
+ targets = targets.collect do |target|
364
+ ensure_dn_attribute(ensure_base(target))
365
+ end
366
+ connection.delete(targets, options)
367
+ end
368
+
369
+ def delete_all(filter=nil, options={})
370
+ options = {:base => base, :scope => ldap_scope}.merge(options)
371
+ options = options.merge(:filter => filter) if filter
372
+ targets = connection.search(options).collect do |dn, attributes|
373
+ dn
374
+ end.sort_by do |dn|
375
+ dn.reverse
376
+ end.reverse
377
+
378
+ connection.delete(targets)
379
+ end
380
+
381
+ def add(dn, entries, options={})
382
+ unnormalized_entries = entries.collect do |type, key, value|
383
+ [type, key, unnormalize_attribute(key, value)]
384
+ end
385
+ connection.add(dn, unnormalized_entries, options)
386
+ end
387
+
388
+ def modify(dn, entries, options={})
389
+ unnormalized_entries = entries.collect do |type, key, value|
390
+ [type, key, unnormalize_attribute(key, value)]
391
+ end
392
+ connection.modify(dn, unnormalized_entries, options)
393
+ end
394
+
395
+ # find
396
+ #
397
+ # Finds the first match for value where |value| is the value of some
398
+ # |field|, or the wildcard match. This is only useful for derived classes.
399
+ # usage: Subclass.find(:attribute => "cn", :value => "some*val")
400
+ # Subclass.find('some*val')
401
+ def find(*args)
402
+ options = extract_options_from_args!(args)
403
+ args = [:first] if args.empty? and !options.empty?
404
+ case args.first
405
+ when :first
406
+ find_initial(options)
407
+ when :all
408
+ find_every(options)
409
+ else
410
+ find_from_dns(args, options)
411
+ end
412
+ end
413
+
414
+ def exists?(dn, options={})
415
+ prefix = /^#{Regexp.escape(truncate_base(ensure_dn_attribute(dn)))}/
416
+ suffix = /,#{Regexp.escape(base)}$/
417
+ not search({:value => dn}.merge(options)).find do |_dn,|
418
+ prefix.match(_dn) and suffix.match(_dn)
419
+ end.nil?
420
+ end
421
+
422
+ def update(dn, attributes, options={})
423
+ if dn.is_a?(Array)
424
+ i = -1
425
+ dns = dn
426
+ dns.collect do |dn|
427
+ i += 1
428
+ update(dn, attributes[i], options)
429
+ end
430
+ else
431
+ object = find(dn, options)
432
+ object.update_attributes(attributes)
433
+ object
434
+ end
435
+ end
436
+
437
+ def update_all(attributes, filter=nil, options={})
438
+ search_options = options
439
+ if filter
440
+ if /[=\(\)&\|]/ =~ filter
441
+ search_options = search_options.merge(:filter => filter)
442
+ else
443
+ search_options = search_options.merge(:value => filter)
444
+ end
445
+ end
446
+ targets = search(search_options).collect do |dn, attrs|
447
+ dn
448
+ end
449
+
450
+ entries = attributes.collect do |name, value|
451
+ normalized_name, normalized_value = normalize_attribute(name, value)
452
+ [:replace, normalized_name,
453
+ unnormalize_attribute(normalized_name, normalized_value)]
454
+ end
455
+ targets.each do |dn|
456
+ connection.modify(dn, entries, options)
457
+ end
458
+ end
459
+
460
+ def base_class
461
+ if self == Base or superclass == Base
462
+ self
463
+ else
464
+ superclass.base_class
465
+ end
466
+ end
467
+
468
+ def human_attribute_name(attribute_key_name)
469
+ attribute_key_name.humanize
470
+ end
471
+
472
+ private
473
+ def validate_ldap_mapping_options(options)
474
+ options.assert_valid_keys(VALID_LDAP_MAPPING_OPTIONS)
475
+ end
476
+
477
+ def extract_options_from_args!(args)
478
+ args.last.is_a?(Hash) ? args.pop : {}
479
+ end
480
+
481
+ def find_initial(options)
482
+ find_every(options.merge(:limit => 1)).first
483
+ end
484
+
485
+ def find_every(options)
486
+ search(options).collect do |dn, attrs|
487
+ instantiate([dn, attrs])
488
+ end
489
+ end
490
+
491
+ def find_from_dns(dns, options)
492
+ expects_array = dns.first.is_a?(Array)
493
+ return [] if expects_array and dns.first.empty?
494
+
495
+ dns = dns.flatten.compact.uniq
496
+
497
+ case dns.size
498
+ when 0
499
+ raise EntryNotFound, "Couldn't find #{name} without a DN"
500
+ when 1
501
+ result = find_one(dns.first, options)
502
+ expects_array ? [result] : result
503
+ else
504
+ find_some(dns, options)
505
+ end
506
+ end
507
+
508
+ def find_one(dn, options)
509
+ attr, value, prefix = split_search_value(dn)
510
+ filter = "(#{attr || dn_attribute}=#{escape_filter_value(value, true)})"
511
+ filter = "(&#{filter}#{options[:filter]})" if options[:filter]
512
+ options = {:prefix => prefix}.merge(options.merge(:filter => filter))
513
+ result = find_initial(options)
514
+ if result
515
+ result
516
+ else
517
+ message = "Couldn't find #{name} with DN=#{dn}"
518
+ message << " #{options[:filter]}" if options[:filter]
519
+ raise EntryNotFound, message
520
+ end
521
+ end
522
+
523
+ def find_some(dns, options)
524
+ dn_filters = dns.collect do |dn|
525
+ attr, value, prefix = split_search_value(dn)
526
+ attr ||= dn_attribute
527
+ filter = "(#{attr}=#{escape_filter_value(value, true)})"
528
+ if prefix
529
+ filter = "(&#{filter}(dn=*,#{escape_filter_value(prefix)},#{base}))"
530
+ end
531
+ filter
532
+ end
533
+ filter = "(|#{dn_filters.join('')})"
534
+ filter = "(&#{filter}#{options[:filter]})" if options[:filter]
535
+ result = find_every(options.merge(:filter => filter))
536
+ if result.size == dns.size
537
+ result
538
+ else
539
+ message = "Couldn't find all #{name} with DNs (#{dns.join(', ')})"
540
+ message << " #{options[:filter]}"if options[:filter]
541
+ raise EntryNotFound, message
542
+ end
543
+ end
544
+
545
+ def split_search_value(value)
546
+ value, prefix = value.split(/,/, 2)
547
+ attr, value = value.split(/=/, 2)
548
+ attr, value = value, attr if value.nil?
549
+ prefix = nil if prefix == base
550
+ prefix = truncate_base(prefix) if prefix
551
+ [attr, value, prefix]
552
+ end
553
+
554
+ def escape_filter_value(value, without_asterisk=false)
555
+ value.gsub(/[\*\(\)\\\0]/) do |x|
556
+ if without_asterisk and x == "*"
557
+ x
558
+ else
559
+ "\\%02x" % x[0]
560
+ end
561
+ end
562
+ end
563
+
564
+ def ensure_dn(target)
565
+ attr, value, prefix = split_search_value(target)
566
+ "#{attr || dn_attribute}=#{value},#{prefix || base}"
567
+ end
568
+
569
+ def ensure_dn_attribute(target)
570
+ "#{dn_attribute}=" +
571
+ target.gsub(/^#{Regexp.escape(dn_attribute)}\s*=\s*/, '')
572
+ end
573
+
574
+ def ensure_base(target)
575
+ [truncate_base(target), base].join(',')
576
+ end
577
+
578
+ def truncate_base(target)
579
+ target.sub(/,#{Regexp.escape(base)}$/, '')
580
+ end
581
+
582
+ def ensure_logger
583
+ @@logger ||= configuration[:logger]
584
+ # Setup default logger to console
585
+ if @@logger.nil?
586
+ require 'log4r'
587
+ @@logger = Log4r::Logger.new('activeldap')
588
+ @@logger.level = Log4r::OFF
589
+ Log4r::StderrOutputter.new 'console'
590
+ @@logger.add('console')
591
+ end
592
+ configuration[:logger] ||= @@logger
593
+ end
594
+
595
+ def instantiate(entry)
596
+ dn, attributes = entry
597
+ if self.class == Class
598
+ klass = self.ancestors[0].to_s.split(':').last
599
+ real_klass = self.ancestors[0]
600
+ else
601
+ klass = self.class.to_s.split(':').last
602
+ real_klass = self.class
603
+ end
604
+
605
+ obj = real_klass.allocate
606
+ obj.instance_eval do
607
+ initialize_by_ldap_data(dn, attributes)
608
+ end
609
+ obj
610
+ end
611
+
612
+ def default_dn_attribute
613
+ if name.empty?
614
+ "cn"
615
+ else
616
+ Inflector.underscore(Inflector.demodulize(name))
617
+ end
618
+ end
619
+
620
+ def default_prefix
621
+ if name.empty?
622
+ nil
623
+ else
624
+ "ou=#{Inflector.pluralize(Inflector.demodulize(name))}"
625
+ end
626
+ end
627
+ end
628
+
629
+ self.ldap_scope = :sub
630
+ self.required_classes = ['top']
631
+
632
+ include Enumerable
633
+
634
+ ### All instance methods, etc
635
+
636
+ # new
637
+ #
638
+ # Creates a new instance of Base initializing all class and all
639
+ # initialization. Defines local defaults. See examples If multiple values
640
+ # exist for dn_attribute, the first one put here will be authoritative
641
+ def initialize(attributes=nil)
642
+ init_base
643
+ @new_entry = true
644
+ if attributes.is_a?(String) or attributes.is_a?(Array)
645
+ apply_object_class(required_classes)
646
+ self.dn = attributes
647
+ elsif attributes.is_a?(Hash)
648
+ classes, attributes = extract_object_class(attributes)
649
+ apply_object_class(classes | required_classes)
650
+ normalized_attributes = {}
651
+ attributes.each do |key, value|
652
+ real_key = to_real_attribute_name(key)
653
+ normalized_attributes[real_key] = value if real_key
654
+ end
655
+ self.dn = normalized_attributes[dn_attribute]
656
+ self.attributes = normalized_attributes
657
+ end
658
+ yield self if block_given?
659
+ end
660
+
661
+ # Returns true if the +comparison_object+ is the same object, or is of
662
+ # the same type and has the same dn.
663
+ def ==(comparison_object)
664
+ comparison_object.equal?(self) or
665
+ (comparison_object.instance_of?(self.class) and
666
+ comparison_object.dn == dn and
667
+ !comparison_object.new_entry?)
668
+ end
669
+
670
+ # Delegates to ==
671
+ def eql?(comparison_object)
672
+ self == (comparison_object)
673
+ end
674
+
675
+ # Delegates to id in order to allow two records of the same type and id
676
+ # to work with something like:
677
+ # [ User.find("a"), User.find("b"), User.find("c") ] &
678
+ # [ User.find("a"), User.find("d") ] # => [ User.find("a") ]
679
+ def hash
680
+ dn.hash
681
+ end
682
+
683
+ def may
684
+ ensure_apply_object_class
685
+ @may
686
+ end
687
+
688
+ def must
689
+ ensure_apply_object_class
690
+ @must
691
+ end
692
+
693
+ # attributes
694
+ #
695
+ # Return attribute methods so that a program can determine available
696
+ # attributes dynamically without schema awareness
697
+ def attribute_names
698
+ logger.debug {"stub: attribute_names called"}
699
+ ensure_apply_object_class
700
+ return @attr_methods.keys
701
+ end
702
+
703
+ def attribute_present?(name)
704
+ values = get_attribute(name, true)
705
+ !values.empty? or values.any? {|x| not (x and x.empty?)}
706
+ end
707
+
708
+ # exists?
709
+ #
710
+ # Return whether the entry exists in LDAP or not
711
+ def exists?
712
+ self.class.exists?(dn)
713
+ end
714
+
715
+ # new_entry?
716
+ #
717
+ # Return whether the entry is new entry in LDAP or not
718
+ def new_entry?
719
+ @new_entry
720
+ end
721
+
722
+ # dn
723
+ #
724
+ # Return the authoritative dn
725
+ def dn
726
+ logger.debug {"stub: dn called"}
727
+ dn_value = id
728
+ if dn_value.nil?
729
+ raise DistinguishedNameNotSetError.new,
730
+ "#{dn_attribute} value of #{self} doesn't set"
731
+ end
732
+ _base = base
733
+ _base = nil if _base.empty?
734
+ ["#{dn_attribute}=#{dn_value}", _base].compact.join(",")
735
+ end
736
+
737
+ def id
738
+ get_attribute(dn_attribute)
739
+ end
740
+
741
+ def dn=(value)
742
+ set_attribute(dn_attribute, value)
743
+ end
744
+ alias_method(:id=, :dn=)
745
+
746
+ # destroy
747
+ #
748
+ # Delete this entry from LDAP
749
+ def destroy
750
+ logger.debug {"stub: delete called"}
751
+ begin
752
+ self.class.delete(dn)
753
+ @new_entry = true
754
+ rescue Error
755
+ raise DeleteError.new("Failed to delete LDAP entry: '#{dn}'")
756
+ end
757
+ end
758
+
759
+ # save
760
+ #
761
+ # Save and validate this object into LDAP
762
+ # either adding or replacing attributes
763
+ # TODO: Relative DN support
764
+ def save
765
+ create_or_update
766
+ end
767
+
768
+ def save!
769
+ unless create_or_update
770
+ raise EntryNotSaved, "entry #{dn} can't saved"
771
+ end
772
+ end
773
+
774
+ # method_missing
775
+ #
776
+ # If a given method matches an attribute or an attribute alias
777
+ # then call the appropriate method.
778
+ # TODO: Determine if it would be better to define each allowed method
779
+ # using class_eval instead of using method_missing. This would
780
+ # give tab completion in irb.
781
+ def method_missing(name, *args, &block)
782
+ logger.debug {"stub: called method_missing" +
783
+ "(#{name.inspect}, #{args.inspect})"}
784
+ ensure_apply_object_class
785
+
786
+ key = name.to_s
787
+ case key
788
+ when /=$/
789
+ real_key = $PREMATCH
790
+ logger.debug {"method_missing: have_attribute? #{real_key}"}
791
+ if have_attribute?(real_key, ['objectClass'])
792
+ if args.size != 1
793
+ raise ArgumentError,
794
+ "wrong number of arguments (#{args.size} for 1)"
795
+ end
796
+ logger.debug {"method_missing: calling set_attribute" +
797
+ "(#{real_key}, #{args.inspect})"}
798
+ return set_attribute(real_key, *args, &block)
799
+ end
800
+ when /(?:(_before_type_cast)|(\?))?$/
801
+ real_key = $PREMATCH
802
+ before_type_cast = !$1.nil?
803
+ query = !$2.nil?
804
+ logger.debug {"method_missing: have_attribute? #{real_key}"}
805
+ if have_attribute?(real_key, ['objectClass'])
806
+ if args.size > 1
807
+ raise ArgumentError,
808
+ "wrong number of arguments (#{args.size} for 1)"
809
+ end
810
+ if before_type_cast
811
+ return get_attribute_before_type_cast(real_key, *args)
812
+ elsif query
813
+ return get_attribute_as_query(real_key, *args)
814
+ else
815
+ return get_attribute(real_key, *args)
816
+ end
817
+ end
818
+ end
819
+ super
820
+ end
821
+
822
+ # Add available attributes to the methods
823
+ def methods(inherited_too=true)
824
+ ensure_apply_object_class
825
+ target_names = @attr_methods.keys + @attr_aliases.keys - ['objectClass']
826
+ super + target_names.uniq.collect do |x|
827
+ [x, "#{x}=", "#{x}?", "#{x}_before_type_cast"]
828
+ end.flatten
829
+ end
830
+
831
+ alias_method :respond_to_without_attributes?, :respond_to?
832
+ def respond_to?(name, include_priv=false)
833
+ have_attribute?(name.to_s) or
834
+ (/(?:=|\?|_before_type_cast)$/ =~ name.to_s and
835
+ have_attribute?($PREMATCH)) or
836
+ super
837
+ end
838
+
839
+ # Updates a given attribute and saves immediately
840
+ def update_attribute(name, value)
841
+ set_attribute(name, value) if have_attribute?(name)
842
+ save
843
+ end
844
+
845
+ # This performs a bulk update of attributes and immediately
846
+ # calls #save.
847
+ def update_attributes(attrs)
848
+ self.attributes = attrs
849
+ save
850
+ end
851
+
852
+ # This returns the key value pairs in @data with all values
853
+ # cloned
854
+ def attributes
855
+ Marshal.load(Marshal.dump(@data))
856
+ end
857
+
858
+ # This allows a bulk update to the attributes of a record
859
+ # without forcing an immediate save or validation.
860
+ #
861
+ # It is unwise to attempt objectClass updates this way.
862
+ # Also be sure to only pass in key-value pairs of your choosing.
863
+ # Do not let URL/form hackers supply the keys.
864
+ def attributes=(hash_or_assoc)
865
+ targets = remove_attributes_protected_from_mass_assignment(hash_or_assoc)
866
+ targets.each do |key, value|
867
+ set_attribute(key, value) if have_attribute?(key)
868
+ end
869
+ end
870
+
871
+ def to_ldif
872
+ self.class.to_ldif(dn, normalize_data(@data))
873
+ end
874
+
875
+ def to_xml(options={})
876
+ root = options[:root] || Inflector.underscore(self.class.name)
877
+ result = "<#{root}>\n"
878
+ result << " <dn>#{dn}</dn>\n"
879
+ normalize_data(@data).sort_by {|key, values| key}.each do |key, values|
880
+ targets = []
881
+ values.each do |value|
882
+ if value.is_a?(Hash)
883
+ value.each do |option, real_value|
884
+ targets << [real_value, " #{option}=\"true\""]
885
+ end
886
+ else
887
+ targets << [value]
888
+ end
889
+ end
890
+ targets.sort_by {|value, attr| value}.each do |value, attr|
891
+ result << " <#{key}#{attr}>#{value}</#{key}>\n"
892
+ end
893
+ end
894
+ result << "</#{root}>\n"
895
+ result
896
+ end
897
+
898
+ def have_attribute?(name, except=[])
899
+ real_name = to_real_attribute_name(name)
900
+ real_name and !except.include?(real_name)
901
+ end
902
+ alias_method :has_attribute?, :have_attribute?
903
+
904
+ def reload
905
+ _, attributes = self.class.search(:value => id).find do |_dn, _attributes|
906
+ dn == _dn
907
+ end
908
+ raise EntryNotFound, "Can't find dn '#{dn}' to reload" if attributes.nil?
909
+
910
+ @ldap_data.update(attributes)
911
+ classes, attributes = extract_object_class(attributes)
912
+ apply_object_class(classes)
913
+ self.attributes = attributes
914
+ @new_entry = false
915
+ self
916
+ end
917
+
918
+ def [](name, force_array=false)
919
+ get_attribute(name, force_array)
920
+ end
921
+
922
+ def []=(name, value)
923
+ set_attribute(name, value)
924
+ end
925
+
926
+ def each
927
+ @data.each do |key, values|
928
+ yield(key.dup, values.dup)
929
+ end
930
+ end
931
+
932
+ private
933
+ def logger
934
+ @@logger
935
+ end
936
+
937
+ def extract_object_class(attributes)
938
+ classes = []
939
+ attrs = attributes.reject do |key, value|
940
+ if key.to_s == 'objectClass' or
941
+ Inflector.underscore(key) == 'object_class'
942
+ classes |= [value].flatten
943
+ true
944
+ else
945
+ false
946
+ end
947
+ end
948
+ [classes, attrs]
949
+ end
950
+
951
+ def init_base
952
+ check_configuration
953
+ init_instance_variables
954
+ end
955
+
956
+ def initialize_by_ldap_data(dn, attributes)
957
+ init_base
958
+ @new_entry = false
959
+ @ldap_data = attributes
960
+ classes, attributes = extract_object_class(attributes)
961
+ apply_object_class(classes)
962
+ self.dn = dn
963
+ self.attributes = attributes
964
+ yield self if block_given?
965
+ end
966
+
967
+ def to_real_attribute_name(name)
968
+ ensure_apply_object_class
969
+ name = name.to_s
970
+ @attr_methods[name] || @attr_aliases[Inflector.underscore(name)]
971
+ end
972
+
973
+ def ensure_apply_object_class
974
+ current_object_class = @data['objectClass']
975
+ return if current_object_class.nil? or current_object_class == @last_oc
976
+ apply_object_class(current_object_class)
977
+ end
978
+
979
+ # enforce_type
980
+ #
981
+ # enforce_type applies your changes without attempting to write to LDAP.
982
+ # This means that if you set userCertificate to somebinary value, it will
983
+ # wrap it up correctly.
984
+ def enforce_type(key, value)
985
+ logger.debug {"stub: enforce_type called"}
986
+ ensure_apply_object_class
987
+ # Enforce attribute value formatting
988
+ result = self.class.normalize_attribute(key, value)[1]
989
+ logger.debug {"stub: enforce_types done"}
990
+ result
991
+ end
992
+
993
+ def init_instance_variables
994
+ @data = {} # where the r/w entry data is stored
995
+ @ldap_data = {} # original ldap entry data
996
+ @attr_methods = {} # list of valid method calls for attributes used for
997
+ # dereferencing
998
+ @attr_aliases = {} # aliases of @attr_methods
999
+ @last_oc = false # for use in other methods for "caching"
1000
+ @base = nil
1001
+ end
1002
+
1003
+ # apply_object_class
1004
+ #
1005
+ # objectClass= special case for updating appropriately
1006
+ # This updates the objectClass entry in @data. It also
1007
+ # updating all required and allowed attributes while
1008
+ # removing defined attributes that are no longer valid
1009
+ # given the new objectclasses.
1010
+ def apply_object_class(val)
1011
+ logger.debug {"stub: objectClass=(#{val.inspect}) called"}
1012
+ new_oc = val
1013
+ new_oc = [val] if new_oc.class != Array
1014
+ new_oc = new_oc.uniq
1015
+ return new_oc if @last_oc == new_oc
1016
+
1017
+ # Store for caching purposes
1018
+ @last_oc = new_oc.dup
1019
+
1020
+ # Set the actual objectClass data
1021
+ define_attribute_methods('objectClass')
1022
+ replace_class(*new_oc)
1023
+
1024
+ # Build |data| from schema
1025
+ # clear attr_method mapping first
1026
+ @attr_methods = {}
1027
+ @attr_aliases = {}
1028
+ @musts = {}
1029
+ @mays = {}
1030
+ new_oc.each do |objc|
1031
+ # get all attributes for the class
1032
+ attributes = schema.class_attributes(objc)
1033
+ @musts[objc] = attributes[:must]
1034
+ @mays[objc] = attributes[:may]
1035
+ end
1036
+ @must = @musts.values.flatten.uniq
1037
+ @may = @mays.values.flatten.uniq
1038
+ (@must + @may).uniq.each do |attr|
1039
+ # Update attr_method with appropriate
1040
+ define_attribute_methods(attr)
1041
+ end
1042
+ end
1043
+
1044
+ alias_method :base_of_class, :base
1045
+ def base
1046
+ logger.debug {"stub: called base"}
1047
+ [@base, base_of_class].compact.join(",")
1048
+ end
1049
+
1050
+ undef_method :base=
1051
+ def base=(object_local_base)
1052
+ @base = object_local_base
1053
+ end
1054
+
1055
+ # get_attribute
1056
+ #
1057
+ # Return the value of the attribute called by method_missing?
1058
+ def get_attribute(name, force_array=false)
1059
+ logger.debug {"stub: called get_attribute" +
1060
+ "(#{name.inspect}, #{force_array.inspect}"}
1061
+ get_attribute_before_type_cast(name, force_array)
1062
+ end
1063
+
1064
+ def get_attribute_as_query(name, force_array=false)
1065
+ logger.debug {"stub: called get_attribute_as_query" +
1066
+ "(#{name.inspect}, #{force_array.inspect}"}
1067
+ value = get_attribute_before_type_cast(name, force_array)
1068
+ if force_array
1069
+ value.collect {|x| !false_value?(x)}
1070
+ else
1071
+ !false_value?(value)
1072
+ end
1073
+ end
1074
+
1075
+ def false_value?(value)
1076
+ value.nil? or value == false or value == [] or
1077
+ value == "false" or value == "FALSE" or value == ""
1078
+ end
1079
+
1080
+ def get_attribute_before_type_cast(name, force_array=false)
1081
+ logger.debug {"stub: called get_attribute_before_type_cast" +
1082
+ "(#{name.inspect}, #{force_array.inspect}"}
1083
+ attr = to_real_attribute_name(name)
1084
+
1085
+ value = @data[attr] || []
1086
+ # Return a copy of the stored data
1087
+ if force_array
1088
+ value.dup
1089
+ else
1090
+ array_of(value.dup, false)
1091
+ end
1092
+ end
1093
+
1094
+ # set_attribute
1095
+ #
1096
+ # Set the value of the attribute called by method_missing?
1097
+ def set_attribute(name, value)
1098
+ logger.debug {"stub: called set_attribute" +
1099
+ "(#{name.inspect}, #{value.inspect})"}
1100
+
1101
+ # Get the attr and clean up the input
1102
+ attr = to_real_attribute_name(name)
1103
+
1104
+ if attr == dn_attribute and value.is_a?(String)
1105
+ value = value.gsub(/,#{Regexp.escape(base_of_class)}$/, '')
1106
+ value, @base = value.split(/,/, 2)
1107
+ value = $POSTMATCH if /^#{dn_attribute}=/ =~ value
1108
+ end
1109
+
1110
+ logger.debug {"set_attribute(#{name.inspect}, #{value.inspect}): " +
1111
+ "method maps to #{attr}"}
1112
+
1113
+ # Enforce LDAP-pleasing values
1114
+ logger.debug {"value = #{value.inspect}, value.class = #{value.class}"}
1115
+ real_value = value
1116
+ # Squash empty values
1117
+ if value.class == Array
1118
+ real_value = value.collect {|c| (c.nil? or c.empty?) ? [] : c}.flatten
1119
+ end
1120
+ real_value = [] if real_value.nil?
1121
+ real_value = [] if real_value == ''
1122
+ real_value = [real_value] if real_value.class == String
1123
+ real_value = [real_value.to_s] if real_value.class == Fixnum
1124
+ # NOTE: Hashes are allowed for subtyping.
1125
+
1126
+ # Assign the value
1127
+ @data[attr] = enforce_type(attr, real_value)
1128
+
1129
+ # Return the passed in value
1130
+ logger.debug {"stub: exiting set_attribute"}
1131
+ @data[attr]
1132
+ end
1133
+
1134
+
1135
+ # define_attribute_methods
1136
+ #
1137
+ # Make a method entry for _every_ alias of a valid attribute and map it
1138
+ # onto the first attribute passed in.
1139
+ def define_attribute_methods(attr)
1140
+ logger.debug {"stub: called define_attribute_methods(#{attr.inspect})"}
1141
+ return if @attr_methods.has_key? attr
1142
+ aliases = schema.attribute_aliases(attr)
1143
+ aliases.each do |ali|
1144
+ logger.debug {"associating #{ali} --> #{attr}"}
1145
+ @attr_methods[ali] = attr
1146
+ logger.debug {"associating #{Inflector.underscore(ali)}" +
1147
+ " --> #{attr}"}
1148
+ @attr_aliases[Inflector.underscore(ali)] = attr
1149
+ end
1150
+ logger.debug {"stub: leaving define_attribute_methods(#{attr.inspect})"}
1151
+ end
1152
+
1153
+ # array_of
1154
+ #
1155
+ # Returns the array form of a value, or not an array if
1156
+ # false is passed in.
1157
+ def array_of(value, to_a=true)
1158
+ logger.debug {"stub: called array_of" +
1159
+ "(#{value.inspect}, #{to_a.inspect})"}
1160
+ case value
1161
+ when Array
1162
+ if to_a or value.size > 1
1163
+ value.collect {|v| array_of(v, to_a)}
1164
+ else
1165
+ if value.empty?
1166
+ nil
1167
+ else
1168
+ array_of(value.first, to_a)
1169
+ end
1170
+ end
1171
+ when Hash
1172
+ if to_a
1173
+ [value]
1174
+ else
1175
+ result = {}
1176
+ value.each {|k, v| result[k] = array_of(v, to_a)}
1177
+ result
1178
+ end
1179
+ else
1180
+ to_a ? [value.to_s] : value.to_s
1181
+ end
1182
+ end
1183
+
1184
+ def normalize_data(data, except=[])
1185
+ result = {}
1186
+ data.each do |key, values|
1187
+ next if except.include?(key)
1188
+ real_name = to_real_attribute_name(key)
1189
+ next if real_name and except.include?(real_name)
1190
+ real_name ||= key
1191
+ result[real_name] ||= []
1192
+ result[real_name].concat(values)
1193
+ end
1194
+ result
1195
+ end
1196
+
1197
+ def collect_modified_entries(ldap_data, data)
1198
+ entries = []
1199
+ # Now that all the subtypes will be treated as unique attributes
1200
+ # we can see what's changed and add anything that is brand-spankin'
1201
+ # new.
1202
+ logger.debug {'#collect_modified_entries: traversing ldap_data ' +
1203
+ 'determining replaces and deletes'}
1204
+ ldap_data.each do |k, v|
1205
+ value = data[k] || []
1206
+
1207
+ next if v == value
1208
+
1209
+ # Create mod entries
1210
+ if value.empty?
1211
+ # Since some types do not have equality matching rules,
1212
+ # delete doesn't work
1213
+ # Replacing with nothing is equivalent.
1214
+ logger.debug {"#save: removing attribute from existing entry: " +
1215
+ "#{new_key}"}
1216
+ if !data.has_key?(k) and schema.binary_required?(k)
1217
+ value = [{'binary' => []}]
1218
+ end
1219
+ else
1220
+ # Ditched delete then replace because attribs with no equality
1221
+ # match rules will fails
1222
+ logger.debug {"#collect_modified_entries: updating attribute of" +
1223
+ " existing entry: #{k}: #{value.inspect}"}
1224
+ end
1225
+ entries.push([:replace, k, value])
1226
+ end
1227
+ logger.debug {'#collect_modified_entries: finished traversing' +
1228
+ ' ldap_data'}
1229
+ logger.debug {'#collect_modified_entries: traversing data ' +
1230
+ 'determining adds'}
1231
+ data.each do |k, v|
1232
+ value = v || []
1233
+ next if ldap_data.has_key?(k) or value.empty?
1234
+
1235
+ # Detect subtypes and account for them
1236
+ logger.debug {"#save: adding attribute to existing entry: " +
1237
+ "#{k}: #{value.inspect}"}
1238
+ # REPLACE will function like ADD, but doesn't hit EQUALITY problems
1239
+ # TODO: Added equality(attr) to Schema
1240
+ entries.push([:replace, k, value])
1241
+ end
1242
+
1243
+ entries
1244
+ end
1245
+
1246
+ def collect_all_entries(data)
1247
+ dn_attr = to_real_attribute_name(dn_attribute)
1248
+ dn_value = data[dn_attr]
1249
+ logger.debug {'#collect_all_entries: adding all attribute value pairs'}
1250
+ logger.debug {"#collect_all_entries: adding " +
1251
+ "#{dn_attr.inspect} = #{dn_value.inspect}"}
1252
+
1253
+ entries = []
1254
+ entries.push([:add, dn_attr, dn_value])
1255
+
1256
+ oc_value = data['objectClass']
1257
+ logger.debug {"#collect_all_entries: adding objectClass = " +
1258
+ "#{oc_value.inspect}"}
1259
+ entries.push([:add, 'objectClass', oc_value])
1260
+ data.each do |key, value|
1261
+ next if value.empty? or key == 'objectClass' or key == dn_attr
1262
+
1263
+ logger.debug {"#collect_all_entries: adding attribute to new " +
1264
+ "entry: #{key.inspect}: #{value.inspect}"}
1265
+ entries.push([:add, key, value])
1266
+ end
1267
+
1268
+ entries
1269
+ end
1270
+
1271
+ def check_configuration
1272
+ unless dn_attribute
1273
+ raise ConfigurationError,
1274
+ "dn_attribute not set for this class: #{self.class}"
1275
+ end
1276
+ end
1277
+
1278
+ def create_or_update
1279
+ new_entry? ? create : update
1280
+ end
1281
+
1282
+ def prepare_data_for_saving
1283
+ logger.debug {"stub: save called"}
1284
+
1285
+ # Expand subtypes to real ldap_data entries
1286
+ # We can't reuse @ldap_data because an exception would leave
1287
+ # an object in an unknown state
1288
+ logger.debug {"#save: expanding subtypes in @ldap_data"}
1289
+ ldap_data = normalize_data(@ldap_data)
1290
+ logger.debug {'#save: subtypes expanded for @ldap_data'}
1291
+
1292
+ # Expand subtypes to real data entries, but leave @data alone
1293
+ logger.debug {'#save: expanding subtypes for @data'}
1294
+ bad_attrs = @data.keys - attribute_names
1295
+ data = normalize_data(@data, bad_attrs)
1296
+ logger.debug {'#save: subtypes expanded for @data'}
1297
+
1298
+ success = yield(data, ldap_data)
1299
+
1300
+ if success
1301
+ logger.debug {"#save: resetting @ldap_data to a dup of @data"}
1302
+ @ldap_data = Marshal.load(Marshal.dump(data))
1303
+ # Delete items disallowed by objectclasses.
1304
+ # They should have been removed from ldap.
1305
+ logger.debug {'#save: removing attributes from @ldap_data not ' +
1306
+ 'sent in data'}
1307
+ bad_attrs.each do |remove_me|
1308
+ @ldap_data.delete(remove_me)
1309
+ end
1310
+ logger.debug {'#save: @ldap_data reset complete'}
1311
+ end
1312
+
1313
+ logger.debug {'stub: save exited'}
1314
+ success
1315
+ end
1316
+
1317
+ def create
1318
+ prepare_data_for_saving do |data, ldap_data|
1319
+ entries = collect_all_entries(data)
1320
+ logger.debug {"#create: adding #{dn}"}
1321
+ begin
1322
+ self.class.add(dn, entries)
1323
+ logger.debug {"#create: add successful"}
1324
+ @new_entry = false
1325
+ rescue UnwillingToPerform
1326
+ logger.warn {"#create: didn't perform: #{$!.message}"}
1327
+ end
1328
+ true
1329
+ end
1330
+ end
1331
+
1332
+ def update
1333
+ prepare_data_for_saving do |data, ldap_data|
1334
+ entries = collect_modified_entries(ldap_data, data)
1335
+ logger.debug {'#update: traversing data complete'}
1336
+ logger.debug {"#update: modifying #{dn}"}
1337
+ self.class.modify(dn, entries)
1338
+ logger.debug {'#update: modify successful'}
1339
+ true
1340
+ end
1341
+ end
1342
+ end # Base
1343
+ end # ActiveLdap