ldaptic 0.2.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 (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +104 -0
  3. data/Rakefile +41 -0
  4. data/lib/ldaptic.rb +151 -0
  5. data/lib/ldaptic/active_model.rb +37 -0
  6. data/lib/ldaptic/adapters.rb +90 -0
  7. data/lib/ldaptic/adapters/abstract_adapter.rb +123 -0
  8. data/lib/ldaptic/adapters/active_directory_adapter.rb +78 -0
  9. data/lib/ldaptic/adapters/active_directory_ext.rb +12 -0
  10. data/lib/ldaptic/adapters/ldap_conn_adapter.rb +262 -0
  11. data/lib/ldaptic/adapters/net_ldap_adapter.rb +173 -0
  12. data/lib/ldaptic/adapters/net_ldap_ext.rb +24 -0
  13. data/lib/ldaptic/attribute_set.rb +283 -0
  14. data/lib/ldaptic/dn.rb +365 -0
  15. data/lib/ldaptic/entry.rb +646 -0
  16. data/lib/ldaptic/error_set.rb +34 -0
  17. data/lib/ldaptic/errors.rb +136 -0
  18. data/lib/ldaptic/escape.rb +110 -0
  19. data/lib/ldaptic/filter.rb +282 -0
  20. data/lib/ldaptic/methods.rb +387 -0
  21. data/lib/ldaptic/railtie.rb +9 -0
  22. data/lib/ldaptic/schema.rb +246 -0
  23. data/lib/ldaptic/syntaxes.rb +319 -0
  24. data/test/core.schema +582 -0
  25. data/test/ldaptic_active_model_test.rb +40 -0
  26. data/test/ldaptic_adapters_test.rb +35 -0
  27. data/test/ldaptic_attribute_set_test.rb +57 -0
  28. data/test/ldaptic_dn_test.rb +110 -0
  29. data/test/ldaptic_entry_test.rb +22 -0
  30. data/test/ldaptic_errors_test.rb +23 -0
  31. data/test/ldaptic_escape_test.rb +47 -0
  32. data/test/ldaptic_filter_test.rb +53 -0
  33. data/test/ldaptic_hierarchy_test.rb +90 -0
  34. data/test/ldaptic_schema_test.rb +44 -0
  35. data/test/ldaptic_syntaxes_test.rb +66 -0
  36. data/test/mock_adapter.rb +47 -0
  37. data/test/rbslapd1.rb +111 -0
  38. data/test/rbslapd4.rb +172 -0
  39. data/test/test_helper.rb +2 -0
  40. metadata +146 -0
data/lib/ldaptic/dn.rb ADDED
@@ -0,0 +1,365 @@
1
+ require 'ldaptic/escape'
2
+
3
+ module Ldaptic
4
+
5
+ # Instantiate a new Ldaptic::DN object with the arguments given. Unlike
6
+ # Ldaptic::DN.new(dn), this method coerces the first argument to a string,
7
+ # unless it is already a string or an array. If the first argument is nil,
8
+ # nil is returned.
9
+ def self.DN(dn, source = nil)
10
+ return if dn.nil?
11
+ dn = dn.dn if dn.respond_to?(:dn)
12
+ if dn.kind_of?(::Ldaptic::DN)
13
+ if source
14
+ dn = dn.dup
15
+ dn.source = source
16
+ end
17
+ return dn
18
+ end
19
+ if dn.respond_to?(:to_hash)
20
+ dn = [dn]
21
+ elsif ! dn.respond_to?(:to_ary)
22
+ dn = dn.to_s
23
+ end
24
+ DN.new(dn, source)
25
+ end
26
+
27
+ # RFC4512 - Lightweight Directory Access Protocol (LDAP): Directory Information Models
28
+ # RFC4514 - Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names
29
+ #
30
+ class DN < ::String
31
+
32
+ OID = '1.3.6.1.4.1.1466.115.121.1.12' unless defined? OID
33
+ # Ldaptic::DN[{:dc => 'com'}, {:dc => 'amazon'}]
34
+ # => "dc=amazon,dc=com"
35
+ def self.[](*args)
36
+ Ldaptic::DN(args.reverse)
37
+ end
38
+
39
+ attr_accessor :source
40
+
41
+ # Create a new Ldaptic::DN object. dn can either be a string, or an array
42
+ # of pairs.
43
+ #
44
+ # Ldaptic::DN([{:cn=>"Thomas, David"}, {:dc=>"pragprog"}, {:dc=>"com"}])
45
+ # # => "CN=Thomas\\, David,DC=pragprog,DC=com"
46
+ #
47
+ # The optional second object specifies either an LDAP::Conn object or a
48
+ # Ldaptic object to be used to find the DN with #find.
49
+ def initialize(dn, source = nil)
50
+ @source = source
51
+ dn = dn.dn if dn.respond_to?(:dn)
52
+ if dn.respond_to?(:to_ary)
53
+ dn = dn.map do |pair|
54
+ if pair.kind_of?(Hash)
55
+ Ldaptic::RDN(pair).to_str
56
+ else
57
+ pair
58
+ end
59
+ end * ','
60
+ end
61
+ if dn.include?(".") && !dn.include?("=")
62
+ dn = dn.split(".").map {|dc| "DC=#{Ldaptic.escape(dc)}"} * ","
63
+ end
64
+ super(dn)
65
+ end
66
+
67
+ def to_dn
68
+ self
69
+ end
70
+
71
+ # If a source object was given, it is used to search for the DN.
72
+ # Otherwise, an exception is raised.
73
+ def find(source = @source)
74
+ scope = 0
75
+ filter = "(objectClass=*)"
76
+ if source.respond_to?(:search2_ext)
77
+ source.search2(
78
+ self.to_s,
79
+ scope,
80
+ filter
81
+ )
82
+ elsif source.respond_to?(:search)
83
+ Array(source.search(
84
+ :base => self.to_s,
85
+ :scope => scope,
86
+ :filter => filter,
87
+ :limit => 1
88
+ ))
89
+ else
90
+ raise RuntimeError, "missing or invalid source for LDAP search", caller
91
+ end.first
92
+ end
93
+
94
+ # Convert the DN to an array of RDNs.
95
+ #
96
+ # Ldaptic::DN("cn=Thomas\\, David,dc=pragprog,dc=com").rdns
97
+ # # => [{:cn=>"Thomas, David"},{:dc=>"pragprog"},{:dc=>"com"}]
98
+ def rdns
99
+ rdn_strings.map {|rdn| RDN.new(rdn)}
100
+ end
101
+
102
+ def rdn_strings
103
+ Ldaptic.split(self, ?,)
104
+ end
105
+
106
+ def to_a
107
+ # This is really horrid, but the last hack broke. Consider abandoning
108
+ # this method entirely.
109
+ if caller.first =~ /:in `Array'$/
110
+ [self]
111
+ else
112
+ rdns
113
+ end
114
+ end
115
+
116
+ def parent
117
+ Ldaptic::DN(rdns[1..-1], source)
118
+ end
119
+
120
+ def rdn
121
+ rdns.first
122
+ end
123
+
124
+ def normalize
125
+ Ldaptic::DN(rdns, source)
126
+ end
127
+
128
+ def normalize!
129
+ replace(normalize)
130
+ end
131
+
132
+ # TODO: investigate compliance with
133
+ # RFC4517 - Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching Rules
134
+ def ==(other)
135
+ if other.respond_to?(:dn)
136
+ other = Ldaptic::DN(other)
137
+ end
138
+ normalize = lambda do |hash|
139
+ hash.inject({}) do |m, (k, v)|
140
+ m[Ldaptic.encode(k).upcase] = v
141
+ m
142
+ end
143
+ end
144
+ if other.kind_of?(Ldaptic::DN)
145
+ self.rdns == other.rdns
146
+ else
147
+ super
148
+ end
149
+ # rescue
150
+ # super
151
+ end
152
+
153
+ # Pass in one or more hashes to augment the DN. Otherwise, this behaves
154
+ # the same as String#[]
155
+
156
+ def [](*args)
157
+ if args.first.kind_of?(Hash) || args.first.kind_of?(Ldaptic::DN)
158
+ send(:/, *args)
159
+ else
160
+ super
161
+ end
162
+ end
163
+
164
+ # Prepend an RDN to the DN.
165
+ #
166
+ # Ldaptic::DN(:dc => "com")/{:dc => "foobar"} #=> "DC=foobar,DC=com"
167
+ def /(*args)
168
+ Ldaptic::DN(args.reverse + rdns, source)
169
+ end
170
+
171
+ # With a Hash (and only with a Hash), prepends a RDN to the DN, modifying
172
+ # the receiver in place. Otherwise, behaves like String#<<.
173
+ def <<(arg)
174
+ if arg.kind_of?(Hash)
175
+ replace(self/arg)
176
+ else
177
+ super
178
+ end
179
+ end
180
+
181
+ # With a Hash, check for the presence of an RDN. Otherwise, behaves like
182
+ # String#include?
183
+ def include?(arg)
184
+ if arg.kind_of?(Hash)
185
+ rdns.include?(arg)
186
+ else
187
+ super
188
+ end
189
+ end
190
+
191
+ end
192
+
193
+ def self.RDN(rdn)
194
+ rdn = rdn.rdn if rdn.respond_to?(:rdn)
195
+ if rdn.respond_to?(:to_rdn)
196
+ rdn.to_rdn
197
+ else
198
+ RDN.new(rdn||{})
199
+ end
200
+ end
201
+
202
+ class RDN < Hash
203
+
204
+ def self.parse_string(string) #:nodoc:
205
+
206
+ Ldaptic.split(string, ?+).inject({}) do |hash, pair|
207
+ k, v = Ldaptic.split(pair, ?=).map {|x| Ldaptic.unescape(x)}
208
+ hash[k.downcase.to_sym] = v
209
+ hash
210
+ end
211
+
212
+ rescue
213
+ raise RuntimeError, "error parsing RDN", caller
214
+ end
215
+
216
+ def initialize(rdn = {})
217
+ rdn = rdn.rdn if rdn.respond_to?(:rdn)
218
+ if rdn.kind_of?(String)
219
+ rdn = RDN.parse_string(rdn)
220
+ end
221
+ if rdn.kind_of?(Hash)
222
+ super()
223
+ update(rdn)
224
+ else
225
+ raise TypeError, "default value #{rdn.inspect} not allowed", caller
226
+ end
227
+ end
228
+
229
+ def /(*args)
230
+ Ldaptic::DN([self]).send(:/, *args)
231
+ end
232
+
233
+ def to_rdn
234
+ self
235
+ end
236
+
237
+ def to_str
238
+ collect do |k, v|
239
+ "#{k.kind_of?(String) ? k : Ldaptic.encode(k).upcase}=#{Ldaptic.escape(v)}"
240
+ end.sort.join("+")
241
+ end
242
+
243
+ alias to_s to_str
244
+
245
+ def downcase!
246
+ values.each {|v| v.downcase!}
247
+ self
248
+ end
249
+
250
+ def upcase!
251
+ values.each {|v| v.upcase!}
252
+ self
253
+ end
254
+
255
+ def downcase() clone.downcase! end
256
+ def upcase() clone. upcase! end
257
+
258
+ unless defined? MANDATORY_ATTRIBUTE_TYPES
259
+ MANDATORY_ATTRIBUTE_TYPES = %w(CN L ST O OU C STREET DC UID)
260
+ end
261
+
262
+ MANDATORY_ATTRIBUTE_TYPES.map {|a| a.downcase.to_sym }.each do |type|
263
+ define_method(type) { self[type] }
264
+ end
265
+
266
+ def [](*args)
267
+ if args.size == 1
268
+ if args.first.respond_to?(:to_sym)
269
+ return super(convert_key(args.first))
270
+ elsif args.first.kind_of?(Hash)
271
+ return self/args.first
272
+ end
273
+ end
274
+ to_str[*args]
275
+ end
276
+
277
+ def hash
278
+ to_str.downcase.hash
279
+ end
280
+
281
+ def eql?(other)
282
+ if other.respond_to?(:to_str)
283
+ to_str.casecmp(other.to_str).zero?
284
+ elsif other.kind_of?(Hash)
285
+ eql?(Ldaptic::RDN(other)) rescue false
286
+ else
287
+ super
288
+ end
289
+ end
290
+
291
+ alias == eql?
292
+
293
+ def clone
294
+ inject(RDN.new) do |h, (k, v)|
295
+ h[k] = v.dup; h
296
+ end
297
+ end
298
+
299
+ # Net::LDAP compatibility
300
+ def to_ber #:nodoc:
301
+ to_str.to_ber
302
+ end
303
+
304
+ # Based on ActiveSupport's HashWithIndifferentAccess
305
+
306
+ alias_method :regular_writer, '[]=' unless method_defined?(:regular_writer)
307
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
308
+
309
+ def []=(key, value)
310
+ regular_writer(convert_key(key), convert_value(value))
311
+ end
312
+
313
+ def update(other_hash)
314
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
315
+ self
316
+ end
317
+
318
+ alias_method :merge!, :update
319
+
320
+ def key?(key)
321
+ super(convert_key(key))
322
+ end
323
+
324
+ alias_method :include?, :key?
325
+ alias_method :has_key?, :key?
326
+ alias_method :member?, :key?
327
+
328
+ def fetch(key, *extras)
329
+ super(convert_key(key), *extras)
330
+ end
331
+
332
+ def values_at(*indices)
333
+ indices.collect {|key| self[convert_key(key)]}
334
+ end
335
+
336
+ def dup
337
+ RDN.new(self)
338
+ end
339
+
340
+ def merge(hash)
341
+ self.dup.update(hash)
342
+ end
343
+
344
+ def delete(key)
345
+ super(convert_key(key))
346
+ end
347
+
348
+ private
349
+
350
+ def convert_key(key)
351
+ if key.respond_to?(:to_str)
352
+ key.to_str
353
+ elsif key.respond_to?(:to_sym)
354
+ key.to_sym.to_s
355
+ else
356
+ raise TypeError, "keys in an Ldaptic::RDN must be symbols", caller(1)
357
+ end.downcase.to_sym
358
+ end
359
+
360
+ def convert_value(value)
361
+ value.to_s
362
+ end
363
+
364
+ end
365
+ end
@@ -0,0 +1,646 @@
1
+ require 'ldaptic/attribute_set'
2
+ require 'ldaptic/error_set'
3
+
4
+ module Ldaptic
5
+
6
+ # When a new Ldaptic namespace is created, a Ruby class hierarchy is
7
+ # contructed that mirrors the server's object classes. Ldaptic::Entry
8
+ # serves as the base class for this hierarchy.
9
+ class Entry
10
+ # Constructs a deep copy of a set of LDAP attributes, normalizing them to
11
+ # arrays as appropriate. The returned hash has a default value of [].
12
+ def self.clone_ldap_hash(attributes) #:nodoc:
13
+ hash = Hash.new
14
+ attributes.each do |k, v|
15
+ k = k.kind_of?(Symbol) ? k.to_s.tr('_', '-') : k.dup
16
+ hash[k] = Array(v).map {|x| x.dup rescue x}
17
+ end
18
+ hash
19
+ end
20
+
21
+ # For Active Model compliance. Delegates to #namespace.
22
+ def self.model_name
23
+ namespace.model_name
24
+ end
25
+
26
+ class << self
27
+ attr_reader :oid, :desc, :sup
28
+ %w(obsolete abstract structural auxiliary).each do |attr|
29
+ class_eval("def #{attr}?; !! @#{attr}; end")
30
+ end
31
+
32
+ def logger
33
+ namespace.logger
34
+ end
35
+
36
+ # Returns an array of all names for the object class. Typically the
37
+ # number of names is one, but it is possible for an object class to have
38
+ # aliases.
39
+ def names
40
+ Array(@name)
41
+ end
42
+
43
+ def has_attribute?(attribute)
44
+ attribute = Ldaptic.encode(attribute)
45
+ may.include?(attribute) || must.include?(attribute)
46
+ end
47
+
48
+ def create_accessors #:nodoc:
49
+ to_be_evaled = ""
50
+ (may(false) + must(false)).each do |attr|
51
+ method = attr.to_s.tr_s('-_', '_-')
52
+ to_be_evaled << <<-RUBY
53
+ def #{method}() read_attribute('#{attr}').one end
54
+ def #{method}=(value) write_attribute('#{attr}', value) end
55
+ RUBY
56
+ end
57
+ class_eval(to_be_evaled, __FILE__, __LINE__)
58
+ end
59
+
60
+ # An array of classes that make up the inheritance hierarchy.
61
+ #
62
+ # L::OrganizationalPerson.ldap_ancestors #=> [L::OrganizationalPerson, L::Person, L::Top]
63
+ def ldap_ancestors
64
+ ancestors.select {|o| o.respond_to?(:oid) && o.oid }
65
+ end
66
+
67
+ attr_reader :namespace
68
+
69
+ def may(all = true)
70
+ if all
71
+ core = []
72
+ nott = []
73
+ ldap_ancestors.reverse.each do |klass|
74
+ core |= Array(klass.may(false))
75
+ nott |= Array(klass.must(false))
76
+ end
77
+ if dit = dit_content_rule
78
+ core.push(*Array(dit.may))
79
+ core -= Array(dit.must)
80
+ core -= Array(dit.not)
81
+ end
82
+ core -= nott
83
+ core
84
+ else
85
+ Array(@may)
86
+ end
87
+ end
88
+
89
+ def must(all = true)
90
+ if all
91
+ core = ldap_ancestors.inject([]) do |memo, klass|
92
+ memo |= Array(klass.must(false))
93
+ memo
94
+ end
95
+ if dit = dit_content_rule
96
+ core.push(*Array(dit.must))
97
+ end
98
+ core
99
+ else
100
+ Array(@must)
101
+ end
102
+ end
103
+
104
+ def aux
105
+ if dit_content_rule
106
+ Array(dit_content_rule.aux)
107
+ else
108
+ []
109
+ end
110
+ end
111
+
112
+ def attributes(all = true)
113
+ may(all) + must(all)
114
+ end
115
+
116
+ def dit_content_rule
117
+ namespace.dit_content_rule(oid)
118
+ end
119
+
120
+ def object_class
121
+ @object_class || names.first
122
+ end
123
+
124
+ def object_classes
125
+ ldap_ancestors.map {|a| a.object_class}.compact.reverse.uniq
126
+ end
127
+
128
+ alias objectClass object_classes
129
+
130
+ # Converts an attribute name to a human readable form. For compatibility
131
+ # with ActiveRecord.
132
+ #
133
+ # L::User.human_attribute_name(:givenName) #=> "Given name"
134
+ def human_attribute_name(attribute, options={})
135
+ attribute = Ldaptic.encode(attribute)
136
+ if at = namespace.attribute_type(attribute)
137
+ attribute = at.verbose_name
138
+ end
139
+ attribute = attribute[0..0].upcase + attribute[1..-1]
140
+ attribute.gsub!(/([A-Z])([A-Z][a-z])/) { "#$1 #{$2.downcase}" }
141
+ attribute.gsub!(/([a-z\d])([A-Z])/) { "#$1 #{$2.downcase}" }
142
+ attribute.gsub!(/\b[a-z][A-Z]/) { $&.upcase }
143
+ attribute.gsub!('_', '-')
144
+ attribute
145
+ end
146
+
147
+ def instantiate(attributes) #:nodoc:
148
+ ocs = attributes["objectClass"].to_a.map {|c| namespace.object_class(c)}
149
+ subclass = (@subclasses.to_a & ocs).detect {|x| !x.auxiliary?}
150
+ if subclass
151
+ return subclass.instantiate(attributes)
152
+ end
153
+ unless structural? || ocs.empty?
154
+ logger.warn "#{name}: invalid object class for #{attributes.inspect}"
155
+ end
156
+ obj = allocate
157
+ obj.instance_variable_set(:@dn, ::Ldaptic::DN(Array(attributes.delete('dn')).first, obj))
158
+ obj.instance_variable_set(:@original_attributes, attributes)
159
+ obj.instance_variable_set(:@attributes, {})
160
+ obj.instance_eval { common_initializations; after_load }
161
+ obj
162
+ end
163
+
164
+ protected
165
+ def inherited(subclass) #:nodoc:
166
+ if superclass != Object
167
+ @subclasses ||= []
168
+ @subclasses << subclass
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+ def initialize(data = {})
175
+ Ldaptic::Errors.raise(TypeError.new("abstract class initialized")) if self.class.oid.nil? || self.class.abstract?
176
+ @attributes = {}
177
+ data = data.dup
178
+ if dn = data.delete('dn') || data.delete(:dn)
179
+ dn.first if dn.kind_of?(Array)
180
+ self.dn = dn
181
+ end
182
+ merge_attributes(data)
183
+ @attributes['objectClass'] ||= []
184
+ @attributes['objectClass'].insert(0, *self.class.object_classes).uniq!
185
+ common_initializations
186
+ after_build
187
+ end
188
+
189
+ def merge_attributes(data)
190
+ # If it's a HashWithIndifferentAccess (eg, params in Rails), convert it
191
+ # to a Hash with symbolic keys. This causes the underscore/hyphen
192
+ # translation to take place in write_attribute. Form helpers in Rails
193
+ # use a method name to read data,
194
+ if defined?(::HashWithIndifferentAccess) && data.is_a?(HashWithIndifferentAccess)
195
+ data = data.symbolize_keys
196
+ end
197
+ data.each do |key, value|
198
+ write_attribute(key, value)
199
+ end
200
+ end
201
+
202
+ alias attributes= merge_attributes
203
+
204
+ # A link back to the namespace.
205
+ def namespace
206
+ @namespace || self.class.namespace
207
+ end
208
+
209
+ def logger
210
+ self.class.logger
211
+ end
212
+
213
+ # Returns +self+. For ActiveModel compatibility.
214
+ def to_model
215
+ self
216
+ end
217
+
218
+ attr_reader :dn
219
+
220
+ # The first (relative) component of the distinguished name.
221
+ def rdn
222
+ dn && dn.rdn
223
+ end
224
+
225
+ # Returns an array containing the DN. For ActiveModel compatibility.
226
+ def to_key
227
+ [dn] if persisted?
228
+ end
229
+
230
+ # Returns the DN. For ActiveModel compatibility.
231
+ def to_param
232
+ dn if persisted?
233
+ end
234
+
235
+ # The parent object containing this one.
236
+ def parent
237
+ unless @parent
238
+ @parent = search(:base => dn.parent, :scope => :base, :limit => true)
239
+ @parent.instance_variable_get(:@children)[rdn] = self
240
+ end
241
+ @parent
242
+ end
243
+
244
+ def inspect
245
+ str = "#<#{self.class.inspect} #{dn}"
246
+ (@original_attributes||{}).merge(@attributes).each do |k, values|
247
+ next if values.empty?
248
+ s = (values.size == 1 ? "" : "s")
249
+ at = namespace.attribute_type(k)
250
+ syntax = namespace.attribute_syntax(k)
251
+ if at && syntax && !syntax.x_not_human_readable? && syntax.desc != "Octet String"
252
+ str << " " << k << ": " << values.inspect[1..-2]
253
+ else
254
+ str << " " << k << ": "
255
+ if !at
256
+ str << "(unknown attribute)"
257
+ elsif !syntax
258
+ str << "(unknown type)"
259
+ else
260
+ str << "(" << values.size.to_s << " binary value" << s << ")"
261
+ end
262
+ end
263
+ end
264
+ str << ">"
265
+ end
266
+
267
+ def to_s
268
+ "#<#{self.class} #{dn}>"
269
+ end
270
+
271
+ # Reads an attribute and typecasts it if neccessary. If the argument given
272
+ # is a symbol, underscores are translated into hyphens. Since
273
+ # #method_missing delegates to this method, method names with underscores
274
+ # map to attributes with hyphens.
275
+ def read_attribute(key)
276
+ key = Ldaptic.encode(key)
277
+ @attributes[key] ||= ((@original_attributes || {}).fetch(key, [])).dup
278
+ Ldaptic::AttributeSet.new(self, key, @attributes[key])
279
+ end
280
+ protected :read_attribute
281
+
282
+ # Returns a hash of attributes.
283
+ def attributes
284
+ (@original_attributes||{}).merge(@attributes).keys.inject({}) do |hash, key|
285
+ hash[key] = read_attribute(key)
286
+ hash
287
+ end
288
+ end
289
+
290
+ def changes
291
+ @attributes.reject do |k, v|
292
+ (@original_attributes || {})[k].to_a == v
293
+ end.keys.inject({}) do |hash, key|
294
+ hash[key] = read_attribute(key)
295
+ hash
296
+ end
297
+ end
298
+
299
+ # Change an attribute. This is called by #method_missing and
300
+ # <tt>[]=</tt>.
301
+ #
302
+ # Changes are not committed to the server until #save is called.
303
+ def write_attribute(key, values)
304
+ set = read_attribute(key)
305
+ if values.respond_to?(:to_str) && set.syntax_object && set.syntax_object.error("1\n1")
306
+ values = values.split(/\r?\n/)
307
+ elsif values == ''
308
+ values = []
309
+ end
310
+ set.replace(values)
311
+ end
312
+ protected :write_attribute
313
+
314
+ # Note the values are not typecast and thus must be strings.
315
+ def modify_attribute(action, key, *values)
316
+ key = Ldaptic.encode(key)
317
+ values.flatten!.map! {|v| Ldaptic.encode(v)}
318
+ @original_attributes[key] ||= []
319
+ virgin = @original_attributes[key].dup
320
+ original = Ldaptic::AttributeSet.new(self, key, @original_attributes[key])
321
+ original.__send__(action, values)
322
+ begin
323
+ namespace.modify(dn, [[action, key, values]])
324
+ rescue
325
+ @original_attributes[key] = virgin
326
+ raise $!
327
+ end
328
+ if @attributes[key]
329
+ read_attribute(key).__send__(action, values)
330
+ end
331
+ self
332
+ end
333
+ private :modify_attribute
334
+
335
+ # Commit an array of modifications directly to LDAP, without updating the
336
+ # local object.
337
+ def modify_attributes(mods) #:nodoc:
338
+ namespace.modify(dn, mods)
339
+ self
340
+ end
341
+
342
+ def add!(key, *values) #:nodoc:
343
+ modify_attribute(:add, key, values)
344
+ end
345
+
346
+ def replace!(key, *values) #:nodoc:
347
+ modify_attribute(:replace, key, values)
348
+ end
349
+
350
+ def delete!(key, *values) #:nodoc:
351
+ modify_attribute(:delete, key, values)
352
+ end
353
+
354
+ # Compare an attribute to see if it has a given value. This happens at the
355
+ # server.
356
+ def compare(key, value)
357
+ namespace.compare(dn, key, value)
358
+ end
359
+
360
+ # attr_reader :attributes
361
+ def attribute_names
362
+ attributes.keys
363
+ end
364
+
365
+ def ldap_ancestors
366
+ self.class.ldap_ancestors | objectClass.map {|c|namespace.object_class(c)}
367
+ end
368
+
369
+ def aux
370
+ self['objectClass'].map {|c| namespace.object_class(c)} - self.class.ldap_ancestors
371
+ end
372
+
373
+ def must(all = true)
374
+ self.class.must(all) + aux.map {|a|a.must(false)}.flatten
375
+ end
376
+
377
+ def may(all = true)
378
+ self.class.may(all) + aux.map {|a|a.may(false)}.flatten
379
+ end
380
+
381
+ def may_must(attribute)
382
+ attribute = Ldaptic.encode(attribute)
383
+ if must.include?(attribute)
384
+ :must
385
+ elsif may.include?(attribute)
386
+ :may
387
+ end
388
+ end
389
+
390
+ def respond_to?(method, *) #:nodoc:
391
+ both = may + must
392
+ super || (both + both.map {|x| "#{x}="} + both.map {|x| "#{x}-before-type-cast"}).include?(Ldaptic.encode(method.to_sym))
393
+ end
394
+
395
+ # Delegates to +read_attribute+ or +write_attribute+. Pops an element out
396
+ # of its set if the attribute is marked SINGLE-VALUE.
397
+ def method_missing(method, *args, &block)
398
+ attribute = Ldaptic.encode(method)
399
+ if attribute[-1] == ?=
400
+ attribute.chop!
401
+ if may_must(attribute)
402
+ return write_attribute(attribute, *args, &block)
403
+ end
404
+ elsif attribute[-1] == ??
405
+ attribute.chop!
406
+ if may_must(attribute)
407
+ if args.empty?
408
+ return !read_attribute(attribute).empty?
409
+ else
410
+ return args.flatten.any? {|arg| compare(attribute, arg)}
411
+ end
412
+ end
413
+ elsif attribute =~ /\A(.*)-before-type-cast\z/ && may_must($1)
414
+ return read_attribute($1, *args, &block)
415
+ elsif may_must(attribute)
416
+ return read_attribute(attribute, *args, &block).one
417
+ end
418
+ super(method, *args, &block)
419
+ end
420
+
421
+ # Searches for children. This is identical to Ldaptic::Base#search, only
422
+ # the default base is the current object's DN.
423
+ def search(options, &block)
424
+ if options[:base].kind_of?(Hash)
425
+ options = options.merge(:base => dn/options[:base])
426
+ end
427
+ namespace.search({:base => dn}.merge(options), &block)
428
+ end
429
+
430
+ # Searches for a child, given an RDN.
431
+ def /(*args)
432
+ search(:base => dn.send(:/, *args), :scope => :base, :limit => true)
433
+ end
434
+
435
+ alias find /
436
+
437
+ def fetch(dn = self.dn, options = {}) #:nodoc:
438
+ search({:base => dn}.merge(options))
439
+ end
440
+
441
+ # If a Hash or a String containing "=" is given, the argument is treated as
442
+ # an RDN and a search for a child is performed. +nil+ is returned if no
443
+ # match is found.
444
+ #
445
+ # For a singular String or Symbol argument, that attribute is read with
446
+ # read_attribute. Unlike with method_missing, an array is always returned,
447
+ # making this variant useful for metaprogramming.
448
+ def [](key)
449
+ if key.kind_of?(Hash) || key =~ /=/
450
+ cached_child(key)
451
+ else
452
+ read_attribute(key)
453
+ end
454
+ end
455
+
456
+ def []=(key, value)
457
+ if key.kind_of?(Hash) || key =~ /=/
458
+ assign_child(key, value)
459
+ else
460
+ write_attribute(key, value)
461
+ end
462
+ end
463
+
464
+ # Has the object been saved before?
465
+ def persisted?
466
+ !!@original_attributes
467
+ end
468
+
469
+ def errors
470
+ @errors ||= Ldaptic::ErrorSet.new(self)
471
+ end
472
+
473
+ def valid?
474
+ errors.clear
475
+ check_server_constraints
476
+ errors.empty?
477
+ end
478
+
479
+ # Inverse of #valid?
480
+ def invalid?(*args)
481
+ !valid?(*args)
482
+ end
483
+
484
+ def check_server_constraints
485
+ if changes.has_key?('objectClass')
486
+ (attributes.keys - may - must) | must | changes.keys
487
+ else
488
+ changes.keys
489
+ end.each do |k|
490
+ set = read_attribute(k)
491
+ set.errors.each do |message|
492
+ errors.add(k, message)
493
+ end
494
+ end
495
+ end
496
+ private :check_server_constraints
497
+
498
+ # For new objects, does an LDAP add. For existing objects, does an LDAP
499
+ # modify. This only sends the modified attributes to the server. If a
500
+ # server constraint was violated, populates #errors and returns false.
501
+ def save
502
+ return false unless valid?
503
+ if persisted?
504
+ namespace.modify(dn, changes)
505
+ else
506
+ namespace.add(dn, changes)
507
+ end
508
+ @original_attributes = (@original_attributes||{}).merge(@attributes)
509
+ @attributes = {}
510
+ true
511
+ end
512
+
513
+ # Like #save, but raise an exception if the entry could not be saved.
514
+ def save!
515
+ save ? self : raise(EntryNotSaved)
516
+ end
517
+
518
+ # Assign the given attribute hash, then #save.
519
+ def update_attributes(hash)
520
+ merge_attributes(hash)
521
+ save
522
+ end
523
+
524
+ # Like #update_attributes but raise on failure.
525
+ def update_attributes!(hash)
526
+ merge_attributes(hash)
527
+ save!
528
+ end
529
+
530
+ # Refetches the attributes from the server.
531
+ def reload
532
+ new = search(:scope => :base, :limit => true)
533
+ @original_attributes = new.instance_variable_get(:@original_attributes)
534
+ @attributes = new.instance_variable_get(:@attributes)
535
+ @dn = Ldaptic::DN(new.dn, self)
536
+ @children = {}
537
+ self
538
+ end
539
+
540
+ # Deletes the object from the server. If #save is invoked afterwards, the
541
+ # entry will be recreated.
542
+ def delete
543
+ namespace.delete(dn)
544
+ @attributes = (@original_attributes||{}).merge(@attributes)
545
+ @original_attributes = nil
546
+ self
547
+ end
548
+
549
+ # Alias for #delete.
550
+ def destroy
551
+ delete
552
+ end
553
+
554
+ def rename(new_rdn, delete_old = nil)
555
+ old_rdn = rdn
556
+ if new_rdn.kind_of?(Ldaptic::DN)
557
+ new_root = new_rdn.parent
558
+ new_rdn = new_rdn.rdn
559
+ else
560
+ new_rdn = Ldaptic::RDN(new_rdn)
561
+ new_root = nil
562
+ end
563
+ if delete_old.nil?
564
+ delete_old = (new_rdn == old_rdn)
565
+ end
566
+ namespace.rename(dn, new_rdn.to_str, delete_old, *[new_root].compact)
567
+ if delete_old
568
+ old_rdn.each do |k, v|
569
+ [@attributes, @original_attributes].each do |hash|
570
+ hash.delete_if {|k2, v2| k.to_s.downcase == k2.to_s.downcase && v.to_s.downcase == v2.to_s.downcase }
571
+ end
572
+ end
573
+ end
574
+ old_dn = Ldaptic::DN(@dn, self)
575
+ @dn = nil
576
+ if new_root
577
+ self.dn = new_root / new_rdn
578
+ else
579
+ self.dn = old_dn.parent / new_rdn
580
+ end
581
+ write_attributes_from_rdn(rdn, @original_attributes)
582
+ if @parent
583
+ children = @parent.instance_variable_get(:@children)
584
+ if child = children.delete(old_rdn)
585
+ children[new_rdn] = child if child == self
586
+ end
587
+ end
588
+ self
589
+ end
590
+
591
+ protected
592
+
593
+ def dn=(value)
594
+ if @dn
595
+ Ldaptic::Errors.raise(Ldaptic::Error.new("can't reassign DN"))
596
+ end
597
+ @dn = ::Ldaptic::DN(value, self)
598
+ write_attributes_from_rdn(rdn)
599
+ end
600
+
601
+ private
602
+
603
+ def after_build
604
+ end
605
+ def after_load
606
+ end
607
+
608
+ def common_initializations
609
+ @children ||= {}
610
+ end
611
+
612
+ def write_attributes_from_rdn(rdn, attributes = @attributes)
613
+ Ldaptic::RDN(rdn).each do |k, v|
614
+ attributes[k.to_s.downcase] ||= []
615
+ attributes[k.to_s.downcase] |= [v]
616
+ end
617
+ end
618
+
619
+ def cached_child(rdn = nil)
620
+ return self if rdn.nil? || rdn.empty?
621
+ rdn = Ldaptic::RDN(rdn)
622
+ return @children[rdn] if @children.has_key?(rdn)
623
+ child = search(:base => rdn, :scope => :base, :limit => true)
624
+ child.instance_variable_set(:@parent, self)
625
+ @children[rdn] = child
626
+ rescue Ldaptic::Errors::NoSuchObject
627
+ end
628
+
629
+ def assign_child(rdn, child)
630
+ unless child.respond_to?(:dn)
631
+ Ldaptic::Errors.raise(TypeError.new("#{child.class} cannot be a child"))
632
+ end
633
+ if child.dn
634
+ Ldaptic::Errors.raise(Ldaptic::Error.new("#{child.class} already has a DN of #{child.dn}"))
635
+ end
636
+ rdn = Ldaptic::RDN(rdn)
637
+ if cached_child(rdn)
638
+ Ldaptic::Errors.raise(Ldaptic::Error.new("child #{[rdn, dn].join(",")} already exists"))
639
+ end
640
+ @children[rdn] = child
641
+ child.dn = Ldaptic::DN(dn/rdn, child)
642
+ child.instance_variable_set(:@parent, self)
643
+ end
644
+
645
+ end
646
+ end