activedirectory 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,42 @@
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 Password
26
+ #
27
+ # Encodes an unencrypted password into an encrypted password
28
+ # that the Active Directory server will understand.
29
+ #
30
+ def self.encode(password)
31
+ ("\"#{password}\"".split(//).collect { |c| "#{c}\000" }).join
32
+ end
33
+
34
+ #
35
+ # Always returns nil, since you can't decrypt the User's encrypted
36
+ # password.
37
+ #
38
+ def self.decode(hashed)
39
+ nil
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,234 @@
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
+ # UserSynchronizer is a utility class that encapsulates dealings
25
+ # with the Active Directory backend. It is primarily responsible for
26
+ # updating Active Directory people in the local database from the
27
+ # Active Directory store. In this fashion, name changes, email address
28
+ # changes and such are all handled invisibly.
29
+ #
30
+ # UserSynchronizer is also responsible for disabling people who are no
31
+ # longer in the Sales Tracker group, and creating people who are in the group,
32
+ # but not in the local database. This gives us another administrative
33
+ # convenience, since new hires will be added to the system with some
34
+ # regularity, and terminations are eventually cleaned out.
35
+ #
36
+ # UserSynchronizer.sync_users_in_group will return a hash with the following keys:
37
+ # * :added - An array of ActiveDirectory::User objects that were added.
38
+ # * :disabled - An array of ActiveDirectory::User objects that were disabled.
39
+ # * :updated - An array of ActiveDirectory::User objects that were updated.
40
+ #
41
+ # The following method illustrates how this would be used to notify a site
42
+ # administrator to changes brought about by synchronization:
43
+ #
44
+ # def report(results)
45
+ # puts "#####################################################"
46
+ # puts "# Active Directory People Synchronization Summary #"
47
+ # puts "#####################################################"
48
+ # puts
49
+ #
50
+ # puts "New People Added (#{results[:added].size})"
51
+ # puts "-----------------------------------------------------"
52
+ # results[:added].sort_by(&:name).each { |p| out.puts " + #{p.name}" }
53
+ # puts
54
+ #
55
+ # puts "People Disabled (#{results[:disabled].size})"
56
+ # puts "-----------------------------------------------------"
57
+ # results[:disabled].sort_by(&:name).each { |p| out.puts " - #{p.name}" }
58
+ # puts
59
+ #
60
+ # puts "Existing People Updated (#{results[:updated].size})"
61
+ # puts "-----------------------------------------------------"
62
+ # results[:updated].sort_by(&:name).each { |p| out.puts " u #{p.name}" }
63
+ # puts
64
+ # end
65
+ #
66
+ class ActiveDirectory::Rails::UserSynchronizer
67
+ @@default_group = nil
68
+ cattr_accessor :default_group
69
+
70
+ @@run_handler = nil
71
+ cattr_accessor :run_handler
72
+
73
+ @@attribute_map = {
74
+ :first_name => :givenName,
75
+ :last_name => :sn,
76
+ :username => :sAMAccountName,
77
+ :email => :mail,
78
+ }
79
+ cattr_accessor :attribute_map
80
+
81
+ @@person_class = Person
82
+ cattr_accessor :person_class
83
+
84
+ class << self
85
+ # The primary interface to synchronization, run processes
86
+ # all of the Active Directory changes, additions and removals
87
+ # through sync_users_in_group, and then notifies administrators
88
+ # if it finds anyone new.
89
+ #
90
+ # This is the preferred way to run the UserSynchronizer.
91
+ #
92
+ def run
93
+ results = sync_users_in_group
94
+ @@run_handler.nil? results : @@run_handler.call(results)
95
+ end
96
+
97
+ # Compares the membership of the Active Directory group named
98
+ # `group_name' and AD-enabled accounts in the local database.
99
+ #
100
+ # This method is the workhorse of UserSynchronizer, handling
101
+ # the addition, removal and updates of AD people.
102
+ #
103
+ # It will return either false, or a hash with three keys, :added, :updated
104
+ # and :disabled, each of which contains an array of the Person
105
+ # objects that were (respectively) added, updated and disabled.
106
+ #
107
+ # If the given group_name does not resolve to a valid
108
+ # ActiveDirectory::Group object, sync_users_in_group will return
109
+ # false.
110
+ #
111
+ # The return value (for example) can be used by a notification process
112
+ # to construct a message detailing who was added, removed, etc.
113
+ #
114
+ def sync_users_in_group(group_name = nil)
115
+ group_name ||= @@default_group
116
+ return false unless group_name
117
+
118
+ ad_group = ActiveDirectory::Group.find_by_sAMAccountName(group_name)
119
+ return false unless ad_group
120
+
121
+ @people = person_class.in_active_directory.index_by(&:guid)
122
+
123
+ summary = {
124
+ :added => [],
125
+ :disabled => [],
126
+ :updated => []
127
+ }
128
+
129
+ # Find all member users (recursively looking at member groups)
130
+ # and synchronize! them with their Person counterparts.
131
+ #
132
+ ad_group.member_users(true).each do |ad_user|
133
+ person = @people[ad_user.objectGUID]
134
+ if person
135
+ synchronize!(person, ad_user)
136
+ @people.delete(ad_user.objectGUID)
137
+ summary[:updated] << person
138
+ else
139
+ person = create_from(ad_user)
140
+ summary[:added] << person
141
+ end
142
+ end
143
+
144
+ # Disable AD users we didn't find in AD.
145
+ # Because we are not clearing the GUID in the disable! call,
146
+ # we may process someone more than once.
147
+ #
148
+ @people.each do |guid, person|
149
+ disable!(person)
150
+ summary[:disabled] << person
151
+ end
152
+
153
+ summary
154
+ end
155
+
156
+ # Synchronize a peron with AD store by looking up their username.
157
+ #
158
+ # This is used for the initial bootstrap, because we don't know
159
+ # a person's objectGUID offhand. It will probably never be seen
160
+ # in any production code.
161
+ #
162
+ def update_using_username(person)
163
+ ad_user = ActiveDirectory::User.find_by_sAMAccountName(person.username)
164
+ synchronize!(person, ad_user)
165
+ end
166
+
167
+ # Sync a person with AD store by looking up their GUID
168
+ # (This is the most reliable option, as a username can change,
169
+ # but the GUID will stay the same).
170
+ #
171
+ # This method is not used in production, but can be useful in
172
+ # a console'd environment to selectively update just a few people.
173
+ #
174
+ def update_using_guid(person)
175
+ ad_user = ActiveDirectory::User.find_by_objectGUID(person.guid)
176
+ synchronize!(person, ad_user)
177
+ end
178
+
179
+ # Synchronize the attributes of the given Person with those
180
+ # found in the (hopefully associated) Active Directory user
181
+ #
182
+ # Because we are managing a mixed database of both AD and non-AD
183
+ # people, we have to be careful. We cannot assume that a nil
184
+ # ad_user argument means the person should be disabled.
185
+ #
186
+ def synchronize!(person, ad_user)
187
+ person.update_attributes(attributes_from(ad_user)) if ad_user
188
+ end
189
+
190
+ # Disable a person, and clear out their authentication information.
191
+ # This is primarily used when we find terminated employees who are
192
+ # still in the local database as AD users, but no longer have an
193
+ # AD account.
194
+ #
195
+ # There is a special case for people who have not logged sales.
196
+ # They are removed outright, to keep terminated trainees from
197
+ # cluttering up the Person table.
198
+ #
199
+ # Note that we do not clear their GUID. Active Directory is not
200
+ # supposed to re-use its GUIDs, so we should be safe there.
201
+ #
202
+ def disable!(person)
203
+ if person.respond_to? :removable? and !person.removable?
204
+ person.update_attribute(:username, '')
205
+ person.update_attribute(:email, '')
206
+ else
207
+ person.destroy
208
+ end
209
+ end
210
+
211
+ # Creates a new Person object based on the attributes of
212
+ # an Active Directory user. sync_users_in_group uses this when
213
+ # it finds new people.
214
+ #
215
+ # All Person objects will be created as generic Persons,
216
+ # not CSRs or TeamLeaders. Administrators are responsible
217
+ # for promoting and associating new people in the backend.
218
+ #
219
+ def create_from(ad_user)
220
+ person = person_class.create(attributes_from(ad_user))
221
+ end
222
+
223
+ # Translates the attributes of ad_user into a hash that can
224
+ # be used to create or update a Person object.
225
+ #
226
+ def attributes_from(ad_user)
227
+ h = {}
228
+ @@attribute_map.each { |local, remote| h[local] = ad_user.send(remote) }
229
+ h[:guid] = ad_user.objectGUID
230
+ h[:password => '']
231
+ h
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,137 @@
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::Rails::User
25
+ def self.included(klass)
26
+ klass.extend(ClassMethods)
27
+ klass.send(:include, InstanceMethods)
28
+ end
29
+
30
+ module InstanceMethods
31
+ # Is this Person active? Active people have valid
32
+ # usernames. Inactive people have empty usernames.
33
+ #
34
+ def active?
35
+ username != ""
36
+ end
37
+
38
+ # Whether or not this Person has a corresponding Active Directory
39
+ # account that we can synchronize with, through the PeopleSynchronizer.
40
+ #
41
+ def in_active_directory?
42
+ !guid.blank?
43
+ end
44
+
45
+ # Whether or not this Person can be authenticated with the
46
+ # given password, against Active Directory.
47
+ #
48
+ # For Active Directory authentication, we attempt to bind to the
49
+ # configured AD server as the user, and supply the password for
50
+ # authentication.
51
+ #
52
+ # There are two special cases for authentication, related to the
53
+ # environment the app is currently running in:
54
+ #
55
+ # *Development*
56
+ #
57
+ # In development, the blank password ('') will always cause this method
58
+ # to return true, thereby allowing developers to test functionality
59
+ # for a variety of roles.
60
+ #
61
+ # *Training*
62
+ #
63
+ # In training, a special training password ('trainme') will always
64
+ # cause this method to return true, thereby allowing trainers to
65
+ # use other people accounts to illustrate certain restricted processes.
66
+ #
67
+ def authenticates?(password)
68
+ # Never allow inactive users.
69
+ return false unless active?
70
+
71
+ # Allow blank password for any account in development.
72
+ return true if password == "" and ENV['RAILS_ENV'] == 'development'
73
+ return true if password == "trainme" and ENV['RAILS_ENV'] == 'training'
74
+
75
+ # Don't go against AD unless we really mean it.
76
+ return false unless ENV['RAILS_ENV'] == 'production'
77
+
78
+ # If they are not in AD, fail.
79
+ return false unless in_active_directory?
80
+
81
+ ad_user = ActiveDirectory::User.find_by_sAMAccountName(self.username)
82
+ ad_user and ad_user.authenticate(password)
83
+ end
84
+
85
+ def active_directory_equivalent=(ad_user)
86
+ return unless ad_user
87
+ update_attributes(
88
+ :first_name => ad_user.givenName,
89
+ :middle_name => ad_user.initials,
90
+ :last_name => ad_user.sn,
91
+ :username => ad_user.sAMAccountName,
92
+ :email => ad_user.mail,
93
+ :guid => ad_user.objectGUID
94
+ )
95
+ end
96
+ end
97
+
98
+ module ClassMethods
99
+ # Attempt to authenticate someone with a username and password.
100
+ # This method properly handles both local store users and AD
101
+ # users.
102
+ #
103
+ # If the username is valid, and the password matches the username,
104
+ # the Person object corresponding to the username is return.
105
+ #
106
+ # Otherwise, nil is returned, to indicate an authentication failure.
107
+ #
108
+ def authenticate(username, password)
109
+ person = find_by_username(username)
110
+ return person if (person and person.authenticates?(password))
111
+ nil
112
+ end
113
+
114
+ # Retrieves all of the Person objects that have corresponding
115
+ # Active Directory accounts. This method does not contact
116
+ # the AD servers to retrieve the AD objects -- that is left up
117
+ # to the caller.
118
+ #
119
+ def in_active_directory
120
+ find(:all, :conditions => 'guid IS NOT NULL AND guid != ""')
121
+ end
122
+
123
+ # Retrieves all Person objects that are currently active,
124
+ # meaning they have not been disabled by PeopleSynchronizer.
125
+ #
126
+ def active
127
+ find(:all, :conditions => 'username != ""')
128
+ end
129
+
130
+ # Retrieves all Person objects that are currently inactive,
131
+ # meaning they have been disabled by PeopleSynchronizer.
132
+ #
133
+ def inactive
134
+ find(:all, :conditions => 'username = ""')
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,46 @@
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 Timestamp
26
+ AD_DIVISOR = 10_000_000 #:nodoc:
27
+ AD_OFFSET = 11_644_473_600 #:nodoc:
28
+
29
+ #
30
+ # Encodes a local Time object (or the number of seconds since January
31
+ # 1, 1970) into a timestamp that the Active Directory server can
32
+ # understand (number of 100 nanosecond time units since January 1, 1600)
33
+ #
34
+ def self.encode(local_time)
35
+ (local_time.to_i + AD_OFFSET) * AD_DIVISOR
36
+ end
37
+
38
+ #
39
+ # Decodes an Active Directory timestamp (the number of 100 nanosecond time
40
+ # units since January 1, 1600) into a Ruby Time object.
41
+ #
42
+ def self.decode(remote_time)
43
+ Time.at( (remote_time.to_i / AD_DIVISOR) - AD_OFFSET )
44
+ end
45
+ end
46
+ end