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.
- data/lib/active_directory.rb +39 -0
- data/lib/active_directory/base.rb +405 -0
- data/lib/active_directory/computer.rb +15 -0
- data/lib/active_directory/container.rb +94 -0
- data/lib/active_directory/group.rb +145 -0
- data/lib/active_directory/member.rb +33 -0
- data/lib/active_directory/ou.rb +164 -0
- data/lib/active_directory/password.rb +19 -0
- data/lib/active_directory/rails/synchronizer.rb +211 -0
- data/lib/active_directory/rails/user.rb +118 -0
- data/lib/active_directory/timestamp.rb +23 -0
- data/lib/active_directory/user.rb +142 -0
- metadata +73 -0
@@ -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
|