chuckdbacon-activedirectory 1.0.4

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.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ = Active Directory
2
+
3
+ Ruby Integration with Microsoft's Active Directory system
4
+
@@ -0,0 +1,37 @@
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
23
+
24
+ require 'net/ldap'
25
+
26
+ require 'active_directory/base.rb'
27
+ require 'active_directory/container.rb'
28
+ require 'active_directory/member.rb'
29
+
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'
@@ -0,0 +1,405 @@
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
23
+
24
+ module ActiveDirectory
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!(:filter => 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.first if value.kind_of?(Array) && value.size == 1
394
+ value = value.to_s if value.nil? || value.size == 1
395
+ return value
396
+ rescue NoMethodError
397
+ return nil
398
+ end
399
+ else
400
+ super
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,38 @@
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
23
+
24
+ module ActiveDirectory
25
+ class Computer < Base
26
+ def self.filter # :nodoc:
27
+ Net::LDAP::Filter.eq(:objectClass,'computer')
28
+ end
29
+
30
+ def self.required_attributes # :nodoc:
31
+ { :objectClass => [ 'top', 'person', 'organizationalPerson', 'user', 'computer' ] }
32
+ end
33
+
34
+ def hostname
35
+ dNSHostName || name
36
+ end
37
+ end
38
+ end