activedirectory 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,37 @@
1
- $:.unshift(File.dirname(__FILE__) + "/active_directory/vendor/")
1
+ #-- license
2
+ #
3
+ # This file is part of the Ruby Active Directory Project
4
+ # on the web at http://rubyforge.org/projects/activedirectory
5
+ #
6
+ # Copyright (c) 2008, James Hunt <filefrog@gmail.com>
7
+ # based on original code by Justin Mecham
8
+ #
9
+ # This program is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # This program is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ #++ license
2
23
 
3
- # External Dependencies
4
- require 'ldap'
5
- require 'logger'
24
+ require 'net/ldap'
6
25
 
7
- # Extensions to Ruby
8
- Dir[File.dirname(__FILE__) + "/active_directory/ext/*.rb"].each { |file|
9
- require file
10
- }
26
+ require 'active_directory/base.rb'
27
+ require 'active_directory/container.rb'
28
+ require 'active_directory/member.rb'
11
29
 
12
- # Active Directory Classes
13
- require 'active_directory/base'
14
- require 'active_directory/user'
15
- require 'active_directory/group'
30
+ require 'active_directory/user.rb'
31
+ require 'active_directory/group.rb'
32
+ require 'active_directory/computer.rb'
33
+
34
+ require 'active_directory/password.rb'
35
+ require 'active_directory/timestamp.rb'
36
+
37
+ require 'active_directory/rails/user.rb'
@@ -1,368 +1,404 @@
1
- #--
2
- # Active Directory Module for Ruby
1
+ #-- license
3
2
  #
4
- # Copyright (c) 2005-2006 Justin Mecham
3
+ # This file is part of the Ruby Active Directory Project
4
+ # on the web at http://rubyforge.org/projects/activedirectory
5
5
  #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the "Software"), to
8
- # deal in the Software without restriction, including without limitation the
9
- # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
- # sell copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
6
+ # Copyright (c) 2008, James Hunt <filefrog@gmail.com>
7
+ # based on original code by Justin Mecham
12
8
  #
13
- # The above copyright notice and this permission notice shall be included in
14
- # all copies or substantial portions of the Software.
9
+ # This program is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
15
13
  #
16
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
- # IN THE SOFTWARE.
23
- #++
14
+ # This program is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ #++ license
24
23
 
25
24
  module ActiveDirectory
