chuckdbacon-activedirectory 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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