adtools 0.0.1pre

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.
@@ -0,0 +1,4 @@
1
+ html
2
+ pkg
3
+ .DS_STORE
4
+ spec/test_values.yml
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/License ADDED
@@ -0,0 +1,2 @@
1
+ Adtools is based on active_directory by Adam T Kerr
2
+ http://github.com/ajrkerr/active_directory
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,26 @@
1
+ = Adtools
2
+
3
+ Ruby bindings for Microsofts Active Directory (LDAP)
4
+
5
+ == Install
6
+
7
+ For use in scripts:
8
+
9
+ $ gem install adtools
10
+
11
+ For rails / anything using bundler add this to your Gemfile:
12
+
13
+ gem 'adtools'
14
+
15
+ == Usage
16
+
17
+ First off you need to configure Adtools, to do this call .configure:
18
+
19
+ Adtools.configure do |c|
20
+ c.domain = "example.com"
21
+ c.query_user = "Administrator"
22
+ c.query_password = "P4$$w0rd"
23
+ end
24
+
25
+ You can also specify a port, server and base string, but Adtools will assume these for you and _you may not need them_
26
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'adtools/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "adtools"
7
+ s.version = Adtools::Version
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Adam \"Arcath\" Laycock"]
10
+ s.email = ["gems@arcath.net"]
11
+ s.homepage = "http://adtools.arcath.net"
12
+ s.summary = "Ruby bindings for Microsofts Active Directory (LDAP), a fork of ActiveDirectory"
13
+
14
+ s.add_development_dependency "rspec"
15
+ s.add_dependency "net-ldap"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,112 @@
1
+ #-- license
2
+ #
3
+ # Based on original code by Justin Mecham and James Hunt
4
+ # at http://rubyforge.org/projects/activedirectory
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ #++ license
20
+
21
+ require 'net/ldap'
22
+
23
+ require 'adtools/config'
24
+
25
+ require 'adtools/base.rb'
26
+ require 'adtools/container.rb'
27
+ require 'adtools/member.rb'
28
+
29
+ require 'adtools/user.rb'
30
+ require 'adtools/group.rb'
31
+ require 'adtools/computer.rb'
32
+ require 'adtools/ou'
33
+
34
+ require 'adtools/field_type/password.rb'
35
+ require 'adtools/field_type/binary.rb'
36
+ require 'adtools/field_type/date.rb'
37
+ require 'adtools/field_type/timestamp.rb'
38
+ require 'adtools/field_type/dn_array.rb'
39
+ require 'adtools/field_type/user_dn_array.rb'
40
+ require 'adtools/field_type/group_dn_array.rb'
41
+ require 'adtools/field_type/member_dn_array.rb'
42
+
43
+ module Adtools
44
+
45
+ def self.configure
46
+ @config = Config.new
47
+ yield(@config)
48
+ Adtools::Base.setup(@config.settings_hash)
49
+ end
50
+
51
+ def self.config
52
+ @config
53
+ end
54
+ #Special Fields
55
+ def self.special_fields
56
+ @@special_fields
57
+ end
58
+
59
+ def self.special_fields= sp_fields
60
+ @@special_fields = sp_fields
61
+ end
62
+
63
+ @@special_fields = {
64
+
65
+ #All objects in the AD
66
+ :Base => {
67
+ :objectguid => :Binary,
68
+ :whencreated => :Date,
69
+ :whenchanged => :Date,
70
+ :memberof => :DnArray,
71
+ },
72
+
73
+ #User objects
74
+ :User => {
75
+ :objectguid => :Binary,
76
+ :whencreated => :Date,
77
+ :whenchanged => :Date,
78
+ :objectsid => :Binary,
79
+ :msexchmailboxguid => :Binary,
80
+ :msexchmailboxsecuritydescriptor => :Binary,
81
+ :lastlogontimestamp => :Timestamp,
82
+ :pwdlastset => :Timestamp,
83
+ :accountexpires => :Timestamp,
84
+ :memberof => :MemberDnArray,
85
+ },
86
+
87
+ #Group objects
88
+ :Group => {
89
+ :objectguid => :Binary,
90
+ :whencreated => :Date,
91
+ :whenchanged => :Date,
92
+ :objectsid => :Binary,
93
+ :memberof => :GroupDnArray,
94
+ :member => :MemberDnArray,
95
+ },
96
+
97
+ #OU objects
98
+ #:Ou => {
99
+ # :objectguid => :Binary,
100
+ #},
101
+
102
+ #Computer Objects
103
+ :Computer => {
104
+ :objectguid => :Binary,
105
+ :whencreated => :Date,
106
+ :whenchanged => :Date,
107
+ :objectsid => :Binary,
108
+ :memberof => :GroupDnArray,
109
+ :member => :MemberDnArray,
110
+ }
111
+ }
112
+ end
@@ -0,0 +1,584 @@
1
+ #-- license
2
+ #
3
+ # Based on original code by Justin Mecham and James Hunt
4
+ # at http://rubyforge.org/projects/activedirectory
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ #++ license
20
+
21
+ module Adtools
22
+ #
23
+ # Base class for all Ruby/ActiveDirectory Entry Objects (like User and Group)
24
+ #
25
+ class Base
26
+ #
27
+ # A Net::LDAP::Filter object that doesn't do any filtering
28
+ # (outside of check that the CN attribute is present. This
29
+ # is used internally for specifying a 'no filter' condition
30
+ # for methods that require a filter object.
31
+ #
32
+ NIL_FILTER = Net::LDAP::Filter.pres('cn')
33
+
34
+ @@ldap = nil
35
+ @@ldap_connected = false
36
+ @@caching = false
37
+ @@cache = {}
38
+
39
+ #
40
+ # Configures the connection for the Ruby/ActiveDirectory library.
41
+ #
42
+ # For example:
43
+ #
44
+ # ActiveDirectory::Base.setup(
45
+ # :host => 'domain_controller1.example.org',
46
+ # :port => 389,
47
+ # :base => 'dc=example,dc=org',
48
+ # :auth => {
49
+ # :username => 'querying_user@example.org',
50
+ # :password => 'querying_users_password'
51
+ # }
52
+ # )
53
+ #
54
+ # This will configure Ruby/ActiveDirectory to connect to the domain
55
+ # controller at domain_controller1.example.org, using port 389. The
56
+ # domain's base LDAP dn is expected to be 'dc=example,dc=org', and
57
+ # Ruby/ActiveDirectory will try to bind as the
58
+ # querying_user@example.org user, using the supplied password.
59
+ #
60
+ # Currently, there can be only one active connection per
61
+ # execution context.
62
+ #
63
+ # For more advanced options, refer to the Net::LDAP.new
64
+ # documentation.
65
+ #
66
+ def self.setup(settings)
67
+ @@settings = settings
68
+ @@ldap = Net::LDAP.new(settings)
69
+ end
70
+
71
+ def self.error
72
+ "#{@@ldap.get_operation_result.code}: #{@@ldap.get_operation_result.message}"
73
+ end
74
+
75
+ ##
76
+ # Return the last errorcode that ldap generated
77
+ def self.error_code
78
+ @@ldap.get_operation_result.code
79
+ end
80
+
81
+ ##
82
+ # Check to see if the last query produced an error
83
+ # Note: Invalid username/password combinations will not
84
+ # produce errors
85
+ def self.error?
86
+ @@ldap.nil? ? false : @@ldap.get_operation_result.code != 0
87
+ end
88
+
89
+ ##
90
+ # Check to see if we are connected to the LDAP server
91
+ # This method will try to connect, if we haven't already
92
+ def self.connected?
93
+ begin
94
+ @@ldap_connected ||= @@ldap.bind unless @@ldap.nil?
95
+ @@ldap_connected
96
+ rescue Net::LDAP::LdapError => e
97
+ false
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Check to see if result caching is enabled
103
+ def self.cache?
104
+ @@caching
105
+ end
106
+
107
+ ##
108
+ # Clears the cache
109
+ def self.clear_cache
110
+ @@cache = {}
111
+ end
112
+
113
+ ##
114
+ # Enable caching for queries against the DN only
115
+ # This is to prevent membership lookups from hitting the
116
+ # AD unnecessarilly
117
+ def self.enable_cache
118
+ @@caching = true
119
+ end
120
+
121
+ ##
122
+ # Disable caching
123
+ def self.disable_cache
124
+ @@caching = false
125
+ end
126
+
127
+ def self.filter # :nodoc:
128
+ NIL_FILTER
129
+ end
130
+
131
+ def self.required_attributes # :nodoc:
132
+ {}
133
+ end
134
+
135
+ #
136
+ # Check to see if any entries matching the passed criteria exists.
137
+ #
138
+ # Filters should be passed as a hash of
139
+ # attribute_name => expected_value, like:
140
+ #
141
+ # User.exists?(
142
+ # :sn => 'Hunt',
143
+ # :givenName => 'James'
144
+ # )
145
+ #
146
+ # which will return true if one or more User entries have an
147
+ # sn (surname) of exactly 'Hunt' and a givenName (first name)
148
+ # of exactly 'James'.
149
+ #
150
+ # Partial attribute matches are available. For instance,
151
+ #
152
+ # Group.exists?(
153
+ # :description => 'OldGroup_*'
154
+ # )
155
+ #
156
+ # would return true if there are any Group objects in
157
+ # Active Directory whose descriptions start with OldGroup_,
158
+ # like OldGroup_Reporting, or OldGroup_Admins.
159
+ #
160
+ # Note that the * wildcard matches zero or more characters,
161
+ # so the above query would also return true if a group named
162
+ # 'OldGroup_' exists.
163
+ #
164
+ def self.exists?(filter_as_hash)
165
+ criteria = make_filter_from_hash(filter_as_hash) & filter
166
+ (@@ldap.search(:filter => criteria).size > 0)
167
+ end
168
+
169
+ #
170
+ # Whether or not the entry has local changes that have not yet been
171
+ # replicated to the Active Directory server via a call to Base#save
172
+ #
173
+ def changed?
174
+ !@attributes.empty?
175
+ end
176
+
177
+ ##
178
+ # Makes a single filter from a given key and value
179
+ # It will try to encode an array if there is a process for it
180
+ # Otherwise, it will treat it as an or condition
181
+ def self.make_filter(key, value)
182
+ #Join arrays using OR condition
183
+ if value.is_a? Array
184
+ filter = ~NIL_FILTER
185
+
186
+ value.each do |v|
187
+ filter |= Net::LDAP::Filter.eq(key, encode_field(key, v).to_s)
188
+ end
189
+ else
190
+ filter = Net::LDAP::Filter.eq(key, encode_field(key, value).to_s)
191
+ end
192
+
193
+ return filter
194
+ end
195
+
196
+ def self.make_filter_from_hash(hash) # :nodoc:
197
+ return NIL_FILTER if hash.nil? || hash.empty?
198
+
199
+ filter = NIL_FILTER
200
+
201
+ hash.each do |key, value|
202
+ filter &= make_filter(key, value)
203
+ end
204
+
205
+ return filter
206
+ end
207
+
208
+ #
209
+ # Performs a search on the Active Directory store, with similar
210
+ # syntax to the Rails ActiveRecord#find method.
211
+ #
212
+ # The first argument passed should be
213
+ # either :first or :all, to indicate that we want only one
214
+ # (:first) or all (:all) results back from the resultant set.
215
+ #
216
+ # The second argument should be a hash of attribute_name =>
217
+ # expected_value pairs.
218
+ #
219
+ # User.find(:all, :sn => 'Hunt')
220
+ #
221
+ # would find all of the User objects in Active Directory that
222
+ # have a surname of exactly 'Hunt'. As with the Base.exists?
223
+ # method, partial searches are allowed.
224
+ #
225
+ # This method always returns an array if the caller specifies
226
+ # :all for the search e (first argument). If no results
227
+ # are found, the array will be empty.
228
+ #
229
+ # If you call find(:first, ...), you will either get an object
230
+ # (a User or a Group) back, or nil, if there were no entries
231
+ # matching your filter.
232
+ #
233
+ def self.find(*args)
234
+ return false unless connected?
235
+
236
+ options = {
237
+ :filter => (args[1].nil?) ? NIL_FILTER : args[1],
238
+ :in => ''
239
+ }
240
+
241
+ cached_results = find_cached_results(args[1])
242
+ return cached_results if cached_results or cached_results.nil?
243
+
244
+ options[:in] = [ options[:in].to_s, @@settings[:base] ].delete_if { |part| part.empty? }.join(",")
245
+
246
+ if options[:filter].is_a? Hash
247
+ options[:filter] = make_filter_from_hash(options[:filter])
248
+ end
249
+
250
+ options[:filter] = options[:filter] & filter unless self.filter == NIL_FILTER
251
+
252
+ if (args.first == :all)
253
+ find_all(options)
254
+ elsif (args.first == :first)
255
+ find_first(options)
256
+ else
257
+ raise ArgumentError, 'Invalid specifier (not :all, and not :first) passed to find()'
258
+ end
259
+ end
260
+
261
+ ##
262
+ # Searches the cache and returns the result
263
+ # Returns false on failure, nil on wrong object type
264
+ #
265
+ def self.find_cached_results(filters)
266
+ return false unless cache?
267
+
268
+ #Check to see if we're only looking for :distinguishedname
269
+ return false unless filters.is_a? Hash and filters.keys == [:distinguishedname]
270
+
271
+ #Find keys we're looking up
272
+ dns = filters[:distinguishedname]
273
+
274
+ if dns.kind_of? Array
275
+ result = []
276
+
277
+ dns.each do |dn|
278
+ entry = @@cache[dn]
279
+
280
+ #If the object isn't in the cache just run the query
281
+ return false if entry.nil?
282
+
283
+ #Only permit objects of the type we're looking for
284
+ result << entry if entry.kind_of? self
285
+ end
286
+
287
+ return result
288
+ else
289
+ return false unless @@cache.key? dns
290
+ return @@cache[dns] if @@cache[dns].is_a? self
291
+ end
292
+ end
293
+
294
+ def self.find_all(options)
295
+ results = []
296
+ ldap_objs = @@ldap.search(:filter => options[:filter], :base => options[:in]) || []
297
+
298
+ ldap_objs.each do |entry|
299
+ ad_obj = new(entry)
300
+ @@cache[entry.dn] = ad_obj unless ad_obj.instance_of? Base
301
+ results << ad_obj
302
+ end
303
+
304
+ results
305
+ end
306
+
307
+ def self.find_first(options)
308
+ ldap_result = @@ldap.search(:filter => options[:filter], :base => options[:in])
309
+ return nil if ldap_result.empty?
310
+
311
+ ad_obj = new(ldap_result[0])
312
+ @@cache[ad_obj.dn] = ad_obj unless ad_obj.instance_of? Base
313
+ return ad_obj
314
+ end
315
+
316
+ def self.method_missing(name, *args) # :nodoc:
317
+ name = name.to_s
318
+ if (name[0,5] == 'find_')
319
+ find_spec, attribute_spec = parse_finder_spec(name)
320
+ raise ArgumentError, "find: Wrong number of arguments (#{args.size} for #{attribute_spec.size})" unless args.size == attribute_spec.size
321
+ filters = {}
322
+ [attribute_spec,args].transpose.each { |pr| filters[pr[0]] = pr[1] }
323
+ find(find_spec, :filter => filters)
324
+ else
325
+ super name.to_sym, args
326
+ end
327
+ end
328
+
329
+ def self.parse_finder_spec(method_name) # :nodoc:
330
+ # FIXME: This is a prime candidate for a
331
+ # first-class object, FinderSpec
332
+
333
+ method_name = method_name.gsub(/^find_/,'').gsub(/^by_/,'first_by_')
334
+ find_spec, attribute_spec = *(method_name.split('_by_'))
335
+ find_spec = find_spec.to_sym
336
+ attribute_spec = attribute_spec.split('_and_').collect { |s| s.to_sym }
337
+
338
+ return find_spec, attribute_spec
339
+ end
340
+
341
+ def ==(other) # :nodoc:
342
+ return false if other.nil?
343
+ other[:objectguid] == get_attr(:objectguid)
344
+ end
345
+
346
+ #
347
+ # Returns true if this entry does not yet exist in Active Directory.
348
+ #
349
+ def new_record?
350
+ @entry.nil?
351
+ end
352
+
353
+ #
354
+ # Refreshes the attributes for the entry with updated data from the
355
+ # domain controller.
356
+ #
357
+ def reload
358
+ return false if new_record?
359
+
360
+ @entry = @@ldap.search(:filter => Net::LDAP::Filter.eq('distinguishedName',distinguishedName))[0]
361
+ return !@entry.nil?
362
+ end
363
+
364
+ #
365
+ # Updates a single attribute (name) with one or more values
366
+ # (value), by immediately contacting the Active Directory
367
+ # server and initiating the update remotely.
368
+ #
369
+ # Entries are always reloaded (via Base.reload) after calling
370
+ # this method.
371
+ #
372
+ def update_attribute(name, value)
373
+ update_attributes(name.to_s => value)
374
+ end
375
+
376
+ #
377
+ # Updates multiple attributes, like ActiveRecord#update_attributes.
378
+ # The updates are immediately sent to the server for processing,
379
+ # and the entry is reloaded after the update (if all went well).
380
+ #
381
+ def update_attributes(attributes_to_update)
382
+ return true if attributes_to_update.empty?
383
+
384
+ operations = []
385
+ attributes_to_update.each do |attribute, values|
386
+ if values.nil? || values.empty?
387
+ operations << [ :delete, attribute, nil ]
388
+ else
389
+ values = [values] unless values.is_a? Array
390
+ values = values.collect { |v| v.to_s }
391
+
392
+ current_value = begin
393
+ @entry[attribute]
394
+ rescue NoMethodError
395
+ nil
396
+ end
397
+
398
+ operations << [ (current_value.nil? ? :add : :replace), attribute, values ]
399
+ end
400
+ end
401
+
402
+ @@ldap.modify(
403
+ :dn => distinguishedName,
404
+ :operations => operations
405
+ ) && reload
406
+ end
407
+
408
+ #
409
+ # Create a new entry in the Active Record store.
410
+ #
411
+ # dn is the Distinguished Name for the new entry. This must be
412
+ # a unique identifier, and can be passed as either a Container
413
+ # or a plain string.
414
+ #
415
+ # attributes is a symbol-keyed hash of attribute_name => value
416
+ # pairs.
417
+ #
418
+ def self.create(dn,attributes)
419
+ return nil if dn.nil? || attributes.nil?
420
+ begin
421
+ attributes.merge!(required_attributes)
422
+ if @@ldap.add(:dn => dn.to_s, :attributes => attributes)
423
+ return find_by_distinguishedName(dn.to_s)
424
+ else
425
+ return nil
426
+ end
427
+ rescue
428
+ return nil
429
+ end
430
+ end
431
+
432
+ #
433
+ # Deletes the entry from the Active Record store and returns true
434
+ # if the operation was successfully.
435
+ #
436
+ def destroy
437
+ return false if new_record?
438
+
439
+ if @@ldap.delete(:dn => distinguishedName)
440
+ @entry = nil
441
+ @attributes = {}
442
+ return true
443
+ else
444
+ return false
445
+ end
446
+ end
447
+
448
+ #
449
+ # Saves any pending changes to the entry by updating the remote
450
+ # entry.
451
+ #
452
+ def save
453
+ if update_attributes(@attributes)
454
+ @attributes = {}
455
+ return true
456
+ else
457
+ return false
458
+ end
459
+ end
460
+
461
+ #
462
+ # This method may one day provide the ability to move entries from
463
+ # container to container. Currently, it does nothing, as we are
464
+ # waiting on the Net::LDAP folks to either document the
465
+ # Net::LDAP#modrdn method, or provide a similar method for
466
+ # moving / renaming LDAP entries.
467
+ #
468
+ def move(new_rdn)
469
+ return false if new_record?
470
+ puts "Moving #{distinguishedName} to RDN: #{new_rdn}"
471
+
472
+ settings = @@settings.dup
473
+ settings[:port] = 636
474
+ settings[:encryption] = { :method => :simple_tls }
475
+
476
+ ldap = Net::LDAP.new(settings)
477
+
478
+ if ldap.rename(
479
+ :olddn => distinguishedName,
480
+ :newrdn => new_rdn,
481
+ :delete_attributes => false
482
+ )
483
+ return true
484
+ else
485
+ puts Base.error
486
+ return false
487
+ end
488
+ end
489
+
490
+ # FIXME: Need to document the Base::new
491
+ def initialize(attributes = {}) # :nodoc:
492
+ if attributes.is_a? Net::LDAP::Entry
493
+ @entry = attributes
494
+ @attributes = {}
495
+ else
496
+ @entry = nil
497
+ @attributes = attributes
498
+ end
499
+ end
500
+
501
+ ##
502
+ # Pull the class we're in
503
+ # This isn't quite right, as extending the object does funny things to how we
504
+ # lookup objects
505
+ def self.class_name
506
+ @klass ||= (self.name.include?('::') ? self.name[/.*::(.*)/, 1] : self.name)
507
+ end
508
+
509
+ ##
510
+ # Grabs the field type depending on the class it is called from
511
+ # Takes the field name as a parameter
512
+ def self.get_field_type(name)
513
+ #Extract class name
514
+ throw "Invalid field name" if name.nil?
515
+ type = ::Adtools.special_fields[class_name.to_sym][name.to_s.downcase.to_sym]
516
+ type.to_s unless type.nil?
517
+ end
518
+
519
+ @types = {}
520
+
521
+ def self.decode_field(name, value) # :nodoc:
522
+ type = get_field_type name
523
+ if !type.nil? and ::ActiveDirectory::FieldType::const_defined? type
524
+ return ::ActiveDirectory::FieldType::const_get(type).decode(value)
525
+ end
526
+ return value
527
+ end
528
+
529
+ def self.encode_field(name, value) # :nodoc:
530
+ type = get_field_type name
531
+ if !type.nil? and ::ActiveDirectory::FieldType::const_defined? type
532
+ return ::ActiveDirectory::FieldType::const_get(type).encode(value)
533
+ end
534
+ return value
535
+ end
536
+
537
+ def valid_attribute? name
538
+ @attributes.has_key?(name) || @entry.attribute_names.include?(name)
539
+ end
540
+
541
+ def get_attr(name)
542
+ name = name.to_s.downcase
543
+
544
+ return decode_field(name, @attributes[name.to_sym]) if @attributes.has_key?(name.to_sym)
545
+
546
+ if @entry.attribute_names.include? name.to_sym
547
+ value = @entry[name.to_sym]
548
+ value = value.first if value.kind_of?(Array) && value.size == 1
549
+ value = value.to_s if value.nil? || value.size == 1
550
+ value = nil.to_s if value.empty?
551
+ return self.class.decode_field(name, value)
552
+ end
553
+ end
554
+
555
+ def set_attr(name, value)
556
+ @attributes[name.to_sym] = encode_field(name, value)
557
+ end
558
+
559
+ ##
560
+ # Reads the array of values for the provided attribute. The attribute name
561
+ # is canonicalized prior to reading. Returns an empty array if the
562
+ # attribute does not exist.
563
+ alias [] get_attr
564
+ alias []= set_attr
565
+
566
+ ##
567
+ # Weird fluke with flattening, probably because of above attribute
568
+ def to_ary
569
+ end
570
+
571
+
572
+ def method_missing(name, args = []) # :nodoc:
573
+ name = name.to_s.downcase
574
+
575
+ return set_attr(name.chop, args) if name[-1] == '='
576
+
577
+ if valid_attribute? name.to_sym
578
+ get_attr(name)
579
+ else
580
+ super
581
+ end
582
+ end
583
+ end
584
+ end