activedirectory 0.9.3 → 1.0.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.
@@ -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