26
-
27
- #
28
- # The ActiveDirectory module contains the classes used for communicating with
29
- # Active Directory LDAP servers. ActiveDirectory::Base is the basis from which
30
- # all classes derive.
31
- #
32
- class Base
33
-
34
- #
35
- # Configuration
36
- #
37
- @@server_settings = {
38
- :host => "localhost",
39
- :port => 389,
40
- :username => nil,
41
- :password => nil,
42
- :domain => nil,
43
- :base_dn => nil
44
- }
45
- cattr_accessor :server_settings
46
-
47
- #
48
- # Logging
49
- #
50
- cattr_writer :logger
51
- def self.logger
52
- @@logger || Logger.new(STDOUT)
53
- end
54
-
55
- #
56
- # Returns a connection to the configured Active Directory instance. If a
57
- # connection is not already established, it will attempt to establish the
58
- # connection.
59
- #
60
- def self.connection
61
- @@connection ||= connect
62
- end
63
-
64
- #
65
- # Opens and returns a connection to the Active Directory instance. By
66
- # default, secure connections will be attempted first while gracefully
67
- # falling back to lesser secure methods.
68
- #
69
- # The order by which connections are attempted are: TLS, SSL, unencrypted.
70
- #
71
- # Calling #connection will automatically call this method if a connection
72
- # has not already been established.
73
- #
74
- def self.connect
75
-
76
- host = @@server_settings[:host]
77
- port = @@server_settings[:port] || 389
78
-
79
- # Attempt to connect using TLS
80
- begin
81
- connection = LDAP::SSLConn.new(host, port, true)
82
- connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
83
- bind(connection)
84
- logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} using TLS...") unless logger.nil?
85
- rescue
86
- logger.debug("ActiveDirectory: Failed to connect to #{@@server_settings[:host]} using TLS!") unless logger.nil?
87
- # Attempt to connect using SSL
88
- begin
89
- connection = LDAP::SSLConn.new(host, port, false)
90
- connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
91
- bind(connection)
92
- logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} over SSL...") unless logger.nil?
93
- rescue
94
- logger.debug("ActiveDirectory: Failed to connect to #{@@server_settings[:host]} over SSL!") unless logger.nil?
95
- # Attempt to connect without encryption
96
- begin
97
- connection = LDAP::Conn.new(host, port)
98
- connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
99
- bind(connection)
100
- logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} without encryption!") unless logger.nil?
101
- rescue
102
- logger.error("ActiveDirectory: Failed to connect to #{@@server_settings[:host]}!") unless logger.nil?
103
- puts "EXCEPTION: #{$!}"
104
- connection = nil
105
- raise
106
- end
107
- end
108
- end
109
-
110
- connection.set_option(LDAP::LDAP_OPT_REFERRALS, 0)
111
-
112
- connection
113
-
114
- end
115
-
116
- #
117
- # Unbinds (if bound) and closes the current connection.
118
- #
119
- def self.close
120
- begin
121
- @@connection.unbind unless @@connection.nil?
122
- rescue
123
- end
124
- @@connection = nil
125
- end
126
-
127
- #
128
- # Attempts to reconnect to the server by closing the existing connection
129
- # and reconnecting.
130
- #
131
- def self.reconnect
132
- close
133
- @@connection = connect
134
- end
135
-
136
- #
137
- # Search interface for querying for objects within the directory, such as
138
- # users and groups. Searching the directory is quite similar to a normal
139
- # LDAP search, but with a better API.
140
- #
141
- # If you call this method from User or Group, it will narrow your searches
142
- # to those specific objects by default. Calling this method on Base will
143
- # return objects of any class and provides the most flexibility.
144
- #
145
- # ==== Searching Users
146
- #
147
- # Users may be located within the directory by their distinguished name
148
- # (DN), their username (sAMAccountName), or through any other valid LDAP
149
- # filter and optional search base.
150
- #
151
- # # Load all users (including disabled) within the default Base DN.
152
- # all_users = ActiveDirectory::User.find(:all)
153
- #
154
- # # Load all disabled users within the default Base DN.
155
- # disabled_users = ActiveDirectory::User.find(:all,
156
- # :filter => "(userAccountControl=514)")
157
- #
158
- # # Load all users who are in the Managers organizational unit whose
159
- # # accounts are not disabled.
160
- # managers = ActiveDirectory::User.find(:all,
161
- # :base => "OU=Managers,DC=example,DC=com",
162
- # :filter => "(userAccountControl=512)")
163
- #
164
- # # Load the user "John Doe" by his sAMAccountName.
165
- # user = ActiveDirectory::User.find("jdoe")
166
- #
167
- # # Load the user "John Doe" by his distinguished name (DN).
168
- # user = ActiveDirectory::User.find("CN=John Doe,CN=Users,DC=example,DC=com")
169
- #
170
- # ==== Searching Groups
171
- #
172
- # Groups may be located within the diretctory by their distinguished name
173
- # (DN) or through any other valid LDAP filter and optional search base.
174
- #
175
- # # Load all groups within the default Base DN.
176
- # all_groups = ActiveDirectory::Group.find(:all)
177
- #
178
- # # Load the "Developers" group by its distinguished name (DN).
179
- # developers = ActiveDirectory::Group.find("CN=Developers,DC=example,DC=com")
180
- #
181
- # ==== More Advanced Examples
182
- #
183
- # By calling ActiveDirectory::Base#find you can query objects across
184
- # classes, allowing you to pull in both Groups and Users that match your
185
- # criteria.
186
- #
187
- # # Load all Contacts
188
- # contacts = ActiveDirectory::Base.find(:all,
189
- # :filter => "(&(objectClass=User)(objectCategory=Contact))")
190
- #
191
- def self.find(*args)
192
-
193
- options = extract_options_from_args!(args)
194
- attributes = [ "distinguishedName", "objectClass" ]
195
-
196
- # Determine the appropriate search filter
197
- if self.name == "ActiveDirectory::Base"
198
- if options[:filter]
199
- filter = options[:filter]
200
- else
201
- filter = "(|(&(objectCategory=Person)(objectClass=User))(objectClass=Group))"
202
- end
203
- else
204
- subklass = class_name_of_active_directory_descendent(self)
205
- if subklass == "ActiveDirectory::User"
206
- filter = "(&(objectCategory=Person)(objectClass=User)#{options[:filter]})"
207
- elsif subklass == "ActiveDirectory::Group"
208
- filter = "(&(objectClass=Group)#{options[:filter]})"
209
- end
210
- end
211
-
212
- # Determine the appropriate search base
213
- base_dn = options[:base] ? options[:base] : @@server_settings[:base_dn]
214
-
215
- # Determine the appropriate scope
216
- scope = options[:scope] ? options[:scope] : LDAP::LDAP_SCOPE_SUBTREE
217
-
218
- # Load all matching objects
219
- if args.first == :all
220
-
221
- logger.debug "Searching Active Directory" \
222
- " - Filter: #{filter}" \
223
- " - Search Base: #{base_dn} " \
224
- " - Scope: #{scope}"
225
-
226
- # Perform search
227
- entries = self.search(base_dn, scope, filter, attributes)
228
-
229
- result = Array.new
230
- unless entries.nil?
231
- for entry in entries
232
- if entry['objectClass'].include? "person"
233
- result << User.new(entry['distinguishedName'][0])
234
- elsif entry['objectClass'].include? "group"
235
- result << Group.new(entry['distinguishedName'][0])
236
- end
237
- end
238
- end
239
- result
240
-
241
- else
242
-
243
- # Load a single matching object by either a common name or a
244
- # sAMAccountName
245
- if args.first =~ /(CN|cn)=/
246
- base_dn = args.first
247
- else
248
- filter = "(&(objectCategory=Person)(objectClass=User)(samAccountName=#{args.first})#{options[:filter]})"
249
- end
250
-
251
- logger.debug "Searching Active Directory" \
252
- " - Filter: #{filter}" \
253
- " - Search Base: #{base_dn} " \
254
- " - Scope: #{scope}"
255
-
256
- begin
257
- entry = self.search(base_dn, scope, filter, attributes)
258
- rescue
259
- if $!.message == "No such object"
260
- raise UnknownUserError
261
- else
262
- raise
263
- end
264
- end
265
-
266
- entry = entry[0]
267
- if entry['objectClass'].include? "person"
268
- User.new(entry['distinguishedName'][0])
269
- elsif entry['objectClass'].include? "group"
270
- Group.new(entry['distinguishedName'][0])
271
- end
272
-
273
- end
274
-
275
- end
276
-
277
- protected
278
-
279
- #
280
- # Implement our own interface to LDAP::Conn#search so that we can attempt
281
- # graceful reconnections when the connection becomes stale or otherwise
282
- # terminates.
283
- #
284
- def self.search(base_dn, scope, filter, attrs = nil) #:nodoc:
285
- retries = 0
286
- entries = nil
287
- begin
288
- connection.search(base_dn, scope, filter, attrs) { |entry|
289
- entries = Array.new if entries.nil?
290
- entries << entry.to_hash
291
- }
292
- rescue
293
- logger.debug("ActiveDirectory: Attempting to re-establish connection to Active Directory server... (Exception: \"#{$!}\" – Retry ##{retries} – #{$!.class})}") unless logger.nil?
294
- begin
295
- reconnect
296
- rescue
297
- (retries += 1) < 3 ? retry : raise
298
- end
299
- retry
300
- end
301
- entries
302
- end
303
-
304
- private
305
-
306
- #
307
- # Attempts to bind to the Active Directory instance. If a bind attempt
308
- # fails by simple authentication an attempt at anonymous binding will be
309
- # made.
310
- #
311
- def self.bind(connection)
312
- # Attempt to bind with a username and password
313
- begin
314
- connection.bind("#{@@server_settings[:username]}@#{@@server_settings[:domain]}", @@server_settings[:password])
315
- logger.info("ActiveDirectory: Bound to Active Directory server as #{@@server_settings[:username]}.") unless logger.nil?
316
- rescue
317
- logger.debug("ActiveDirectory: Failed to bind to Active Directory server as \"#{@@server_settings[:username]}\"!") unless logger.nil?
318
- # Attempt to bind anonymously
319
- begin
320
- connection.bind()
321
- logger.info("ActiveDirectory: Bound anonymously to Active Directory server.") unless logger.nil?
322
- rescue
323
- logger.debug("ActiveDirectory: Failed to bind anonymously to Active Directory server!") unless logger.nil?
324
- raise
325
- end
326
- end
327
- end
328
-
329
- def self.extract_options_from_args!(args)
330
- options = args.last.is_a?(Hash) ? args.pop : {}
331
- validate_find_options(options)
332
- options
333
- end
334
-
335
- def self.validate_find_options(options)
336
- options.assert_valid_keys [ :base, :filter, :scope ]
337
- end
338
-
339
- def self.class_of_active_directory_descendent(klass)
340
- if klass.superclass == Base
341
- klass
342
- elsif klass.superclass.nil?
343
- raise ActiveDirectoryError, "#{name} doesn't belong in a class \
344
- hierarchy descending from ActiveDirectory"
345
- else
346
- self.class_of_active_directory_descendent(klass.superclass)
347
- end
348
- end
349
-
350
- def self.class_name_of_active_directory_descendent(klass)
351
- self.class_of_active_directory_descendent(klass).name
352
- end
353
-
354
- end
355
-
356
- class ActiveDirectoryError < StandardError #:nodoc:
357
- end
358
-
359
- class UnknownUserError < StandardError #:nodoc:
360
- end
361
-
362
- class UnknownGroupError < StandardError #:nodoc:
363
- end
364
-
365
- class PasswordInvalid < StandardError #:nodoc:
366
- end
367
-
368
- end
25
+ #
26
+ # Base class for all Ruby/ActiveDirectory Entry Objects (like User and Group)
27
+ #
28
+ class Base
29
+ #
30
+ # A Net::LDAP::Filter object that doesn't do any filtering
31
+ # (outside of check that the CN attribute is present. This
32
+ # is used internally for specifying a 'no filter' condition
33
+ # for methods that require a filter object.
34
+ #
35
+ NIL_FILTER = Net::LDAP::Filter.pres('cn')
36
+
37
+ @@ldap = nil
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
+ def self.filter # :nodoc:
76
+ NIL_FILTER
77
+ end
78
+
79
+ def self.required_attributes # :nodoc:
80
+ {}
81
+ end
82
+
83
+ #
84
+ # Check to see if any entries matching the passed criteria exists.
85
+ #
86
+ # Filters should be passed as a hash of
87
+ # attribute_name => expected_value, like:
88
+ #
89
+ # User.exists?(
90
+ # :sn => 'Hunt',
91
+ # :givenName => 'James'
92
+ # )
93
+ #
94
+ # which will return true if one or more User entries have an
95
+ # sn (surname) of exactly 'Hunt' and a givenName (first name)
96
+ # of exactly 'James'.
97
+ #
98
+ # Partial attribute matches are available. For instance,
99
+ #
100
+ # Group.exists?(
101
+ # :description => 'OldGroup_*'
102
+ # )
103
+ #
104
+ # would return true if there are any Group objects in
105
+ # Active Directory whose descriptions start with OldGroup_,
106
+ # like OldGroup_Reporting, or OldGroup_Admins.
107
+ #
108
+ # Note that the * wildcard matches zero or more characters,
109
+ # so the above query would also return true if a group named
110
+ # 'OldGroup_' exists.
111
+ #
112
+ def self.exists?(filter_as_hash)
113
+ criteria = make_filter_from_hash(filter_as_hash) & filter
114
+ (@@ldap.search(:filter => criteria).size > 0)
115
+ end
116
+
117
+ #
118
+ # Whether or not the entry has local changes that have not yet been
119
+ # replicated to the Active Directory server via a call to Base#save
120
+ #
121
+ def changed?
122
+ !@attributes.empty?
123
+ end
124
+
125
+ def self.make_filter_from_hash(filter_as_hash) # :nodoc:
126
+ return NIL_FILTER if filter_as_hash.nil? || filter_as_hash.empty?
127
+ keys = filter_as_hash.keys
128
+
129
+ first_key = keys.delete(keys[0])
130
+ f = Net::LDAP::Filter.eq(first_key, filter_as_hash[first_key].to_s)
131
+ keys.each do |key|
132
+ f = f & Net::LDAP::Filter.eq(key, filter_as_hash[key].to_s)
133
+ end
134
+ f
135
+ end
136
+
137
+ #
138
+ # Performs a search on the Active Directory store, with similar
139
+ # syntax to the Rails ActiveRecord#find method.
140
+ #
141
+ # The first argument passed should be
142
+ # either :first or :all, to indicate that we want only one
143
+ # (:first) or all (:all) results back from the resultant set.
144
+ #
145
+ # The second argument should be a hash of attribute_name =>
146
+ # expected_value pairs.
147
+ #
148
+ # User.find(:all, :sn => 'Hunt')
149
+ #
150
+ # would find all of the User objects in Active Directory that
151
+ # have a surname of exactly 'Hunt'. As with the Base.exists?
152
+ # method, partial searches are allowed.
153
+ #
154
+ # This method always returns an array if the caller specifies
155
+ # :all for the search type (first argument). If no results
156
+ # are found, the array will be empty.
157
+ #
158
+ # If you call find(:first, ...), you will either get an object
159
+ # (a User or a Group) back, or nil, if there were no entries
160
+ # matching your filter.
161
+ #
162
+ def self.find(*args)
163
+ options = {
164
+ :filter => NIL_FILTER,
165
+ :in => ''
166
+ }
167
+ options.merge!(args[1]) unless args[1].nil?
168
+ options[:in] = [ options[:in].to_s, @@settings[:base] ].delete_if { |part| part.empty? }.join(",")
169
+ if options[:filter].is_a? Hash
170
+ options[:filter] = make_filter_from_hash(options[:filter])
171
+ end
172
+ options[:filter] = options[:filter] & filter unless self.filter == NIL_FILTER
173
+
174
+ if (args.first == :all)
175
+ find_all(options)
176
+ elsif (args.first == :first)
177
+ find_first(options)
178
+ else
179
+ raise ArgumentError, 'Invalid specifier (not :all, and not :first) passed to find()'
180
+ end
181
+ end
182
+
183
+ def self.find_all(options)
184
+ results = []
185
+ @@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
186
+ results << new(entry)
187
+ end
188
+ results
189
+ end
190
+
191
+ def self.find_first(options)
192
+ @@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
193
+ return new(entry)
194
+ end
195
+ end
196
+
197
+ def self.method_missing(name, *args) # :nodoc:
198
+ name = name.to_s
199
+ if (name[0,5] == 'find_')
200
+ find_spec, attribute_spec = parse_finder_spec(name)
201
+ raise ArgumentError, "find: Wrong number of arguments (#{args.size} for #{attribute_spec.size})" unless args.size == attribute_spec.size
202
+ filters = {}
203
+ [attribute_spec,args].transpose.each { |pr| filters[pr[0]] = pr[1] }
204
+ find(find_spec, :filter => filters)
205
+ else
206
+ super name.to_sym, args
207
+ end
208
+ end
209
+
210
+ def self.parse_finder_spec(method_name) # :nodoc:
211
+ # FIXME: This is a prime candidate for a
212
+ # first-class object, FinderSpec
213
+
214
+ method_name = method_name.gsub(/^find_/,'').gsub(/^by_/,'first_by_')
215
+ find_spec, attribute_spec = *(method_name.split('_by_'))
216
+ find_spec = find_spec.to_sym
217
+ attribute_spec = attribute_spec.split('_and_').collect { |s| s.to_sym }
218
+
219
+ return find_spec, attribute_spec
220
+ end
221
+
222
+ def ==(other) # :nodoc:
223
+ return false if other.nil?
224
+ other.distinguishedName == distinguishedName
225
+ end
226
+
227
+ #
228
+ # Returns true if this entry does not yet exist in Active Directory.
229
+ #
230
+ def new_record?
231
+ @entry.nil?
232
+ end
233
+
234
+ #
235
+ # Refreshes the attributes for the entry with updated data from the
236
+ # domain controller.
237
+ #
238
+ def reload
239
+ return false if new_record?
240
+
241
+ @entry = @@ldap.search(:filter => Net::LDAP::Filter.eq('distinguishedName',distinguishedName))[0]
242
+ return !@entry.nil?
243
+ end
244
+
245
+ #
246
+ # Updates a single attribute (name) with one or more values
247
+ # (value), by immediately contacting the Active Directory
248
+ # server and initiating the update remotely.
249
+ #
250
+ # Entries are always reloaded (via Base.reload) after calling
251
+ # this method.
252
+ #
253
+ def update_attribute(name, value)
254
+ update_attributes(name.to_s => value)
255
+ end
256
+
257
+ #
258
+ # Updates multiple attributes, like ActiveRecord#update_attributes.
259
+ # The updates are immediately sent to the server for processing,
260
+ # and the entry is reloaded after the update (if all went well).
261
+ #
262
+ def update_attributes(attributes_to_update)
263
+ return true if attributes_to_update.empty?
264
+
265
+ operations = []
266
+ attributes_to_update.each do |attribute, values|
267
+ if values.nil? || values.empty?
268
+ operations << [ :delete, attribute, nil ]
269
+ else
270
+ values = [values] unless values.is_a? Array
271
+ values = values.collect { |v| v.to_s }
272
+
273
+ current_value = begin
274
+ @entry.send(attribute)
275
+ rescue NoMethodError
276
+ nil
277
+ end
278
+
279
+ operations << [ (current_value.nil? ? :add : :replace), attribute, values ]
280
+ end
281
+ end
282
+
283
+ @@ldap.modify(
284
+ :dn => distinguishedName,
285
+ :operations => operations
286
+ ) && reload
287
+ end
288
+
289
+ #
290
+ # Create a new entry in the Active Record store.
291
+ #
292
+ # dn is the Distinguished Name for the new entry. This must be
293
+ # a unique identifier, and can be passed as either a Container
294
+ # or a plain string.
295
+ #
296
+ # attributes is a symbol-keyed hash of attribute_name => value
297
+ # pairs.
298
+ #
299
+ def self.create(dn,attributes)
300
+ return nil if dn.nil? || attributes.nil?
301
+ begin
302
+ attributes.merge!(required_attributes)
303
+ if @@ldap.add(:dn => dn.to_s, :attributes => attributes)
304
+ return find_by_distinguishedName(dn.to_s)
305
+ else
306
+ return nil
307
+ end
308
+ rescue
309
+ return nil
310
+ end
311
+ end
312
+
313
+ #
314
+ # Deletes the entry from the Active Record store and returns true
315
+ # if the operation was successfully.
316
+ #
317
+ def destroy
318
+ return false if new_record?
319
+
320
+ if @@ldap.delete(:dn => distinguishedName)
321
+ @entry = nil
322
+ @attributes = {}
323
+ return true
324
+ else
325
+ return false
326
+ end
327
+ end
328
+
329
+ #
330
+ # Saves any pending changes to the entry by updating the remote
331
+ # entry.
332
+ #
333
+ def save
334
+ if update_attributes(@attributes)
335
+ @attributes = {}
336
+ return true
337
+ else
338
+ return false
339
+ end
340
+ end
341
+
342
+ #
343
+ # This method may one day provide the ability to move entries from
344
+ # container to container. Currently, it does nothing, as we are
345
+ # waiting on the Net::LDAP folks to either document the
346
+ # Net::LDAP#modrdn method, or provide a similar method for
347
+ # moving / renaming LDAP entries.
348
+ #
349
+ def move(new_rdn)
350
+ return false if new_record?
351
+ puts "Moving #{distinguishedName} to RDN: #{new_rdn}"
352
+
353
+ settings = @@settings.dup
354
+ settings[:port] = 636
355
+ settings[:encryption] = { :method => :simple_tls }
356
+
357
+ ldap = Net::LDAP.new(settings)
358
+
359
+ if ldap.rename(
360
+ :olddn => distinguishedName,
361
+ :newrdn => new_rdn,
362
+ :delete_attributes => false
363
+ )
364
+ return true
365
+ else
366
+ puts Base.error
367
+ return false
368
+ end
369
+ end
370
+
371
+ # FIXME: Need to document the Base::new
372
+ def initialize(attributes = {}) # :nodoc:
373
+ if attributes.is_a? Net::LDAP::Entry
374
+ @entry = attributes
375
+ @attributes = {}
376
+ else
377
+ @entry = nil
378
+ @attributes = attributes
379
+ end
380
+ end
381
+
382
+ def method_missing(name, args = []) # :nodoc:
383
+ name_s = name.to_s.downcase
384
+ name = name_s.to_sym
385
+ if name_s[-1,1] == '='
386
+ @attributes[name_s[0,name_s.size-1].to_sym] = args
387
+ else
388
+ if @attributes.has_key?(name)
389
+ return @attributes[name]
390
+ elsif @entry
391
+ begin
392
+ value = @entry.send(name)
393
+ value = value.to_s if value.nil? || value.size == 1
394
+ return value
395
+ rescue NoMethodError
396
+ return nil
397
+ end
398
+ else
399
+ super
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end