chuckdbacon-activedirectory 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
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
+ # The ActiveDirectory::Container class represents a more malleable way
27
+ # of dealing with LDAP Distinguished Names (dn), like
28
+ # "cn=UserName,ou=Users,dc=example,dc=org".
29
+ #
30
+ # The following two representations of the above dn are identical:
31
+ #
32
+ # dn = "cn=UserName,ou=Users,dc=example,dc=org"
33
+ # dn = ActiveDirectory::Container.dc('org').dc('example').ou('Users').cn('UserName').to_s
34
+ #
35
+ class Container
36
+ attr_reader :type
37
+ attr_reader :name
38
+ attr_reader :parent
39
+
40
+ def initialize(type, name, node = nil) #:nodoc:
41
+ @type = type
42
+ @name = name
43
+ @node = node
44
+ end
45
+
46
+ #
47
+ # Creates a starting OU (Organizational Unit) dn part.
48
+ #
49
+ # # ou_part = "ou=OrganizationalUnit"
50
+ # ou_part = ActiveDirectory::Container.ou('OrganizationalUnit').to_s
51
+ #
52
+ def self.ou(name)
53
+ new(:ou, name, nil)
54
+ end
55
+
56
+ #
57
+ # Creates a starting DC (Domain Component) dn part.
58
+ #
59
+ # # dc_part = "dc=net"
60
+ # dc_part = ActiveDirectory::Container.dc('net').to_s
61
+ #
62
+ def self.dc(name)
63
+ new(:dc, name, nil)
64
+ end
65
+
66
+ #
67
+ # Creates a starting CN (Canonical Name) dn part.
68
+ #
69
+ # # cn_part = "cn=CanonicalName"
70
+ # cn_part = ActiveDirectory::Container.cn('CanonicalName').to_s
71
+ #
72
+ def self.cn(name)
73
+ new(:cn, name, nil)
74
+ end
75
+
76
+ #
77
+ # Appends an OU (Organizational Unit) dn part to another Container.
78
+ #
79
+ # # ou = "ou=InfoTech,dc=net"
80
+ # ou = ActiveDirectory::Container.dc("net").ou("InfoTech").to_s
81
+ #
82
+ def ou(name)
83
+ self.class.new(:ou, name, self)
84
+ end
85
+
86
+ #
87
+ # Appends a DC (Domain Component) dn part to another Container.
88
+ #
89
+ # # base = "dc=example,dc=net"
90
+ # base = ActiveDirectory::Container.dc("net").dc("example").to_s
91
+ #
92
+ def dc(name)
93
+ self.class.new(:dc, name, self)
94
+ end
95
+
96
+ #
97
+ # Appends a CN (Canonical Name) dn part to another Container.
98
+ #
99
+ # # user = "cn=UID,ou=Users"
100
+ # user = ActiveDirectory::Container.ou("Users").cn("UID")
101
+ #
102
+ def cn(name)
103
+ self.class.new(:cn, name, self)
104
+ end
105
+
106
+ #
107
+ # Converts the Container object to its String representation.
108
+ #
109
+ def to_s
110
+ @node ? "#{@type}=#{name},#{@node.to_s}" : "#{@type}=#{name}"
111
+ end
112
+
113
+ def ==(other) #:nodoc:
114
+ to_s.downcase == other.to_s.downcase
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,162 @@
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 Group < Base
26
+ include Member
27
+
28
+ def self.filter # :nodoc:
29
+ Net::LDAP::Filter.eq(:objectClass,'group')
30
+ end
31
+
32
+ def self.required_attributes # :nodoc:
33
+ { :objectClass => [ 'top', 'group' ] }
34
+ end
35
+
36
+ def reload # :nodoc:
37
+ @member_users_non_r = nil
38
+ @member_users_r = nil
39
+ @member_groups_non_r = nil
40
+ @member_groups_r = nil
41
+ @groups = nil
42
+ super
43
+ end
44
+
45
+ #
46
+ # Returns true if the passed User or Group object belongs to
47
+ # this group. For performance reasons, the check is handled
48
+ # by the User or Group object passed.
49
+ #
50
+ def has_member?(user)
51
+ user.member_of?(self)
52
+ end
53
+
54
+ #
55
+ # Add the passed User or Group object to this Group. Returns true if
56
+ # the User or Group is already a member of the group, or if the operation
57
+ # to add them succeeds.
58
+ #
59
+ def add(new_member)
60
+ return false unless new_member.is_a?(User) || new_member.is_a?(Group)
61
+ if @@ldap.modify(:dn => distinguishedName, :operations => [
62
+ [ :add, :member, new_member.distinguishedName ]
63
+ ])
64
+ return true
65
+ else
66
+ return has_member?(new_member)
67
+ end
68
+ end
69
+
70
+ #
71
+ # Remove a User or Group from this Group. Returns true if the User or
72
+ # Group does not belong to this Group, or if the oepration to remove them
73
+ # succeeds.
74
+ #
75
+ def remove(member)
76
+ return false unless member.is_a?(User) || member.is_a?(Group)
77
+ if @@ldap.modify(:dn => distinguishedName, :operations => [
78
+ [ :delete, :member, member.distinguishedName ]
79
+ ])
80
+ return true
81
+ else
82
+ return !has_member?(member)
83
+ end
84
+ end
85
+
86
+ def has_members?
87
+ begin
88
+ return (@entry.member.nil? || @entry.member.empty?) ? false : true
89
+ rescue NoMethodError
90
+ return false
91
+ end
92
+ end
93
+
94
+ #
95
+ # Returns an array of all User objects that belong to this group.
96
+ #
97
+ # If the recursive argument is passed as false, then only Users who
98
+ # belong explicitly to this Group are returned.
99
+ #
100
+ # If the recursive argument is passed as true, then all Users who
101
+ # belong to this Group, or any of its subgroups, are returned.
102
+ #
103
+ def member_users(recursive = false)
104
+ return [] unless has_members?
105
+ if recursive
106
+ if @member_users_r.nil?
107
+ @member_users_r = []
108
+ @entry.member.each do |member_dn|
109
+ subuser = User.find_by_distinguishedName(member_dn)
110
+ if subuser
111
+ @member_users_r << subuser
112
+ else
113
+ subgroup = Group.find_by_distinguishedName(member_dn)
114
+ if subgroup
115
+ @member_users_r = @member_users_r.concat(subgroup.member_users(true))
116
+ end
117
+ end
118
+ end
119
+ end
120
+ return @member_users_r
121
+ else
122
+ @member_users_non_r ||= @entry.member.collect { |dn| User.find_by_distinguishedName(dn) }.delete_if { |u| u.nil? }
123
+ end
124
+ end
125
+
126
+ #
127
+ # Returns an array of all Group objects that belong to this group.
128
+ #
129
+ # If the recursive argument is passed as false, then only Groups that
130
+ # belong explicitly to this Group are returned.
131
+ #
132
+ # If the recursive argument is passed as true, then all Groups that
133
+ # belong to this Group, or any of its subgroups, are returned.
134
+ #
135
+ def member_groups(recursive = false)
136
+ return [] unless has_members?
137
+ if recursive
138
+ if @member_groups_r.nil?
139
+ @member_groups_r = []
140
+ @entry.member.each do |member_dn|
141
+ subgroup = Group.find_by_distinguishedName(member_dn)
142
+ if subgroup
143
+ @member_groups_r << subgroup
144
+ @member_groups_r = @member_groups_r.concat(subgroup.member_groups(true))
145
+ end
146
+ end
147
+ end
148
+ return @member_groups_r
149
+ else
150
+ @member_groups_non_r ||= @entry.member.collect { |dn| Group.find_by_distinguishedName(dn) }.delete_if { |g| g.nil? }
151
+ end
152
+ end
153
+
154
+ #
155
+ # Returns an array of Group objects that this Group belongs to.
156
+ #
157
+ def groups
158
+ return [] if memberOf.nil?
159
+ @groups ||= memberOf.collect { |group_dn| Group.find_by_distinguishedName(group_dn) }
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,56 @@
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
+ module Member
26
+ #
27
+ # Returns true if this member (User or Group) is a member of
28
+ # the passed Group object.
29
+ #
30
+ def member_of?(usergroup)
31
+ group_dns = memberOf
32
+ return false if group_dns.nil? || group_dns.empty?
33
+ #group_dns = [group_dns] unless group_dns.is_a?(Array)
34
+ group_dns.include?(usergroup.dn)
35
+ end
36
+
37
+ #
38
+ # Add the member to the passed Group object. Returns true if this object
39
+ # is already a member of the Group, or if the operation to add it succeeded.
40
+ #
41
+ def join(group)
42
+ return false unless group.is_a?(Group)
43
+ group.add(self)
44
+ end
45
+
46
+ #
47
+ # Remove the member from the passed Group object. Returns true if this
48
+ # object is not a member of the Group, or if the operation to remove it
49
+ # succeeded.
50
+ #
51
+ def unjoin(group)
52
+ return false unless group.is_a?(Group)
53
+ group.remove(self)
54
+ end
55
+ end
56
+ end
@@ -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