chuckdbacon-activedirectory 1.0.4

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,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