judy-activedirectory 1.1.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.
@@ -0,0 +1,39 @@
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
+ # Modified by Clinton Judy <clinton@j-udy.com>
7
+ # also modified by James Hunt <filefrog@gmail.com>
8
+ # based on original code by Justin Mecham
9
+ #
10
+ # This program is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
+ #
23
+ #++ license
24
+
25
+ require 'net/ldap'
26
+
27
+ require 'active_directory/base.rb'
28
+ require 'active_directory/container.rb'
29
+ require 'active_directory/member.rb'
30
+
31
+ require 'active_directory/user.rb'
32
+ require 'active_directory/group.rb'
33
+ require 'active_directory/ou.rb'
34
+ require 'active_directory/computer.rb'
35
+
36
+ require 'active_directory/password.rb'
37
+ require 'active_directory/timestamp.rb'
38
+
39
+ require 'active_directory/rails/user.rb'
@@ -0,0 +1,405 @@
1
+ module ActiveDirectory
2
+ #
3
+ # Base class for all Ruby/ActiveDirectory Entry Objects (like User and Group)
4
+ #
5
+ class Base
6
+ #
7
+ # A Net::LDAP::Filter object that doesn't do any filtering
8
+ # (outside of check that the CN attribute is present. This
9
+ # is used internally for specifying a 'no filter' condition
10
+ # for methods that require a filter object.
11
+ #
12
+ NIL_FILTER = Net::LDAP::Filter.pres('cn')
13
+
14
+ @@ldap = nil
15
+
16
+ #
17
+ # Configures the connection for the Ruby/ActiveDirectory library.
18
+ #
19
+ # For example:
20
+ #
21
+ # ActiveDirectory::Base.setup(
22
+ # :host => 'domain_controller1.example.org',
23
+ # :port => 389,
24
+ # :base => 'dc=example,dc=org',
25
+ # :auth => {
26
+ # :username => 'querying_user@example.org',
27
+ # :password => 'querying_users_password'
28
+ # }
29
+ # )
30
+ #
31
+ # This will configure Ruby/ActiveDirectory to connect to the domain
32
+ # controller at domain_controller1.example.org, using port 389. The
33
+ # domain's base LDAP dn is expected to be 'dc=example,dc=org', and
34
+ # Ruby/ActiveDirectory will try to bind as the
35
+ # querying_user@example.org user, using the supplied password.
36
+ #
37
+ # Currently, there can be only one active connection per
38
+ # execution context.
39
+ #
40
+ # For more advanced options, refer to the Net::LDAP.new
41
+ # documentation.
42
+ #
43
+ def self.setup(settings)
44
+ @@settings = settings
45
+ @@ldap = Net::LDAP.new(settings)
46
+ end
47
+
48
+ def self.error
49
+ "#{@@ldap.get_operation_result.code}: #{@@ldap.get_operation_result.message}"
50
+ end
51
+
52
+ def self.filter # :nodoc:
53
+ NIL_FILTER
54
+ end
55
+
56
+ def self.required_attributes # :nodoc:
57
+ {}
58
+ end
59
+
60
+ #
61
+ # Check to see if any entries matching the passed criteria exists.
62
+ #
63
+ # Filters should be passed as a hash of
64
+ # attribute_name => expected_value, like:
65
+ #
66
+ # User.exists?(
67
+ # :sn => 'Hunt',
68
+ # :givenName => 'James'
69
+ # )
70
+ #
71
+ # which will return true if one or more User entries have an
72
+ # sn (surname) of exactly 'Hunt' and a givenName (first name)
73
+ # of exactly 'James'.
74
+ #
75
+ # Partial attribute matches are available. For instance,
76
+ #
77
+ # Group.exists?(
78
+ # :description => 'OldGroup_*'
79
+ # )
80
+ #
81
+ # would return true if there are any Group objects in
82
+ # Active Directory whose descriptions start with OldGroup_,
83
+ # like OldGroup_Reporting, or OldGroup_Admins.
84
+ #
85
+ # Note that the * wildcard matches zero or more characters,
86
+ # so the above query would also return true if a group named
87
+ # 'OldGroup_' exists.
88
+ #
89
+ def self.exists?(filter_as_hash)
90
+ criteria = make_filter_from_hash(filter_as_hash) & filter
91
+ if @@ldap.search(:filter => criteria)
92
+ return (@@ldap.search(:filter => criteria).size > 0)
93
+ else
94
+ return false
95
+ end
96
+ end
97
+
98
+ #
99
+ # Whether or not the entry has local changes that have not yet been
100
+ # replicated to the Active Directory server via a call to Base#save
101
+ #
102
+ def changed?
103
+ !@attributes.empty?
104
+ end
105
+
106
+ def self.make_filter_from_hash(filter_as_hash) # :nodoc:
107
+ return NIL_FILTER if filter_as_hash.nil? || filter_as_hash.empty?
108
+ keys = filter_as_hash.keys
109
+
110
+ first_key = keys.delete(keys[0])
111
+ f = Net::LDAP::Filter.eq(first_key, filter_as_hash[first_key].to_s)
112
+ keys.each do |key|
113
+ f = f & Net::LDAP::Filter.eq(key, filter_as_hash[key].to_s)
114
+ end
115
+ f
116
+ end
117
+
118
+ #
119
+ # Performs a search on the Active Directory store, with similar
120
+ # syntax to the Rails ActiveRecord#find method.
121
+ #
122
+ # The first argument passed should be
123
+ # either :first or :all, to indicate that we want only one
124
+ # (:first) or all (:all) results back from the resultant set.
125
+ #
126
+ # The second argument should be a hash of attribute_name =>
127
+ # expected_value pairs.
128
+ #
129
+ # User.find(:all, :sn => 'Hunt')
130
+ #
131
+ # would find all of the User objects in Active Directory that
132
+ # have a surname of exactly 'Hunt'. As with the Base.exists?
133
+ # method, partial searches are allowed.
134
+ #
135
+ # This method always returns an array if the caller specifies
136
+ # :all for the search type (first argument). If no results
137
+ # are found, the array will be empty.
138
+ #
139
+ # If you call find(:first, ...), you will either get an object
140
+ # (a User or a Group) back, or nil, if there were no entries
141
+ # matching your filter.
142
+ #
143
+ def self.find(*args)
144
+ options = {
145
+ :filter => NIL_FILTER,
146
+ :in => ''
147
+ }
148
+ # Add search path to base dn
149
+ options[:in] = [ options[:in].to_s, @@settings[:base] ].delete_if { |part| part.empty? }.join(",")
150
+ # Extract filter arguments from end of input
151
+ filter_args = args.extract_options!
152
+
153
+ if filter_args.is_a? Hash
154
+ # Make LDAP-compatible filter from hash (unless hash is empty)
155
+ our_filters = make_filter_from_hash(filter_args) unless filter_args.empty?
156
+ end
157
+ # Add our filters to final result unless it's just the default "empty" filter
158
+ options[:filter] = our_filters & filter unless self.filter == NIL_FILTER
159
+
160
+ case args.first
161
+ when :first then find_first(options)
162
+ else find_all(options) # Find any that match as a default.
163
+ end
164
+
165
+ end
166
+
167
+ def self.find_all(options)
168
+ results = []
169
+ @@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
170
+ results << new(entry)
171
+ end
172
+ results
173
+ end
174
+
175
+ def self.find_first(options)
176
+ @@ldap.search(:filter => options[:filter], :base => options[:in], :return_result => false) do |entry|
177
+ return new(entry)
178
+ end
179
+ end
180
+
181
+ def self.method_missing(name, *args) # :nodoc:
182
+ name = name.to_s
183
+ if (name[0,5] == 'find_')
184
+ find_spec, attribute_spec = parse_finder_spec(name)
185
+ raise ArgumentError, "find: Wrong number of arguments (#{args.size} for #{attribute_spec.size})" unless args.size == attribute_spec.size
186
+ filters = {}
187
+ [attribute_spec,args].transpose.each { |pr| filters[pr[0]] = pr[1] }
188
+ find(find_spec, :filter => filters)
189
+ else
190
+ super name.to_sym, args
191
+ end
192
+ end
193
+
194
+ def self.parse_finder_spec(method_name) # :nodoc:
195
+ # FIXME: This is a prime candidate for a
196
+ # first-class object, FinderSpec
197
+
198
+ method_name = method_name.gsub(/^find_/,'').gsub(/^by_/,'first_by_')
199
+ find_spec, attribute_spec = *(method_name.split('_by_'))
200
+ find_spec = find_spec.to_sym
201
+ attribute_spec = attribute_spec.split('_and_').collect { |s| s.to_sym }
202
+
203
+ return find_spec, attribute_spec
204
+ end
205
+
206
+ def ==(other) # :nodoc:
207
+ return false if other.nil?
208
+ other.distinguishedName == distinguishedName
209
+ end
210
+
211
+ #
212
+ # Returns true if this entry does not yet exist in Active Directory.
213
+ #
214
+ def new_record?
215
+ @entry.nil?
216
+ end
217
+
218
+ #
219
+ # Refreshes the attributes for the entry with updated data from the
220
+ # domain controller.
221
+ #
222
+ def reload
223
+ return false if new_record?
224
+
225
+ @entry = @@ldap.search(:filter => Net::LDAP::Filter.eq('distinguishedName',distinguishedName))[0]
226
+ return !@entry.nil?
227
+ end
228
+
229
+ #
230
+ # Updates a single attribute (name) with one or more values
231
+ # (value), by immediately contacting the Active Directory
232
+ # server and initiating the update remotely.
233
+ #
234
+ # Entries are always reloaded (via Base.reload) after calling
235
+ # this method.
236
+ #
237
+ def update_attribute(name, value)
238
+ update_attributes(name.to_s => value)
239
+ end
240
+
241
+ #
242
+ # Updates multiple attributes, like ActiveRecord#update_attributes.
243
+ # The updates are immediately sent to the server for processing,
244
+ # and the entry is reloaded after the update (if all went well).
245
+ #
246
+ def update_attributes(attributes_to_update)
247
+ return true if attributes_to_update.empty?
248
+
249
+ operations = []
250
+ attributes_to_update.each do |attribute, values|
251
+ if values.nil? || values.empty?
252
+ operations << [ :delete, attribute, nil ]
253
+ else
254
+ values = [values] unless values.is_a? Array
255
+ values = values.collect { |v| v.to_s }
256
+
257
+ current_value = begin
258
+ @entry.send(attribute)
259
+ rescue NoMethodError
260
+ nil
261
+ end
262
+
263
+ operations << [ (current_value.nil? ? :add : :replace), attribute, values ]
264
+ end
265
+ end
266
+
267
+ @@ldap.modify(
268
+ :dn => distinguishedName,
269
+ :operations => operations
270
+ ) && reload
271
+ end
272
+
273
+ #
274
+ # Create a new entry in the Active Record store.
275
+ #
276
+ # dn is the Distinguished Name for the new entry. This must be
277
+ # a unique identifier, and can be passed as either a Container
278
+ # or a plain string.
279
+ #
280
+ # attributes is a symbol-keyed hash of attribute_name => value
281
+ # pairs.
282
+ #
283
+ def self.create(dn,attributes)
284
+ return nil if dn.nil? || attributes.nil?
285
+ begin
286
+ attributes.merge!(required_attributes)
287
+ if @@ldap.add(:dn => dn.to_s, :attributes => attributes)
288
+ return find_by_distinguishedName(dn.to_s)
289
+ else
290
+ return nil
291
+ end
292
+ rescue
293
+ return nil
294
+ end
295
+ end
296
+
297
+ #
298
+ # Deletes the entry from the Active Record store and returns true
299
+ # if the operation was successfully.
300
+ #
301
+ def destroy
302
+ return false if new_record?
303
+
304
+ if @@ldap.delete(:dn => distinguishedName)
305
+ @entry = nil
306
+ @attributes = {}
307
+ return true
308
+ else
309
+ return false
310
+ end
311
+ end
312
+
313
+ #
314
+ # Saves any pending changes to the entry by updating the remote
315
+ # entry.
316
+ #
317
+ def save
318
+ if update_attributes(@attributes)
319
+ @attributes = {}
320
+ return true
321
+ else
322
+ return false
323
+ end
324
+ end
325
+
326
+ #
327
+ # This method may one day provide the ability to move entries from
328
+ # container to container. Currently, it does nothing, as we are
329
+ # waiting on the Net::LDAP folks to either document the
330
+ # Net::LDAP#modrdn method, or provide a similar method for
331
+ # moving / renaming LDAP entries.
332
+ #
333
+ def move(new_rdn)
334
+ return false if new_record?
335
+ puts "Moving #{distinguishedName} to RDN: #{new_rdn}"
336
+
337
+ settings = @@settings.dup
338
+ settings[:port] = 636
339
+ settings[:encryption] = { :method => :simple_tls }
340
+
341
+ ldap = Net::LDAP.new(settings)
342
+
343
+ if ldap.rename(
344
+ :olddn => distinguishedName,
345
+ :newrdn => new_rdn,
346
+ :delete_attributes => false
347
+ )
348
+ return true
349
+ else
350
+ puts Base.error
351
+ return false
352
+ end
353
+ end
354
+
355
+ # FIXME: Need to document the Base::new
356
+ def initialize(attributes = {}) # :nodoc:
357
+ if attributes.is_a? Net::LDAP::Entry
358
+ @entry = attributes
359
+ @attributes = {}
360
+ else
361
+ @entry = nil
362
+ @attributes = attributes
363
+ end
364
+ end
365
+
366
+ def method_missing(name, args = []) # :nodoc:
367
+ name_s = name.to_s.downcase
368
+ name = name_s.to_sym
369
+ if name_s[-1,1] == '='
370
+ @attributes[name_s[0,name_s.size-1].to_sym] = args
371
+ else
372
+ if @attributes.has_key?(name)
373
+ return @attributes[name]
374
+ elsif @entry
375
+ begin
376
+ value = @entry.send(name)
377
+ value = value.to_s if value.nil? || value.size == 1
378
+ return value
379
+ rescue NoMethodError
380
+ return nil
381
+ end
382
+ else
383
+ super
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ class Array #:nodoc:
391
+ module ExtractOptions
392
+ # Extracts options from a set of arguments. Removes and returns the last
393
+ # element in the array if it's a hash, otherwise returns a blank hash.
394
+ #
395
+ # def options(*args)
396
+ # args.extract_options!
397
+ # end
398
+ #
399
+ # options(1, 2) # => {}
400
+ # options(1, 2, :a => :b) # => {:a=>:b}
401
+ def extract_options!
402
+ last.is_a?(::Hash) ? pop : {}
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveDirectory
2
+ class Computer < Base
3
+ def self.filter # :nodoc:
4
+ Net::LDAP::Filter.eq(:objectClass,'computer')
5
+ end
6
+
7
+ def self.required_attributes # :nodoc:
8
+ { :objectClass => [ 'top', 'person', 'organizationalPerson', 'user', 'computer' ] }
9
+ end
10
+
11
+ def hostname
12
+ dNSHostName || name
13
+ end
14
+ end
15
+ end