radum 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright 2009 Shaun Rowland. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification,
4
+ are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice,
7
+ this list of conditions and the following disclaimer.
8
+ 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ this list of conditions and the following disclaimer in the documentation
10
+ and/or other materials provided with the distribution.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
16
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
+
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+
3
+ gem 'ruby-net-ldap', '= 0.0.4'
4
+ require 'net/ldap'
5
+
6
+ require 'radum/logger'
7
+ require 'radum/ad'
8
+ require 'radum/container'
9
+ require 'radum/group'
10
+ require 'radum/user'
@@ -0,0 +1,3355 @@
1
+ # The RADUM module provides an interface to Microsoft Active Directory for
2
+ # working with users and groups. The User class represents a standard Windows
3
+ # user account. The UNIXUser class represents a Windows account that has UNIX
4
+ # attributes. Similarly, the Group class represents a standard Windows group,
5
+ # and a UNIXGroup represents a Windows group that has UNIX attributes. UNIX
6
+ # attributes are supported if Active Directory has been extended, such as
7
+ # when Microsoft Identity Management for UNIX has been installed. LDAP
8
+ # extensions for UNIX are not required if only Windows users and groups are
9
+ # operated on. This module concentrates only on users and groups at this time.
10
+ #
11
+ # This is a pure Ruby implementation. Windows command line tools are not
12
+ # used in any way, so this will work from other platforms such as Mac OS X
13
+ # and Linux in addition to Windows. RADUM does require the Active Directory
14
+ # server to support SSL because that is a requirement for creating user
15
+ # accounts through LDAP. This means that a domain must have a certificate
16
+ # server. Using a self-signed certificate should be fine.
17
+ #
18
+ # RADUM considers its view of user and group attributes to be authoritative,
19
+ # with the exception that it will not modify the members of a group that it
20
+ # is not already aware of. The general RADUM pattern is:
21
+ #
22
+ # * AD.new(...)
23
+ # * Container.new(...) [for any Containers of interest]
24
+ # * AD#load()
25
+ # * Create, update, or remove existing loaded objects.
26
+ # * AD#sync()
27
+ #
28
+ # See the class documenation for more details, especially the AD#load and
29
+ # AD#sync methods.
30
+ #
31
+ # = AD
32
+ #
33
+ # The AD class represents an Active Directory. An AD object can be created
34
+ # with something like the following code:
35
+ #
36
+ # ad = RADUM::AD.new :root => 'dc=example,dc=com',
37
+ # :user => 'cn=Administrator,cn=Users',
38
+ # :password => 'password',
39
+ # :server => '192.168.1.1'
40
+ #
41
+ # The AD class will connect to Active Directory when it needs to do so
42
+ # automatically. A connection will not be made until required. Some arguments
43
+ # in RADUM are specified using a relative path sans the :root argument to
44
+ # the AD.new method, such as the :user argument and Container names discussed
45
+ # next. The AD class provides a read-only ldap attriubte that allows direct
46
+ # interaction with Active Directory if needed. The AD object automatically
47
+ # adds the "cn=Users" Container object because most users have "Domain Users"
48
+ # as their primary Windows group. The primary Windows group is necessary for
49
+ # creating User and UNIXUser objects, especially when AD#load is called. Note
50
+ # that the :root and :user arguments can use lowercase or uppercase for the
51
+ # "dc=", "cn=", or "ou=" path elements.
52
+ #
53
+ # = Container
54
+ #
55
+ # Container objects are added to the AD object before AD#load is called in
56
+ # most cases. Container objects can be created with something like the
57
+ # following code:
58
+ #
59
+ # people = RADUM::Container.new :name => 'ou=People',
60
+ # :directory => ad
61
+ #
62
+ # It is possible to have nested Container objects as well, such as:
63
+ #
64
+ # staff = RADUM::Container.new :name => 'ou=Staff,ou=People',
65
+ # :directory => ad
66
+ # faculty = RADUM::Container.new :name => 'ou=Faculty,ou=People',
67
+ # :directory => ad
68
+ # grads = RADUM::Container.new :name => 'ou=Grads,ou=People',
69
+ # :directory => ad
70
+ #
71
+ # Container objects do not have a direct reference to Container objects they
72
+ # logically contain, but the RADUM code handles these cases when creating and
73
+ # removing Container objects from Active Directory. Container objects do have
74
+ # references to all users and groups they contain as well as the AD object
75
+ # they belong to.
76
+ #
77
+ # The :name of a Container object is the only other case where a relative path
78
+ # is used. The :name should be the directory path (the distinguished name) sans
79
+ # the :root path given to the AD object that owns the Container. Container
80
+ # objects can be Active Directory organizational units or containers. Note that
81
+ # Active Directory containers cannot contain organizational units, but the
82
+ # reverse is true, therefore the following :name would be invalid:
83
+ #
84
+ # invalid = RADUM::Container.new :name => 'ou=Staff,cn=People',
85
+ # :directory => ad
86
+ #
87
+ # but the following :name is valid:
88
+ #
89
+ # valid = RADUM::Container.new :name => 'cn=Staff,ou=People',
90
+ # :directory => ad
91
+ #
92
+ # In general, one should use Active Directory organizational units instead of
93
+ # containers. They are much more useful. The :name argument can use lowercase
94
+ # or uppercase for the "cn=" and "ou=" path elements.
95
+ #
96
+ # Container objects should be added to the AD object before doing serious
97
+ # work in the general case for the best results. This doesn't necessarily have
98
+ # to be done if only creating new objects, but it is still a good idea that
99
+ # won't hurt anyway. The "cn=Users" container is added by default to an AD
100
+ # object when it is created to help make things easier as it is usually
101
+ # required, and if it really is not needed, it should not take too many
102
+ # resources.
103
+ #
104
+ # == Removing Container Objects
105
+ #
106
+ # Removing a Container causes that Container to be removed from Active Directory
107
+ # when AD#sync is called, but only if that is possible. Container objects
108
+ # cannot be removed if they have groups that cannot be removed nor if they
109
+ # logically contain other Container objects. When calling the
110
+ # AD#remove_container method, checks are made for all the objects RADUM knows
111
+ # about. When calling AD#sync checks are made in Active Directory. See the
112
+ # AD#remove_container method documentation for more details. Note that removing
113
+ # a Container will always remove all User or UNIXUser objects it contains
114
+ # regardless, and when AD#sync is called, all possible objects that can be
115
+ # removed will be due to the implementation of AD#remove_container.
116
+ #
117
+ # It is also possible to destroy a Container. Unlike with users and groups,
118
+ # destroying a Container with AD#destroy_container simply removes the reference
119
+ # to the Container in the AD object. It does not result in the Container
120
+ # objects contents from being removed. The Container simply will not be
121
+ # processed in AD#load and AD#sync calls. The AD object determines all user
122
+ # and group references based on the Container objects it owns.
123
+ #
124
+ # = Loading the AD Object
125
+ #
126
+ # Once the Container objects have been added to the AD object, the AD object
127
+ # can be loaded. Calling AD#load is straightforward:
128
+ #
129
+ # ad.load
130
+ #
131
+ # This will create User, UNIXUser, Group, and UNIXGroup objects to represent
132
+ # all such objects in the Container objects added to the AD object. The AD#load
133
+ # method determines if a user or group is a Windows or UNIX type based on the
134
+ # object's LDAP attributes. AD#load can be called multiple times, but it will
135
+ # ignore loading any user or group objects that already exist in the RADUM
136
+ # environment.
137
+ #
138
+ # = Working with Users and Groups
139
+ #
140
+ # User and group object creation results in the users and groups being added to
141
+ # their Container object automatically. Users and groups also have direct
142
+ # references to their corresponding collections of groups for users and
143
+ # groups plus users for groups (since groups can contain other groups as
144
+ # members). User memberships in groups can be accomplished from either
145
+ # perspective due to this automatic handling:
146
+ #
147
+ # * Users can be added to groups through User#add_group.
148
+ # * Users can be added to groups through Group#add_user.
149
+ #
150
+ # Group membership in groups has to be handled by using the Group#add_group
151
+ # method only, which adds the Group or UNIXGroup used as the argument as a
152
+ # member of the Group object to which the method belongs.
153
+ #
154
+ # RADUM ensures that duplicate users and groups cannot be created, including
155
+ # specific attributes of those objects which must be unique. Trying to do
156
+ # something that would result in duplication generally raises a RuntimeError.
157
+ #
158
+ # RADUM handles implicit group memberships from the Windows perspective for
159
+ # UNIXUser objects when adding them to UNIXGroup objects. When adding a
160
+ # UNIXUser to a UNIXGroup, the UNIXUser gets a membership in the UNIXGroup
161
+ # from both the UNIX and Windows perspectives.
162
+ #
163
+ # == Creating Group and UNIXGroup Objects
164
+ #
165
+ # Group objects represent Windows groups and UNIXGroup objects represent UNIX
166
+ # groups, which are Windows groups that have UNIX attributes. Examples include:
167
+ #
168
+ # research_users = RADUM::Group.new :name => 'Research Users',
169
+ # :container => faculty
170
+ # unix_staff = RADUM::UNIXGroup.new :name => 'staff',
171
+ # :container => staff,
172
+ # :gid => 1005
173
+ #
174
+ # There are additional argumements in both cases. See Group.new and
175
+ # UNIXGroup.new for additional details. In both cases, the :rid argument should
176
+ # only be used by AD#load to specify the group object's relative identifier,
177
+ # which is based on the LDAP objectSid attribute. There is an :nis_domain
178
+ # argument for UNIXGroup objects that defaults to "radum" if not specified.
179
+ # NIS is not required for using Active Directory in a centralized authentication
180
+ # design. One could access the UNIX attributes through LDAP directly for
181
+ # example. However, the NIS domain needs to be present if one wants to use
182
+ # the Active Directory Users and Computers GUI tool, therefore one is specified
183
+ # by default. Of course, this should still be set correctly in order to work
184
+ # in the Active Directory Users and Computers GUI tool, but it can be changed
185
+ # there easily and the attributes should still show up correctly. The :type
186
+ # argument applies to both Group and UNIXGroup objects. The default value is
187
+ # RADUM::GROUP_GLOBAL_SECURITY, which is the default when creating group
188
+ # objects using the Active Directory Users and Computers GUI tool in Windows.
189
+ # More details about the restrictions for certain group types can be found in
190
+ # the User#primary_group= method documentation.
191
+ #
192
+ # There is a useful method for finding the next available GID for the UNIXGroup
193
+ # object's :gid argument:
194
+ #
195
+ # gid = ad.load_next_gid
196
+ #
197
+ # The AD#load_next_gid method searches Active Directory and the current RADUM
198
+ # objects for the next GID value that can be used. The :gid attribute for a
199
+ # UNIXGroup also becomes a UNIXUser object's GID value, depending on the
200
+ # :unix_main_group for that UNIXUser.
201
+ #
202
+ # == Removing Group and UNIXGroup Objects
203
+ #
204
+ # Group and UNIXGroup objects can be removed from RADUM by using the following
205
+ # method for the Container object that they belong to:
206
+ #
207
+ # faculty.remove_group research_users
208
+ #
209
+ # or by searching for a group:
210
+ #
211
+ # faculty.remove_group ad.find_group_by_name('Research Users')
212
+ #
213
+ # Removing a group causes that group to be removed from Active Directory when
214
+ # AD#sync is called, but only if that is possible. Group and UNIXGroup objects
215
+ # cannot be removed if they are the primary Windows group of any user in
216
+ # Active Directory nor if they are the UNIX main group for any UNIXUser objects
217
+ # in Active Directory. When calling the Container#remove_group method, checks
218
+ # are made for all the objects RADUM knows about. When calling AD#sync checks
219
+ # are made for all users in Active Directory.
220
+ #
221
+ # It is also possible to destroy a Group or UNIXGroup object. Destroying a
222
+ # Group or UNIXGroup does not remove the object from Active Directory. Instead
223
+ # it removes all references to the object in RADUM itself. The same checks are
224
+ # made against all objects RADUM knows about before destroying the object can
225
+ # succeed. See the Container#destroy_group method documentation for more
226
+ # details.
227
+ #
228
+ # In both cases, any external references ot the orginal object should be
229
+ # discarded.
230
+ #
231
+ # == Converting Group and UNIXGroup Objects
232
+ #
233
+ # It is possible to convert Group objects to UNIXGroup objects and vice versa
234
+ # under certain conditions. Group objects can converted to UNIXGroup objects
235
+ # if the Group is not the primary Windows group for a user RADUM knows about.
236
+ # This is not really a strict requirement for users in Active Directory, but
237
+ # it is a limitation of the current RADUM implementation. UNIXGroup objects
238
+ # can be converted to Group objects with the same implementation restriction
239
+ # with the additional restriction that the UNIXGroup cannot be the UNIX main
240
+ # group for any user in Active Directory. Conversion causes this check to be
241
+ # made outside of the AD#load and AD#sync methods immediately before proceeding.
242
+ # If the conversion is successful, a new object of the desired type is created
243
+ # and placed into the RADUM system in place of the original object. Any external
244
+ # references to the orginal object should be discarded as they will be
245
+ # treated as if they were removed objects, even though they are not removed
246
+ # from Active Directory. Note that UNIX attributes are removed from UNIXGroup
247
+ # objects in Active Directory immediately before AD#sync when converted to
248
+ # Group objects.
249
+ #
250
+ # == Creating User and UNIXUser Objects
251
+ #
252
+ # User objects represent Windows users and UNIXUser objects represent UNIX
253
+ # users, which are Windows users that have UNIX attributes. Examples include:
254
+ #
255
+ # RADUM::User.new :username => 'martin',
256
+ # :container => faculty,
257
+ # :primary_group => ad.find_group_by_name('Domain Users')
258
+ # RADUM::UNIXUser.new :username => 'rowland',
259
+ # :container => staff,
260
+ # :primary_group => ad.find_group_by_name('Domain Users'),
261
+ # :uid => 5437,
262
+ # :unix_main_group => unix_staff,
263
+ # :shell => '/bin/bash',
264
+ # :home_directory => '/home/rowland'
265
+ #
266
+ # There are additional argumements in both cases. See User.new and
267
+ # UNIXUser.new for additional details. In both cases, the :rid argument should
268
+ # only be used by AD#load to specify the user object's relative identifier,
269
+ # which is based on the LDAP objectSid attribute. There is an :nis_domain
270
+ # argument for UNIXUser objects that defaults to "radum" if not specified.
271
+ # NIS is not required for using Active Directory in a centralized authentication
272
+ # design. One could access the UNIX attributes through LDAP directly for
273
+ # example. However, the NIS domain needs to be present if one wants to use
274
+ # the Active Directory Users and Computers GUI tool, therefore one is specified
275
+ # by default. Of course, this should still be set correctly in order to work
276
+ # in the Active Directory Users and Computers GUI tool, but it can be changed
277
+ # there easily and the attributes should still show up correctly. The :disabled
278
+ # argument applies to both User and UNIXUser objects. The default value is
279
+ # false. The password attribute for User and UNIXUser objects is nil unless
280
+ # set, and when set it causes the user in Active Directory to have that
281
+ # password if it meets the Group Policy password requirements. Once set through
282
+ # AD#sync, the password attribute is set to nil again. If there is no password
283
+ # set when the user is created in Active Directory, a random password that
284
+ # probably meets the Group Policy password requirements will be generated, and
285
+ # the user will not be forced to change that password unless
286
+ # User#force_change_password is also called before calling AD#sync. The
287
+ # GID value for a UNIXUser comes from its :unix_main_group argument UNIXGroup
288
+ # value. There are many attributes that can be set for User and UNIXUser
289
+ # objects. See the User and UNIXUser class documentation for more details.
290
+ #
291
+ # There is a useful method for finding the next available UID for the UNIXUser
292
+ # object's :uid argument:
293
+ #
294
+ # uid = ad.load_next_uid
295
+ #
296
+ # The AD#load_next_uid method searches Active Directory and the current RADUM
297
+ # objects for the next UID value that can be used.
298
+ #
299
+ # == Removing User and UNIXUser Objects
300
+ #
301
+ # User and UNIXUser objects can be removed from RADUM by using the following
302
+ # method for the Container object that they belong to:
303
+ #
304
+ # faculty.remove_user ad.find_user_by_username('martin')
305
+ #
306
+ # or by specifying a reference to a User or UNIXUser object directly.
307
+ #
308
+ # Removing a user causes that user to be removed from Active Directory when
309
+ # AD#sync is called.
310
+ #
311
+ # It is also possible to destroy a User or UNIXUser object. Destroying a
312
+ # User or UNIXUser does not remove the object from Active Directory. Instead
313
+ # it removes all references to the object in RADUM itself.
314
+ #
315
+ # In both cases, any external references ot the orginal object should be
316
+ # discarded.
317
+ #
318
+ # == Converting User and UNIXUser Objects
319
+ #
320
+ # It is possible to convert User objects to UNIXUser objects and vice versa.
321
+ # If the conversion is successful, a new object of the desired type is created
322
+ # and placed into the RADUM system in place of the original object. Any external
323
+ # references to the orginal object should be discarded as they will be
324
+ # treated as if they were removed objects, even though they are not removed
325
+ # from Active Directory. Note that UNIX attributes are removed from UNIXUser
326
+ # objects in Active Directory immediately before AD#sync when converted to
327
+ # User objects.
328
+ #
329
+ # = Synchronizing to Active Directory
330
+ #
331
+ # After making any changes, synchronizing those changes to Active Directory can
332
+ # be accomplished with the following:
333
+ #
334
+ # ad.sync
335
+ #
336
+ # This method can be called whenever changes are made. Changes in RADUM are
337
+ # considered authoritative and Active Directory objects are updated to reflect
338
+ # their attributes as RADUM sees the world. This is true for group memberships
339
+ # except for members that RADUM does not know about. This means that AD#sync
340
+ # will not cause users and groups it does not know about to be removed from
341
+ # a Group or UNIXGroup it does know about, but it will remove users and groups
342
+ # that have been explicitly removed in the RADUM system.
343
+ #
344
+ # = Logging
345
+ #
346
+ # The RADUM module instantiates an object of the Logger class. This can be used
347
+ # to log operational progress:
348
+ #
349
+ # RADUM::logger.log("\nInitializing...\n\n", RADUM::LOG_DEBUG)
350
+ #
351
+ # The RADUM::LOG_DEBUG level includes a lot of verbose progress information
352
+ # that can be highly useful. The log level argument is optional and defaults
353
+ # to RADUM::LOG_NORMAL. See the Logger class documentation for more details.
354
+ # Note that log output can also be sent to a file.
355
+ #
356
+ # = Windows Server Versions
357
+ #
358
+ # RADUM has been exclusively tested against Windows Server 2008 and Windows
359
+ # Server 2003 R2 Standard Edition SP2. The testing system had Microsoft
360
+ # Identity Management for UNIX installed and had the Certificate Services Role
361
+ # added, which is added using Add/Remove Programs in Windows Server 2003.
362
+ # Microsoft Identity Management for UNIX is generally required for editing
363
+ # UNIXUser and UNIXGroup objects in the Active Directory Users and Computers
364
+ # GUI tool if desired, and the Certificate Services Role is required in the
365
+ # domain because SSL is required for creating User and UNIXUser objects using
366
+ # LDAP. The LDAP attributes required for UNIXUser objects don't necessarily
367
+ # require Microsoft Identity Management for UNIX to be installed.
368
+ #
369
+ # RADUM works with a domain functional level of Windows 2003 Server or higher.
370
+ # Most of RADUM will work with a domain functional level of Windows 2000 native
371
+ # except for Universal group types and groups being members of groups. If those
372
+ # features are not needed, the Windows 2000 native domain functional level can
373
+ # be used, but RADUM assumes these features are possible, so one would need to
374
+ # ensure they do not use them if working at a domain functional level lower
375
+ # than Windows Server 2003. RADUM was not tested at a domain functional level
376
+ # lower than Windows 2000 native.
377
+ #
378
+ # Author:: Shaun Rowland <mailto:rowand@shaunrowland.com>
379
+ # Copyright:: Copyright 2009 Shaun Rowland. All rights reserved.
380
+ # License:: BSD License included in the project LICENSE file.
381
+
382
+ module RADUM
383
+ # Group type constants.
384
+ #
385
+ # These are the Fixnum representations of what should be Bignum objects in
386
+ # some cases as far as I am aware. In the Active Directory Users and Groups
387
+ # GUI tool, they are shown as hexidecimal values, indicating they should be
388
+ # Bignums (well, some of them). However, if you try to edit the values in
389
+ # that tool (with advanced attribute editing enabled or with the ADSI Edit
390
+ # tool) these show up as the Fixnum values here. We are going to stick with
391
+ # that, even though it is lame. I could not pull these out as Bignum objects.
392
+ # Some of these are small enough to be Fixnums though, so I left them as their
393
+ # hexidecimal values. These values correspond to the LDAP groupType attribute
394
+ # for group objects.
395
+ GROUP_DOMAIN_LOCAL_SECURITY = -2147483644
396
+ GROUP_DOMAIN_LOCAL_DISTRIBUTION = 0x4
397
+ GROUP_GLOBAL_SECURITY = -2147483646
398
+ GROUP_GLOBAL_DISTRIBUTION = 0x2
399
+ GROUP_UNIVERSAL_SECURITY = -2147483640
400
+ GROUP_UNIVERSAL_DISTRIBUTION = 0x8
401
+
402
+ # Some useful constants from lmaccess.h for use with creating user accounts.
403
+ UF_ACCOUNTDISABLE = 0x0002;
404
+ UF_PASSWD_NOTREQD = 0x0020;
405
+ UF_PASSWD_CANT_CHANGE = 0x0040;
406
+ UF_NORMAL_ACCOUNT = 0x0200;
407
+ # I don't think these are working - I am sure the last one doesn't work.
408
+ #UF_DONT_EXPIRE_PASSWD = 0x10000;
409
+ #UF_PASSWORD_EXPIRED = 0x800000;
410
+
411
+ # This is a convenience method to return a String representation of a
412
+ # Group or UNIXGroup object's type attribute, which has the value of one of
413
+ # the RADUM group type constants.
414
+ def RADUM.group_type_to_s(type) # :nodoc:
415
+ case type
416
+ when GROUP_DOMAIN_LOCAL_SECURITY
417
+ "GROUP_DOMAIN_LOCAL_SECURITY"
418
+ when GROUP_DOMAIN_LOCAL_DISTRIBUTION
419
+ "GROUP_DOMAIN_LOCAL_DISTRIBUTION"
420
+ when GROUP_GLOBAL_SECURITY
421
+ "GROUP_GLOBAL_SECURITY"
422
+ when GROUP_GLOBAL_DISTRIBUTION
423
+ "GROUP_GLOBAL_DISTRIBUTION"
424
+ when GROUP_UNIVERSAL_SECURITY
425
+ "GROUP_UNIVERSAL_SECURITY"
426
+ when GROUP_UNIVERSAL_DISTRIBUTION
427
+ "GROUP_UNIVERSAL_DISTRIBUTION"
428
+ else "UNKNOWN"
429
+ end
430
+ end
431
+
432
+ # The AD class represents the Active Directory. All opeartions that involve
433
+ # communication between the Container, User, UNIXUser, Group, and UNIXGroup
434
+ # objects and Active Directory are handled by the AD object. The AD object
435
+ # should be the first object created, generally followed by Container objects.
436
+ # The Container object requires an AD object. All other objects require a
437
+ # Container object.
438
+ class AD
439
+ # A handle the the Net::LDAP object used for this AD.
440
+ attr_reader :ldap
441
+ # The root of the Active Directory. This is a String representing an LDAP
442
+ # path, such as "dc=example,dc=com".
443
+ attr_reader :root
444
+ # The domain name of the Active Directory. This is calculated from the root
445
+ # attribute. This is automatically made lowercase.
446
+ attr_reader :domain
447
+ # The Active Directory user used to connect to the Active Directory. This
448
+ # is specified using an LDAP path to the user account, without the root
449
+ # component, such as "cn=Administrator,cn=Users". This defaults to
450
+ # "cn=Administrator,cn=Users" when an AD is created using AD.new.
451
+ attr_reader :user
452
+ # The server hostname or IP address of the Active Directory server. This
453
+ # defaults to "localhost" when an AD is created using AD.new.
454
+ attr_reader :server
455
+ # The minimum UID value to use if no other UIDs are found. This defaults to
456
+ # 1000.
457
+ attr_accessor :min_uid
458
+ # The array of UID values from UNIXUser objects in the AD object. This is
459
+ # automatically managed by the other objects and should not be modified
460
+ # directly.
461
+ attr_accessor :uids
462
+ # The minimum GID value to use if no other GIDs are found. This defaults to
463
+ # 1000.
464
+ attr_accessor :min_gid
465
+ # The array of GID values from UNIXGroup objects in the AD object. This is
466
+ # automatically managed by the other objects and should not be modified
467
+ # directly.
468
+ attr_accessor :gids
469
+ # The array of RID values for User, UNIXUser, Group, and UNIXGroup objects
470
+ # in the AD object. This is automatically managed by the other objects and
471
+ # should not be modified directly.
472
+ attr_accessor :rids
473
+ # The array of Containers in the AD object.
474
+ attr_reader :containers
475
+ # The array of Containers set for removal in the AD object.
476
+ attr_reader :removed_containers
477
+
478
+ # Create a new AD object that represents an Active Directory environment.
479
+ # This method takes a Hash containing arguments, some of which are required
480
+ # and others optional. The supported arguments follow:
481
+ #
482
+ # * :root => The root of the Active Directory [required]
483
+ # * :user => The user for an LDAP bind [default "cn=Administrator,cn=Users"]
484
+ # * :password => The user password for an LDAP bind [optional]
485
+ # * :server => The Active Directory server host [default "localhost"]
486
+ #
487
+ # RADUM requires TLS to create user accounts in Active Directory properly,
488
+ # so you will need to make sure you have a certificate server so that you
489
+ # can connect with SSL on port 636. An example instantiation follows:
490
+ #
491
+ # ad = RADUM::AD.new :root => 'dc=example,dc=com',
492
+ # :user => 'cn=Administrator,cn=Users',
493
+ # :password => 'password',
494
+ # :server => '192.168.1.1'
495
+ #
496
+ # The :user argument specifies the path to the user account in Active
497
+ # Directory equivalent to the distinguished_name attribute for the user
498
+ # without the :root portion. The :server argument can be an IP address
499
+ # or a hostname. The :root argument is required. If it is not specified,
500
+ # a RuntimeError is raised. Extraneous spaces are removed from the :root
501
+ # and :user arguments. They can have spaces, but extra spaces after any
502
+ # "," characters are removed automatically along with leading and traling
503
+ # white space.
504
+ #
505
+ # === Parameter Types
506
+ #
507
+ # * :root [String]
508
+ # * :user [String]
509
+ # * :password [String]
510
+ # * :server [String]
511
+ #
512
+ # A Container object for "cn=Users" is automatically created and added to
513
+ # the AD object when it is created. This is meant to be a convenience
514
+ # because most, if not all, User and UNIXUser objects will have the
515
+ # "Domain Users" Windows group as their primary Windows group. It is
516
+ # impossible to remove this Container.
517
+ def initialize(args = {})
518
+ @root = args[:root] or raise "AD :root argument required."
519
+ @root.gsub!(/,\s+/, ",")
520
+ @root.strip!
521
+ @domain = @root.gsub(/[Dd][Cc]=/, "").gsub(/,/, ".").downcase
522
+ @user = args[:user] || "cn=Administrator,cn=Users"
523
+ @user.gsub!(/,\s+/, ",")
524
+ @user.strip!
525
+ @password = args[:password]
526
+ @server = args[:server] || "localhost"
527
+ @containers = []
528
+ @removed_containers = []
529
+ @min_uid = 1000
530
+ @uids = []
531
+ @min_gid = 1000
532
+ @gids = []
533
+ # RIDs are in a flat namespace, so there's no need to keep track of them
534
+ # for user or group objects specifically, just in the directory overall.
535
+ @rids = []
536
+ @port = 636
537
+
538
+ @ldap = Net::LDAP.new :host => @server,
539
+ :port => @port,
540
+ :encryption => :simple_tls,
541
+ :auth => {
542
+ :method => :simple,
543
+ :username => @user + "," + @root,
544
+ :password => @password
545
+ }
546
+
547
+ # We add the cn=Users container by default because it is highly likely
548
+ # that users have the Domain Users Windows group as their Windows
549
+ # primary group. If we did not do this, there would likely be a ton
550
+ # of warning messages in the load() method. Keep in mind that containers
551
+ # automatically add themselves to their AD object.
552
+ @cn_users = Container.new :name => "cn=Users", :directory => self
553
+ end
554
+
555
+ # The port number used to communicate with the Active Directory server.
556
+ def port
557
+ @port
558
+ end
559
+
560
+ # Set the port number used to communicate with the Active Directory server.
561
+ # This defaults to 636 for TLS in order to create user accounts properly,
562
+ # but can be set here for nonstandard configurations.
563
+ #
564
+ # === Parameter Types
565
+ #
566
+ # * port [integer]
567
+ def port=(port)
568
+ @port = port
569
+ @ldap.port = port
570
+ end
571
+
572
+ # Find a Container in the AD by name. The search is case-insensitive. The
573
+ # Container is returned if found, otherwise nil is returned.
574
+ #
575
+ # === Parameter Types
576
+ #
577
+ # * name [String]
578
+ def find_container(name)
579
+ @containers.find do |container|
580
+ # This relies on the fact that a container name must be unique in a
581
+ # directory.
582
+ container.name.downcase == name.downcase
583
+ end
584
+ end
585
+
586
+ # Add a Container a container to the AD. The Container must have the AD
587
+ # as its directory attribute or a RuntimeError is raised. Container objects
588
+ # that were removed cannot be added back and are ignored.
589
+ #
590
+ # === Parameter Types
591
+ #
592
+ # * container [Container]
593
+ def add_container(container) # :nodoc:
594
+ return if container.removed?
595
+
596
+ if self == container.directory
597
+ # We don't want to add the Container more than once.
598
+ @containers.push container unless @containers.include?(container)
599
+ @removed_containers.delete container
600
+ else
601
+ raise "Container must be in the same directory."
602
+ end
603
+ end
604
+
605
+ # Remove a Container from the AD. This attempts to set the Container
606
+ # object's removed attribute to true as well as remove any User, UNIXUser,
607
+ # Group, and UNIXGroup objects it contains. If any Group or UNIXGroup
608
+ # object cannot be removed because it is a dependency, the Container cannot
609
+ # be fully removed either, but all objects that can be removed will be
610
+ # removed. This can happen if a Group or UNIXGroup is another User or
611
+ # UNIXUser object's primary Windows group or UNIX main group and that User
612
+ # or UNIXUser is not in the same Container. Removed Container objects are
613
+ # ignored.
614
+ #
615
+ # Note that this method might succeed based on the user and group objects
616
+ # it knows about, but it still might fail when AD#sync is called because a
617
+ # more extensive Active Directory search will be performed at that point.
618
+ # In any case, all users will be removed and all groups (and the Container)
619
+ # if possible. This method is greedy in that it tries to remove as many
620
+ # objects from Active Directory as possible.
621
+ #
622
+ # This method refuses to remove the "cn=Users" container as a safety
623
+ # measure. There is no error raised in this case, but a warning is logged
624
+ # using RADUM::logger with a log level of LOG_NORMAL.
625
+ #
626
+ # Any external reference to the Container should be discarded unless it
627
+ # was not possible to fully remove the Container. This method returns a
628
+ # boolean that indicates if it was possible to fully remove the Container.
629
+ #
630
+ # === Parameter Types
631
+ #
632
+ # * container [Container]
633
+ def remove_container(container)
634
+ return if container.removed?
635
+
636
+ if container == @cn_users
637
+ RADUM::logger.log("Cannot remove #{container.name} - safety measure.",
638
+ LOG_NORMAL)
639
+ return false
640
+ end
641
+
642
+ can_remove = true
643
+
644
+ # The next two steps clone the arrays because we are modifying them while
645
+ # iterting over them at the same time of course.
646
+ container.users.clone.each do |user|
647
+ container.remove_user user
648
+ end
649
+
650
+ container.groups.clone.each do |group|
651
+ begin
652
+ container.remove_group group
653
+ rescue RuntimeError => error
654
+ RADUM::logger.log(error, LOG_NORMAL)
655
+ can_remove = false
656
+ end
657
+ end
658
+
659
+ @containers.each do |current_container|
660
+ next if current_container == container
661
+
662
+ if current_container.name =~ /#{container.name}/
663
+ RADUM::logger.log("Container #{container.name} contains container " +
664
+ "#{current_container.name}.", LOG_NORMAL)
665
+ can_remove = false
666
+ end
667
+ end
668
+
669
+ if can_remove
670
+ @containers.delete container
671
+
672
+ unless @removed_containers.include?(container)
673
+ @removed_containers.push container
674
+ end
675
+
676
+ container.set_removed
677
+ else
678
+ RADUM::logger.log("Cannot fully remove container #{container.name}.",
679
+ LOG_NORMAL)
680
+ end
681
+
682
+ can_remove
683
+ end
684
+
685
+ # Destroy all references to a Container. This also destroys any references
686
+ # to User, UNIXUser, Group, and UNIXGroup objects the Container owns. This
687
+ # can fail if any of the Group or UNIXGroup objects are the primary Windows
688
+ # group or UNIX main group for any User or UNIXUser objects RADUM knows
689
+ # about.
690
+ #
691
+ # This method refuses to destroy the "cn=Users" container as a safety
692
+ # measure. There is no error raised in this case, but a warning is logged
693
+ # using RADUM::logger with a log level of LOG_NORMAL.
694
+ #
695
+ # Any external reference to the Container should be discarded unless it
696
+ # was not possible to fully destroy the Container. This method returns a
697
+ # boolean that indicates if it was possible to fully destroy the Container.
698
+ #
699
+ # === Parameter Types
700
+ #
701
+ # * container [Container]
702
+ def destroy_container(container)
703
+ # We have to allow removed Container objects to be destroyed because
704
+ # that's the only way they are deleted from the RADUM environment
705
+ # when they are deleted from Active Directory.
706
+ if container == @cn_users
707
+ RADUM::logger.log("Cannot destroy #{container.name} - safety measure.",
708
+ LOG_NORMAL)
709
+ return
710
+ end
711
+
712
+ can_destroy = true
713
+
714
+ # The next two steps clone the arrays because we are modifying them while
715
+ # iterting over them at the same time of course.
716
+ container.users.clone.each do |user|
717
+ container.destroy_user user
718
+ end
719
+
720
+ container.groups.clone.each do |group|
721
+ begin
722
+ container.destroy_group group
723
+ rescue RuntimeError => error
724
+ RADUM::logger.log(error, LOG_NORMAL)
725
+ can_destroy = false
726
+ end
727
+ end
728
+
729
+ # There is no need to worry about sub-container objects.
730
+
731
+ if can_destroy
732
+ @containers.delete container
733
+ # Removed Containers can be destroyed as well, so we want to make sure
734
+ # all references are removed.
735
+ @removed_containers.delete container
736
+ container.set_removed
737
+ end
738
+
739
+ can_destroy
740
+ end
741
+
742
+ # Returns an Array of all User and UNIXUser objects in the AD.
743
+ def users
744
+ all_users = []
745
+
746
+ @containers.each do |container|
747
+ all_users += container.users
748
+ end
749
+
750
+ all_users
751
+ end
752
+
753
+ # Returns an Array of all removed User and UNIXUser objects in the AD.
754
+ def removed_users
755
+ all_removed_users = []
756
+
757
+ @containers.each do |container|
758
+ all_removed_users += container.removed_users
759
+ end
760
+
761
+ # We also need to check removed Containers too because they can have
762
+ # removed users too.
763
+ @removed_containers.each do |container|
764
+ all_removed_users += container.removed_users
765
+ end
766
+
767
+ all_removed_users
768
+ end
769
+
770
+ # Find a User or UNIXUser in the AD object. The first argument is a boolean
771
+ # that indicates if the search should be for removed users. It is optional
772
+ # and defaults to false. The second required argument is a block.
773
+ # The block is passed each User and UNIXUser object and contains the
774
+ # desired testing expression. Attributes for either User or UNIXUser
775
+ # objects can be used without worrying about the accessor methods existing.
776
+ # Examples follow:
777
+ #
778
+ # * Find a User or UNIXUser by username:
779
+ # find_user { |user| user.username == "username" }
780
+ # * Find a UNIXUser by gid:
781
+ # find_user { |user| user.gid == 1002 }
782
+ # * Find a removed User or UNIXUser by username:
783
+ # find_user(true) { |user| user.username == "username" }
784
+ # * Find a removed UNIXUser by gid:
785
+ # find_user(true) { |user| user.gid == 1002 }
786
+ #
787
+ # If no block is given the method returns nil. If the User or UNIXUser is
788
+ # not found, the method also returns nil, otherwise the User or UNIXUser
789
+ # object found is returned.
790
+ #
791
+ # There are convenient find_user_by_<attribute> methods defined that take
792
+ # care of the example cases here as well, but this method allows any block
793
+ # test to be given.
794
+ #
795
+ # === Parameter Types
796
+ #
797
+ # * removed [boolean]
798
+ # * [block]
799
+ def find_user(removed = false)
800
+ if block_given?
801
+ if removed
802
+ search_users = removed_users
803
+ else
804
+ search_users = users
805
+ end
806
+
807
+ found = search_users.find do |user|
808
+ begin
809
+ yield user
810
+ rescue NoMethodError
811
+ end
812
+ end
813
+
814
+ return found if found
815
+ return nil
816
+ end
817
+
818
+ return nil
819
+ end
820
+
821
+ # Find a User or UNIXUser in the AD by username. The search is
822
+ # case-insensitive. The User or UNIXUser is returned if found, otherwise
823
+ # nil is returned. Specify the second argument as true if you wish to
824
+ # search for removed User or UNIXUser objects.
825
+ #
826
+ # === Parameter Types
827
+ #
828
+ # * username [String]
829
+ # * removed [boolean]
830
+ def find_user_by_username(username, removed = false)
831
+ find_user removed do |user|
832
+ user.username.downcase == username.downcase
833
+ end
834
+ end
835
+
836
+ # Find a User or UNIXUser in the AD by RID. The User or UNIXUser is
837
+ # returned if found, otherwise nil is returned. Specify the second argument
838
+ # as true if you wish to search for removed User or UNIXUser objects.
839
+ #
840
+ # === Parameter Types
841
+ #
842
+ # * rid [integer]
843
+ # * removed [boolean]
844
+ def find_user_by_rid(rid, removed = false)
845
+ find_user(removed) { |user| user.rid == rid }
846
+ end
847
+
848
+ # Find a UNIXUser in the AD by UID. The UNIXUser is returned if found,
849
+ # otherwise nil is returned. Specify the second argument as true if you
850
+ # wish to search for removed UNIXUser objects.
851
+ #
852
+ # === Parameter Types
853
+ #
854
+ # * uid [integer]
855
+ # * removed [boolean]
856
+ def find_user_by_uid(uid, removed = false)
857
+ find_user(removed) { |user| user.uid == uid }
858
+ end
859
+
860
+ # Find a User or UNIXUser in the AD by distinguished name. The search is
861
+ # case-insensitive. The User or UNIXUser is returned if found, otherwise
862
+ # nil is returned. Specify the second argument as true if you wish to
863
+ # search for removed User or UNIXUser objects.
864
+ #
865
+ # === Parameter Types
866
+ #
867
+ # * dn [String]
868
+ # * removed [boolean]
869
+ def find_user_by_dn(dn, removed = false)
870
+ find_user removed do |user|
871
+ user.distinguished_name.downcase == dn.downcase
872
+ end
873
+ end
874
+
875
+ # Convert a User to a UNIXUser. This method returns the new UNIXUser
876
+ # if successful, otherwise there will be a RuntimeError somewhere first.
877
+ # The original User is destroyed with Container#destroy_user and a new
878
+ # UNIXUser is created with the same information. New UNIX attributes will
879
+ # be added to Active Directory. Any external references to the old User
880
+ # should be discarded and replaced with the new UNIXUser object returned.
881
+ # Supported arguments follow:
882
+ #
883
+ # * :user => The User to convert to a UNIXUser [required]
884
+ # * :uid => The UNIXUser UID attribute [required]
885
+ # * :unix_main_group => The UNIXUser object's UNIX main group [required]
886
+ # * :shell => The UNIXUser shell attribute [required]
887
+ # * :home_directory => The UNIXUser home directory attribute [required]
888
+ # * :nis_domain => The UNIXUser NIS domain attribute [default "radum"]
889
+ #
890
+ # The :uid argument specifies the UNIX UID value of the UNIXUser. The
891
+ # :unix_main_group argument must be a UNIXGroup object or a RuntimeError
892
+ # is raised. The :nis_domain defaults to "radum". The use of an NIS domain
893
+ # is not strictly required as one could simply set the right attributes in
894
+ # Active Directory and use LDAP on clients to access that data, but
895
+ # specifying an NIS domain allows for easy editing of UNIX attributes
896
+ # using the GUI tools in Windows, thus the use of a default value.
897
+ #
898
+ # === Parameter Types
899
+ #
900
+ # * :user [User]
901
+ # * :uid [integer]
902
+ # * :unix_main_group [UNIXGroup]
903
+ # * :shell [String]
904
+ # * :home_directory [String]
905
+ # * :nis_domain [String]
906
+ #
907
+ # If the :user argument is not a User object, a RuntimeError is raised.
908
+ # If the User has already been removed a RuntimeError is raised.
909
+ # The :uid argument is also checked first in case it already is in use
910
+ # so that the original user is not destroyed by accident due to an error.
911
+ #
912
+ # In this case, unlike when simply creating a UNIXUser object, all UIDs
913
+ # are checked, including those already in Active Directory.
914
+ #
915
+ # No changes to the User happen until AD#sync is called.
916
+ def user_to_unix_user(args = {})
917
+ user = args[:user]
918
+
919
+ # Make sure we are working with a User object only.
920
+ unless user.instance_of?(User)
921
+ raise ":user argument just be a User object."
922
+ end
923
+
924
+ if user.removed?
925
+ raise ":user has been removed."
926
+ end
927
+
928
+ uid = args[:uid]
929
+ all_uids = load_ldap_uids
930
+
931
+ # The UID must be unique.
932
+ if (all_uids + @uids).include?(uid)
933
+ raise "UID #{uid} is already in use in the directory."
934
+ end
935
+
936
+ unix_main_group = args[:unix_main_group]
937
+
938
+ unless unix_main_group.instance_of?(UNIXGroup)
939
+ raise ":unix_main_group is not a UNIXGroup object."
940
+ end
941
+
942
+ shell = args[:shell]
943
+ home_directory = args[:home_directory]
944
+ nis_domain = args[:nis_domain]
945
+ # User attributes.
946
+ username = user.username
947
+ container = user.container
948
+ primary_group = user.primary_group
949
+ disabled = user.disabled?
950
+ rid = user.rid
951
+ first_name = user.first_name
952
+ initials = user.initials
953
+ middle_name = user.middle_name
954
+ surname = user.surname
955
+ script_path = user.script_path
956
+ profile_path = user.profile_path
957
+ local_path = user.local_path
958
+ local_drive = user.local_drive
959
+ password = user.password
960
+ must_change_password = user.must_change_password?
961
+ groups = user.groups.clone
962
+ removed_groups = user.removed_groups.clone
963
+ loaded = user.loaded?
964
+
965
+ # Destroy the user now that we have its information.
966
+ container.destroy_user user
967
+ user = UNIXUser.new :username => username, :container => container,
968
+ :primary_group => primary_group,
969
+ :disabled => disabled, :rid => rid, :uid => uid,
970
+ :unix_main_group => unix_main_group,
971
+ :shell => shell, :home_directory => home_directory,
972
+ :nis_domain => nis_domain
973
+
974
+ # Set the user to loaded if it was loaded orginally. This sets the
975
+ # modified attribute to false, but the actions below will ensure the
976
+ # modified attribute is actually true when we are done, which is required
977
+ # in order to update the attributes in Active Directory through AD#sync.
978
+ user.set_loaded if loaded
979
+
980
+ # Set other User attributes.
981
+ user.first_name = first_name
982
+ user.initials = initials
983
+ user.middle_name = middle_name
984
+ user.surname = surname
985
+ user.script_path = script_path
986
+ user.profile_path = profile_path
987
+
988
+ # Figure out the Windows home directory type attributes.
989
+ if local_path && local_drive
990
+ user.connect_drive_to local_drive, local_path
991
+ elsif local_path
992
+ user.local_path = local_path
993
+ end
994
+
995
+ user.password = password
996
+
997
+ if must_change_password
998
+ user.force_change_password
999
+ end
1000
+
1001
+ (groups + removed_groups).each do |group_member|
1002
+ user.add_group group_member
1003
+ end
1004
+
1005
+ removed_groups.each do |group_member|
1006
+ user.remove_group group_member
1007
+ end
1008
+
1009
+ user
1010
+ end
1011
+
1012
+ # Convert a UNIXUser to a User. This method returns the new User
1013
+ # if successful, otherwise there will be a RuntimeError somewhere first.
1014
+ # The original UNIXUser is destroyed with Container#destroy_user and a
1015
+ # new User is created with the same information where applicable. Old
1016
+ # UNIX attributes will be removed from Active Directory if possible
1017
+ # immediately (not waiting on AD#sync because AD#sync will think this is
1018
+ # now only a User object with no UNIX attributes). Any external references
1019
+ # to the old UNIXUser should be discarded and replaced with the new User
1020
+ # object returned. Supported arguments follow:
1021
+ #
1022
+ # * :user => The UNIXUser to convert to a User [required]
1023
+ # * :remove_unix_groups => Remove UNIXGroup memberships [default false]
1024
+ #
1025
+ # The :user argument is the UNIXUser to convert. If the :user argument
1026
+ # is not a UNIXUser object, a RuntimeError is raised. If the UNIXUser
1027
+ # has already been removed a RuntimeError is raised. The :remove_unix_groups
1028
+ # argument is a boolean flag that determines if the new User object should
1029
+ # continue to be a member of the UNIXGroup objects it was previously.
1030
+ # UNIXUser objects are members of their UNIXGroup objects from the Windows
1031
+ # perspective by default because they are members from the UNIX perspective.
1032
+ # This is the default behavior in RADUM. The default action is to not remove
1033
+ # their Windows group memberships when converting a UNIXUser to a User.
1034
+ #
1035
+ # === Parameter Types
1036
+ #
1037
+ # * :user [UNIXUser]
1038
+ # * :remove_unix_groups [boolean]
1039
+ #
1040
+ # UNIX attributes are removed from Active Directory immedately if it is
1041
+ # actually possible to destroy the UNIXUser properly without waiting for
1042
+ # AD#sync to be called.
1043
+ def unix_user_to_user(args = {})
1044
+ RADUM::logger.log("[AD #{self.root}] entering unix_user_to_user()",
1045
+ LOG_DEBUG)
1046
+ user = args[:user]
1047
+
1048
+ # Make sure we are working with a UNIXUser object only.
1049
+ unless user.instance_of?(UNIXUser)
1050
+ raise ":user argument just be a UNIXUser object."
1051
+ end
1052
+
1053
+ if user.removed?
1054
+ raise ":user has been removed."
1055
+ end
1056
+
1057
+ remove_unix_groups = args[:remove_unix_groups] || false
1058
+
1059
+ # User attributes.
1060
+ username = user.username
1061
+ container = user.container
1062
+ primary_group = user.primary_group
1063
+ # This is needed later if removal from UNIXGroup objects was requested.
1064
+ unix_main_group = user.unix_main_group
1065
+ disabled = user.disabled?
1066
+ rid = user.rid
1067
+ first_name = user.first_name
1068
+ initials = user.initials
1069
+ middle_name = user.middle_name
1070
+ surname = user.surname
1071
+ script_path = user.script_path
1072
+ profile_path = user.profile_path
1073
+ local_path = user.local_path
1074
+ local_drive = user.local_drive
1075
+ password = user.password
1076
+ must_change_password = user.must_change_password?
1077
+ groups = user.groups.clone
1078
+ removed_groups = user.removed_groups.clone
1079
+ loaded = user.loaded?
1080
+
1081
+ # Destroy the user now that we have its information.
1082
+ container.destroy_user user
1083
+
1084
+ # If the user was destroyed and we got this far, we need to remove
1085
+ # any of its UNIX attributes in Active Directory directly. We do need
1086
+ # to make sure it is actually there first of course.
1087
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
1088
+ found = @ldap.search(:base => user.distinguished_name,
1089
+ :filter => user_filter,
1090
+ :scope => Net::LDAP::SearchScope_BaseObject,
1091
+ :return_result => false)
1092
+
1093
+ unless found == false
1094
+ ops = [
1095
+ [:replace, :loginShell, nil],
1096
+ [:replace, :unixHomeDirectory, nil],
1097
+ [:replace, :msSFU30NisDomain, nil],
1098
+ [:replace, :gecos, nil],
1099
+ [:replace, :unixUserPassword, nil],
1100
+ [:replace, :shadowExpire, nil],
1101
+ [:replace, :shadowFlag, nil],
1102
+ [:replace, :shadowInactive, nil],
1103
+ [:replace, :shadowLastChange, nil],
1104
+ [:replace, :shadowMax, nil],
1105
+ [:replace, :shadowMin, nil],
1106
+ [:replace, :shadowWarning, nil],
1107
+ [:replace, :gidNumber, nil],
1108
+ [:replace, :uidNumber, nil]
1109
+ ]
1110
+
1111
+ RADUM::logger.log("\tRemoving user's previous UNIX attributes.",
1112
+ LOG_DEBUG)
1113
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
1114
+ @ldap.modify :dn => user.distinguished_name, :operations => ops
1115
+ check_ldap_result
1116
+ end
1117
+
1118
+ user = User.new :username => username, :container => container,
1119
+ :primary_group => primary_group, :disabled => disabled,
1120
+ :rid => rid
1121
+
1122
+ # Set the user to loaded if it was loaded orginally. This sets the
1123
+ # modified attribute to false, but the actions below will ensure the
1124
+ # modified attribute is actually true when we are done, which is required
1125
+ # in order to update the attributes in Active Directory through AD#sync.
1126
+ user.set_loaded if loaded
1127
+
1128
+ # Set other User attributes.
1129
+ user.first_name = first_name
1130
+ user.initials = initials
1131
+ user.middle_name = middle_name
1132
+ user.surname = surname
1133
+ user.script_path = script_path
1134
+ user.profile_path = profile_path
1135
+
1136
+ # Figure out the Windows home directory type attributes.
1137
+ if local_path && local_drive
1138
+ user.connect_drive_to local_drive, local_path
1139
+ elsif local_path
1140
+ user.local_path = local_path
1141
+ end
1142
+
1143
+ user.password = password
1144
+
1145
+ if must_change_password
1146
+ user.force_change_password
1147
+ end
1148
+
1149
+ (groups + removed_groups).each do |group_member|
1150
+ user.add_group group_member
1151
+ end
1152
+
1153
+ removed_groups.each do |group_member|
1154
+ user.remove_group group_member
1155
+ end
1156
+
1157
+ # An extra step to remove any UNIXGroup objects if that was requested.
1158
+ if remove_unix_groups
1159
+ user.groups.clone.each do |group|
1160
+ if group.instance_of?(UNIXGroup)
1161
+ # The user is not a member of their primary Windows group in the
1162
+ # groups array for the user, so no check is needed. In that case,
1163
+ # nothing happens.
1164
+ user.remove_group group
1165
+
1166
+ # Don't try to remove a membership in UNIX LDAP attributes that does
1167
+ # not exist.
1168
+ if group != unix_main_group
1169
+ # Immediately remove UNIX group membership through UNIX LDAP
1170
+ # attributes if the UNIXGroup is actually in Active Directory at
1171
+ # this point.
1172
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
1173
+ found = @ldap.search(:base => group.distinguished_name,
1174
+ :filter => group_filter,
1175
+ :scope => Net::LDAP::SearchScope_BaseObject,
1176
+ :return_result => false)
1177
+
1178
+ unless found == false
1179
+ ops = [
1180
+ [:delete, :memberUid, user.username],
1181
+ [:delete, :msSFU30PosixMember, user.distinguished_name]
1182
+ ]
1183
+
1184
+ RADUM::logger.log("\tRemoving UNIX LDAP attribute membership" +
1185
+ " for <#{group.name}>.", LOG_DEBUG)
1186
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
1187
+ @ldap.modify :dn => group.distinguished_name, :operations => ops
1188
+ check_ldap_result
1189
+ end
1190
+ end
1191
+ end
1192
+ end
1193
+ end
1194
+
1195
+ RADUM::logger.log("[AD #{self.root}] exiting unix_user_to_user()",
1196
+ LOG_DEBUG)
1197
+ user
1198
+ end
1199
+
1200
+ # Returns an Array of all Group and UNIXGroup objects in the AD.
1201
+ def groups
1202
+ all_groups = []
1203
+
1204
+ @containers.each do |container|
1205
+ all_groups += container.groups
1206
+ end
1207
+
1208
+ all_groups
1209
+ end
1210
+
1211
+ # Returns an Array of all removed Group and UNIXGroup objects in the AD.
1212
+ def removed_groups
1213
+ all_removed_groups = []
1214
+
1215
+ @containers.each do |container|
1216
+ all_removed_groups += container.removed_groups
1217
+ end
1218
+
1219
+ # We also need to check removed Containers too because they can have
1220
+ # removed groups too.
1221
+ @removed_containers.each do |container|
1222
+ all_removed_groups += container.removed_groups
1223
+ end
1224
+
1225
+ all_removed_groups
1226
+ end
1227
+
1228
+ # Find a Group or UNIXGroup in the AD object. The first argument is a
1229
+ # boolean that indicates if the search should be for removed groups. It is
1230
+ # optional and defaults to false. The second required argument is a block.
1231
+ # The block is passed each Group and UNIXGroup object and contains the
1232
+ # desired testing expression. Attributes for either Group or UNIXGroup
1233
+ # objects can be used without worrying about the accessor methods existing.
1234
+ # Examples follow:
1235
+ #
1236
+ # * Find a Group or UNIXGroup by name:
1237
+ # find_group { |group| group.name == "name" }
1238
+ # * Find a UNIXGroup by gid:
1239
+ # find_group { |group| group.gid == 1002 }
1240
+ # * Find a removed Group or UNIXGroup by name:
1241
+ # find_group(true) { |group| group.name == "name" }
1242
+ # * Find a removed UNIXGroup by gid:
1243
+ # find_group(true) { |group| group.gid == 1002 }
1244
+ #
1245
+ # If no block is given the method returns nil. If the Group or UNIXGroup is
1246
+ # not found, the method also returns nil, otherwise the Group or UNIXGroup
1247
+ # object found is returned.
1248
+ #
1249
+ # There are convenient find_group_by_<attribute> methods defined that take
1250
+ # care of the example cases here as well, but this method allows any block
1251
+ # test to be given.
1252
+ #
1253
+ # === Parameter Types
1254
+ #
1255
+ # * removed [boolean]
1256
+ # * [block]
1257
+ def find_group(removed = false)
1258
+ if block_given?
1259
+ if removed
1260
+ search_groups = removed_groups
1261
+ else
1262
+ search_groups = groups
1263
+ end
1264
+
1265
+ found = search_groups.find do |group|
1266
+ begin
1267
+ yield group
1268
+ rescue NoMethodError
1269
+ end
1270
+ end
1271
+
1272
+ return found if found
1273
+ return nil
1274
+ end
1275
+
1276
+ return nil
1277
+ end
1278
+
1279
+ # Find a Group or UNIXGroup in the AD by name. The search is
1280
+ # case-insensitive. The Group or UNIXGroup is returned if found, otherwise
1281
+ # nil is returned. Specify the second argument as true if you wish to
1282
+ # search for removed Group or UNIXGroup objects.
1283
+ #
1284
+ # === Parameter Types
1285
+ #
1286
+ # * name [String]
1287
+ # * removed [boolean]
1288
+ def find_group_by_name(name, removed = false)
1289
+ find_group removed do |group|
1290
+ group.name.downcase == name.downcase
1291
+ end
1292
+ end
1293
+
1294
+ # Find a Group or UNIXGroup in the AD by RID. The Group or UNIXGroup is
1295
+ # returned if found, otherwise nil is returned. Specify the second argument
1296
+ # as true if you wish to search for removed Group or UNIXGroup objects.
1297
+ #
1298
+ # === Parameter Types
1299
+ #
1300
+ # * rid [integer]
1301
+ # * removed [boolean]
1302
+ def find_group_by_rid(rid, removed = false)
1303
+ find_group(removed) { |group| group.rid == rid }
1304
+ end
1305
+
1306
+ # Find a UNIXGroup in the AD by GID. The UNIXGroup is returned if found,
1307
+ # otherwise nil is returned. Specify the second argument as true if you
1308
+ # wish to search for removed UNIXGroup objects.
1309
+ #
1310
+ # === Parameter Types
1311
+ #
1312
+ # * gid [integer]
1313
+ # * removed [boolean]
1314
+ def find_group_by_gid(gid, removed = false)
1315
+ find_group(removed) { |group| group.gid == gid }
1316
+ end
1317
+
1318
+ # Find a Group or UNIXGroup in the AD by distinguished name. The search is
1319
+ # case-insensitive. The Group or UNIXGroup is returned if found, otherwise
1320
+ # nil is returned. Specify the second argument as true if you wish to
1321
+ # search for removed Group or UNIXGroup objects.
1322
+ #
1323
+ # === Parameter Types
1324
+ #
1325
+ # * dn [String]
1326
+ # * removed [boolean]
1327
+ def find_group_by_dn(dn, removed = false)
1328
+ find_group removed do |group|
1329
+ group.distinguished_name.downcase == dn.downcase
1330
+ end
1331
+ end
1332
+
1333
+ # Convert a Group to a UNIXGroup. This method returns the new UNIXGroup
1334
+ # if successful, otherwise there will be a RuntimeError somewhere first.
1335
+ # The original Group is destroyed with Container#destroy_group and a new
1336
+ # UNIXGroup is created with the same information. New UNIX attributes will
1337
+ # be added to Active Directory. Any external references to the old Group
1338
+ # should be discarded and replaced with the new UNIXGroup object returned.
1339
+ # Supported arguments follow:
1340
+ #
1341
+ # * :group => The Group to convert to a UNIXGroup [required]
1342
+ # * :gid => The UNIXGroup GID attribute [required]
1343
+ # * :nis_domain => The UNIXGroup NIS domain attribute [default "radum"]
1344
+ #
1345
+ # The :gid argument specifies the UNIX GID value of the UNIXGroup. The
1346
+ # :nis_domain defaults to "radum". The use of an NIS domain is not
1347
+ # strictly required as one could simply set the right attributes in Active
1348
+ # Directory and use LDAP on clients to access that data, but specifying an
1349
+ # NIS domain allows for easy editing of UNIX attributes using the GUI tools
1350
+ # in Windows, thus the use of a default value.
1351
+ #
1352
+ # === Parameter Types
1353
+ #
1354
+ # * :group [Group]
1355
+ # * :gid [integer]
1356
+ # * :nis_domain [String]
1357
+ #
1358
+ # If the :group argument is not a Group object, a RuntimeError is raised.
1359
+ # If the Group has already been removed a RuntimeError is raised.
1360
+ # The :gid argument is also checked first in case it already is in use
1361
+ # so that the original group is not destroyed by accident due to an error.
1362
+ # Note that Container#destroy_group checks to make sure the group is not
1363
+ # the primary Windows group for any User or UNIXUser first, so the :group
1364
+ # must not be the primary Windows group for any users. If the group is
1365
+ # someone's primary Windows group a RuntimeError is raised. You will have
1366
+ # to modify it by hand in Active Directory if you want to convert it. The
1367
+ # primary Windows group check is not done for all users in Active Directory
1368
+ # because it is safe to convert unless RADUM thinks the Group is one of
1369
+ # its User or UNIXUser object's primary Windows group. In that specific
1370
+ # case, you'll have to modify the group in Active Directory by hand
1371
+ # unfortunately.
1372
+ #
1373
+ # In this case, unlike when simply creating a UNIXGroup object, all GIDs
1374
+ # are checked, including those already in Active Directory.
1375
+ #
1376
+ # No changes to the Group happen until AD#sync is called.
1377
+ def group_to_unix_group(args = {})
1378
+ group = args[:group]
1379
+
1380
+ # Make sure we are working with a Group object only.
1381
+ unless group.instance_of?(Group)
1382
+ raise ":group argument just be a Group object."
1383
+ end
1384
+
1385
+ if group.removed?
1386
+ raise ":group has been removed."
1387
+ end
1388
+
1389
+ gid = args[:gid]
1390
+ all_gids = load_ldap_gids
1391
+
1392
+ # The GID must be unique.
1393
+ if (all_gids + @gids).include?(gid)
1394
+ raise "GID #{gid} is already in use in the directory."
1395
+ end
1396
+
1397
+ nis_domain = args[:nis_domain]
1398
+ # Group attributes.
1399
+ name = group.name
1400
+ container = group.container
1401
+ type = group.type
1402
+ rid = group.rid
1403
+ users = group.users.clone
1404
+ groups = group.groups.clone
1405
+ removed_users = group.removed_users.clone
1406
+ removed_groups = group.removed_groups.clone
1407
+ loaded = group.loaded?
1408
+
1409
+ # Destroy the group now that we have its information. This will fail if
1410
+ # the group is someone's primary Windows group from the RADUM perspective
1411
+ # because of the logic I have to employ, but it is fine if the group is
1412
+ # some other user account's primary Windows group that RADUM doesn't
1413
+ # know about.
1414
+ container.destroy_group group
1415
+
1416
+ group = UNIXGroup.new :name => name, :container => container,
1417
+ :type => type, :rid => rid, :gid => gid,
1418
+ :nis_domain => nis_domain
1419
+
1420
+ # Set the group to loaded if it was loaded orginally. This sets the
1421
+ # modified attribute to false, but the actions below will ensure the
1422
+ # modified attribute is actually true when we are done, which is required
1423
+ # in order to update the attributes in Active Directory through AD#sync.
1424
+ group.set_loaded if loaded
1425
+
1426
+ (users + removed_users).each do |user_member|
1427
+ group.add_user user_member
1428
+ end
1429
+
1430
+ removed_users.each do |user_member|
1431
+ group.remove_user user_member
1432
+ end
1433
+
1434
+ (groups + removed_groups).each do |group_member|
1435
+ group.add_group group_member
1436
+ end
1437
+
1438
+ removed_groups.each do |group_member|
1439
+ group.remove_group group_member
1440
+ end
1441
+
1442
+ group
1443
+ end
1444
+
1445
+ # Convert a UNIXGroup to a Group. This method returns the new Group
1446
+ # if successful, otherwise there will be a RuntimeError somewhere first.
1447
+ # The original UNIXGroup is destroyed with Container#destroy_group and a
1448
+ # new Group is created with the same information where applicable. Old
1449
+ # UNIX attributes will be removed from Active Directory if possible
1450
+ # immediately (not waiting on AD#sync because AD#sync will think this is
1451
+ # now only a Group object with no UNIX attributes). Any external references
1452
+ # to the old UNIXGroup should be discarded and replaced with the new Group
1453
+ # object returned. Supported arguments follow:
1454
+ #
1455
+ # * :group => The UNIXGroup to convert to a Group [required]
1456
+ # * :remove_unix_users => Remove UNIXUser object members [default false]
1457
+ #
1458
+ # The :group argument is the UNIXGroup to convert. If the :group argument
1459
+ # is not a UNIXGroup object, a RuntimeError is raised. If the UNIXGroup
1460
+ # has already been removed a RuntimeError is raised. The :remove_unix_users
1461
+ # argument is a boolean flag that determines if UNIXUser objects who were
1462
+ # members of the UNIXGroup from the Windows perspective should be removed
1463
+ # as members when converting to a Group object. UNIXUser objects are
1464
+ # members from the Windows perspective as well by default because they are
1465
+ # members from the UNIX perspective. This is the default behavior in
1466
+ # RADUM. The default action is to not remove their Windows user
1467
+ # memberships when converting a UNIXGroup to a Group.
1468
+ #
1469
+ # === Parameter Types
1470
+ #
1471
+ # * :group [UNIXGroup]
1472
+ # * :remove_unix_users [boolean]
1473
+ #
1474
+ # Note that Container#destroy_group checks to make sure the group is not
1475
+ # the primary Windows group or UNIX main group for any User or UNIXUser,
1476
+ # so the :group must not be the primary Windows group or UNIX main group
1477
+ # for any users. If the group is someone's primary Windows group or UNIX
1478
+ # main group a RuntimeError will be raised. You will have to modify it by
1479
+ # hand in Active Directory if you want to convert it. The UNIX main group
1480
+ # condition is checked for all users in Active Directory before attempting
1481
+ # to use Container#destroy_group to ensure the conversion is safe. The
1482
+ # primary Windows group check is not done for all users in Active Directory
1483
+ # because it is safe to convert unless RADUM thinks the UNIXGroup
1484
+ # is one of its User or UNIXUser object's primary Windows group. In that
1485
+ # specific case, you'll have to modify the group in Active Directory by
1486
+ # hand unfortunately.
1487
+ #
1488
+ # UNIX attributes are removed from Active Directory immedately if it is
1489
+ # actually possible to destroy the UNIXGroup properly without waiting for
1490
+ # AD#sync to be called.
1491
+ def unix_group_to_group(args = {})
1492
+ RADUM::logger.log("[AD #{self.root}] entering unix_group_to_group()",
1493
+ LOG_DEBUG)
1494
+ group = args[:group]
1495
+
1496
+ # Make sure we are working with a UNIXGroup object only.
1497
+ unless group.instance_of?(UNIXGroup)
1498
+ raise ":group argument just be a UNIXGroup object."
1499
+ end
1500
+
1501
+ if group.removed?
1502
+ raise ":group has been removed."
1503
+ end
1504
+
1505
+ remove_unix_users = args[:remove_unix_users] || false
1506
+
1507
+ # Group attributes.
1508
+ name = group.name
1509
+ container = group.container
1510
+ type = group.type
1511
+ rid = group.rid
1512
+ users = group.users.clone
1513
+ groups = group.groups.clone
1514
+ removed_users = group.removed_users.clone
1515
+ removed_groups = group.removed_groups.clone
1516
+ loaded = group.loaded?
1517
+
1518
+ # Make sure the group is not someone's UNIX main group for all users
1519
+ # in Active Directory before trying to destroy the group. The
1520
+ # Container#destroy_group method only checks for objects in RADUM
1521
+ # itself.
1522
+ if ldap_is_unix_main_group?(group)
1523
+ raise ":group is someone's UNIX main group."
1524
+ end
1525
+
1526
+ # Destroy the group now that we have its information. This will fail if
1527
+ # the group is someone's primary Windows group from the RADUM perspective
1528
+ # because of the logic I have to employ, but it is fine if the group is
1529
+ # some other user account's primary Windows group that RADUM doesn't
1530
+ # know about.
1531
+ container.destroy_group group
1532
+
1533
+ # If the group was destroyed and we got this far, we need to remove
1534
+ # any of its UNIX attributes in Active Directory directly. We do need
1535
+ # to make sure it is actually there first of course.
1536
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
1537
+ found = @ldap.search(:base => group.distinguished_name,
1538
+ :filter => group_filter,
1539
+ :scope => Net::LDAP::SearchScope_BaseObject,
1540
+ :return_result => false)
1541
+
1542
+ unless found == false
1543
+ ops = [
1544
+ [:replace, :gidNumber, nil],
1545
+ [:replace, :msSFU30NisDomain, nil],
1546
+ [:replace, :unixUserPassword, nil],
1547
+ [:replace, :memberUid, nil],
1548
+ [:replace, :msSFU30PosixMember, nil]
1549
+ ]
1550
+
1551
+ RADUM::logger.log("\tRemoving groups's previous UNIX attributes.",
1552
+ LOG_DEBUG)
1553
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
1554
+ @ldap.modify :dn => group.distinguished_name, :operations => ops
1555
+ check_ldap_result
1556
+ end
1557
+
1558
+ group = Group.new :name => name, :container => container,
1559
+ :type => type, :rid => rid
1560
+
1561
+ # Set the group to loaded if it was loaded orginally. This sets the
1562
+ # modified attribute to false, but the actions below will ensure the
1563
+ # modified attribute is actually true when we are done, which is required
1564
+ # in order to update the attributes in Active Directory through AD#sync.
1565
+ group.set_loaded if loaded
1566
+
1567
+ (users + removed_users).each do |user_member|
1568
+ group.add_user user_member
1569
+ end
1570
+
1571
+ removed_users.each do |user_member|
1572
+ group.remove_user user_member
1573
+ end
1574
+
1575
+ (groups + removed_groups).each do |group_member|
1576
+ group.add_group group_member
1577
+ end
1578
+
1579
+ removed_groups.each do |group_member|
1580
+ group.remove_group group_member
1581
+ end
1582
+
1583
+ # An extra step to remove any UNIXUser objects if that was requested.
1584
+ if remove_unix_users
1585
+ group.users.clone.each do |user|
1586
+ group.remove_user user if user.instance_of?(UNIXUser)
1587
+ end
1588
+ end
1589
+
1590
+ RADUM::logger.log("[AD #{self.root}] exiting unix_group_to_group()",
1591
+ LOG_DEBUG)
1592
+ group
1593
+ end
1594
+
1595
+ # Load all user and group objects in Active Directory that are in the AD
1596
+ # object's Containers. This automatically creates User, UNIXUser, Group,
1597
+ # and UNIXGroup objects as needed and sets all of their attributes
1598
+ # correctly. This can be used to initialize a program using the RADUM
1599
+ # module for account management work.
1600
+ #
1601
+ # User objects are not created if their primary Windows group is not found
1602
+ # during the load. UNIXUser objects are not created if their UNIX main group
1603
+ # is not found during the load. Warning messages are printed in each
1604
+ # case. Make sure all required Containers are in the AD before loading
1605
+ # data from Active Directory to avoid this problem.
1606
+ #
1607
+ # You generally should call AD#load to ensure the RADUM system has a valid
1608
+ # representation of the Active Directory objects. You can call AD#sync
1609
+ # without calling AD#load first, but your object values are authoritative.
1610
+ # Unless you set every attribute correctly, unset object attributes will
1611
+ # overwrite current values in Active Directory. Note that AD#sync will
1612
+ # not touch Active Directory group memberships it does not know about
1613
+ # explicitly, so AD#sync will not remove group and user memberships in
1614
+ # groups that were not explicitly removed in RADUM. The general RADUM
1615
+ # pattern is:
1616
+ #
1617
+ # * AD.new(...)
1618
+ # * Container.new(...) [for any Containers of interest]
1619
+ # * AD#load()
1620
+ # * Create, update, or remove existing loaded objects.
1621
+ # * AD#sync()
1622
+ #
1623
+ # This methods will sliently ignore objects that already exist in the AD
1624
+ # object unless the Logger default_level is set to LOG_DEBUG. Therefore,
1625
+ # it is possible to load new Container objects after calling this method
1626
+ # previously to create new objects. Anything that previously exists will
1627
+ # be ignored.
1628
+ def load
1629
+ RADUM::logger.log("[AD #{self.root}] entering load()", LOG_DEBUG)
1630
+ # This method can be called more than once. After loading users and
1631
+ # groups, we just want to work with those after they are created. This
1632
+ # allows the method to be called more than once.
1633
+ loaded_users = []
1634
+ loaded_groups = []
1635
+ # Find all the groups first. We might need one to represent the main
1636
+ # group of a UNIX user.
1637
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
1638
+
1639
+ @containers.each do |container|
1640
+ @ldap.search(:base => container.distinguished_name,
1641
+ :filter => group_filter,
1642
+ :scope => Net::LDAP::SearchScope_SingleLevel) do |entry|
1643
+ attr = group_ldap_entry_attr(entry)
1644
+
1645
+ # Skip any Group or UNIXGroup objects that already exist (have this
1646
+ # :name attribute).
1647
+ if find_group_by_name(attr[:name])
1648
+ RADUM::logger.log("\tNot loading group <#{attr[:name]}>: already" +
1649
+ " exists.", LOG_DEBUG)
1650
+ next
1651
+ end
1652
+
1653
+ # Note that groups add themselves to their container.
1654
+ unless attr[:gid].nil?
1655
+ attr[:nis_domain] = "radum" unless attr[:nis_domain]
1656
+ group = UNIXGroup.new :name => attr[:name], :container => container,
1657
+ :gid => attr[:gid], :type => attr[:type],
1658
+ :nis_domain => attr[:nis_domain],
1659
+ :rid => attr[:rid]
1660
+ group.unix_password = attr[:unix_password] if attr[:unix_password]
1661
+ loaded_groups.push group
1662
+ else
1663
+ group = Group.new :name => attr[:name], :container => container,
1664
+ :type => attr[:type], :rid => attr[:rid]
1665
+ loaded_groups.push group
1666
+ end
1667
+ end
1668
+ end
1669
+
1670
+ # Find all the users. The UNIX main group must be set for UNIXUser
1671
+ # objects, so it will be necessary to search for that.
1672
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
1673
+
1674
+ @containers.each do |container|
1675
+ @ldap.search(:base => container.distinguished_name,
1676
+ :filter => user_filter,
1677
+ :scope => Net::LDAP::SearchScope_SingleLevel) do |entry|
1678
+ attr = user_ldap_entry_attr(entry)
1679
+
1680
+ # Skip any User or UNIXUser objects that already exist (have this
1681
+ # :username attribute).
1682
+ if find_user_by_username(attr[:username])
1683
+ RADUM::logger.log("\tNot loading user <#{attr[:username]}>:" +
1684
+ " already exists.", LOG_DEBUG)
1685
+ next
1686
+ end
1687
+
1688
+ # Note that users add themselves to their container. We have to have
1689
+ # found the primary_group already, or we can't make the user. The
1690
+ # primary group is important information, but it is stored as a RID
1691
+ # value in the primaryGroupID AD attribute. The group membership
1692
+ # it defines is defined nowhere else however. We will print a warning
1693
+ # for any users skipped. This is why the AD object automatically
1694
+ # adds a cn=Users container.
1695
+ if attr[:primary_group]
1696
+ unless attr[:uid].nil? || attr[:gid].nil?
1697
+ if unix_main_group = find_group_by_gid(attr[:gid])
1698
+ attr[:nis_domain] = "radum" unless attr[:nis_domain]
1699
+ user = UNIXUser.new :username => attr[:username],
1700
+ :container => container,
1701
+ :primary_group => attr[:primary_group],
1702
+ :uid => attr[:uid],
1703
+ :unix_main_group => unix_main_group,
1704
+ :shell => attr[:shell],
1705
+ :home_directory => attr[:home_directory],
1706
+ :nis_domain => attr[:nis_domain],
1707
+ :disabled => attr[:disabled?],
1708
+ :rid => attr[:rid]
1709
+ user.distinguished_name = attr[:distinguished_name]
1710
+ user.first_name = attr[:first_name] if attr[:first_name]
1711
+ user.initials = attr[:initials] if attr[:initials]
1712
+ user.middle_name = attr[:middle_name] if attr[:middle_name]
1713
+ user.surname = attr[:surname] if attr[:surname]
1714
+ user.script_path = attr[:script_path] if attr[:script_path]
1715
+ user.profile_path = attr[:profile_path] if attr[:profile_path]
1716
+
1717
+ if attr[:local_drive] && attr[:local_path]
1718
+ user.connect_drive_to(attr[:local_drive], attr[:local_path])
1719
+ elsif attr[:local_path]
1720
+ user.local_path = attr[:local_path]
1721
+ end
1722
+
1723
+ user.gecos = attr[:gecos] if attr[:gecos]
1724
+ user.unix_password = attr[:unix_password] if
1725
+ attr[:unix_password]
1726
+ user.shadow_expire = attr[:shadow_expire] if
1727
+ attr[:shadow_expire]
1728
+ user.shadow_flag = attr[:shadow_flag] if attr[:shadow_flag]
1729
+ user.shadow_inactive = attr[:shadow_inactive] if
1730
+ attr[:shadow_inactive]
1731
+ user.shadow_last_change = attr[:shadow_last_change] if
1732
+ attr[:shadow_last_change]
1733
+ user.shadow_max = attr[:shadow_max] if attr[:shadow_max]
1734
+ user.shadow_min = attr[:shadow_min] if attr[:shadow_min]
1735
+ user.shadow_warning = attr[:shadow_warning] if
1736
+ attr[:shadow_warning]
1737
+
1738
+ if attr[:must_change_password?]
1739
+ user.force_change_password
1740
+ end
1741
+
1742
+ loaded_users.push user
1743
+ else
1744
+ RADUM::logger.log("Warning: Main UNIX group could not be " +
1745
+ "found for: " + attr[:username], LOG_NORMAL)
1746
+ RADUM::logger.log("Not loading #{attr[:username]}.", LOG_NORMAL)
1747
+ end
1748
+ else
1749
+ user = User.new :username => attr[:username],
1750
+ :container => container,
1751
+ :primary_group => attr[:primary_group],
1752
+ :disabled => attr[:disabled?],
1753
+ :rid => attr[:rid]
1754
+ user.distinguished_name = attr[:distinguished_name]
1755
+ user.first_name = attr[:first_name] if attr[:first_name]
1756
+ user.initials = attr[:initials] if attr[:initials]
1757
+ user.middle_name = attr[:middle_name] if attr[:middle_name]
1758
+ user.surname = attr[:surname] if attr[:surname]
1759
+ user.script_path = attr[:script_path] if attr[:script_path]
1760
+ user.profile_path = attr[:profile_path] if attr[:profile_path]
1761
+
1762
+ if attr[:local_drive] && attr[:local_path]
1763
+ user.connect_drive_to(attr[:local_drive], attr[:local_path])
1764
+ elsif attr[:local_path]
1765
+ user.local_path = attr[:local_path]
1766
+ end
1767
+
1768
+ if attr[:must_change_password?]
1769
+ user.force_change_password
1770
+ end
1771
+
1772
+ loaded_users.push user
1773
+ end
1774
+ else
1775
+ RADUM::logger.log("Warning: Windows primary group not found for: " +
1776
+ attr[:username], LOG_NORMAL)
1777
+ RADUM::logger.log("Not loading #{attr[:username]}.", LOG_NORMAL)
1778
+ end
1779
+ end
1780
+ end
1781
+
1782
+ # Add users to groups, which also adds the groups to the user, etc. The
1783
+ # Windows primary_group was taken care of when creating the users
1784
+ # previously. This can happen even if this method is called multiple
1785
+ # times because it is safe to add users to groups (and vice versa)
1786
+ # more than once. If the membership already exists, nothing happens.
1787
+ # Note that in this case, we do have to process all groups again in case
1788
+ # some of the new users are in already processed groups.
1789
+ #
1790
+ # Here it is key to process all groups, even if they were already
1791
+ # loaded once.
1792
+ groups.each do |group|
1793
+ begin
1794
+ # Note that this takes care of the case where a group was created
1795
+ # and then AD#load is called before AD#sync. In that case, the group
1796
+ # is not even in Active Directory yet and the pop method call will
1797
+ # not be possible because it does not exist. There are other ways
1798
+ # to check this, but I don't really care if it was not found because
1799
+ # I assume the user knows what they are doing if they do this type
1800
+ # of pattern.
1801
+ entry = @ldap.search(:base => group.distinguished_name,
1802
+ :filter => group_filter,
1803
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
1804
+
1805
+ entry.member.each do |member|
1806
+ # Groups can have groups or users as members, unlike UNIX where
1807
+ # groups cannot contain group members.
1808
+ member_group = find_group_by_dn(member)
1809
+
1810
+ if member_group
1811
+ group.add_group member_group
1812
+ end
1813
+
1814
+ member_user = find_user_by_dn(member)
1815
+
1816
+ if member_user
1817
+ group.add_user member_user
1818
+ end
1819
+ end
1820
+ rescue NoMethodError
1821
+ end
1822
+ end
1823
+
1824
+ # Set all users and groups as loaded. This has to be done last to make
1825
+ # sure the modified attribute is correct. The modified attribute needs
1826
+ # to be false, and it is hidden from direct access by the set_loaded
1827
+ # method. In this case "all users and groups" means ones we explicitly
1828
+ # processed because this method can be called more than once. If the
1829
+ # original object was loaded, but then modified, we don't want to reset
1830
+ # the modified attribute as well. We also don't want to set the loaded
1831
+ # attribute and reset the modified attribute for objects that were
1832
+ # created after an initial call to this method and were skipped on
1833
+ # later calls.
1834
+ #
1835
+ # Here it is key to only touch things explicitly loaded in this call.
1836
+ loaded_groups.each do |group|
1837
+ group.set_loaded
1838
+ end
1839
+
1840
+ loaded_users.each do |user|
1841
+ user.set_loaded
1842
+ end
1843
+
1844
+ RADUM::logger.log("[AD #{self.root}] exiting load()", LOG_DEBUG)
1845
+ end
1846
+
1847
+ # Load the next free UID value. This is a convenience method that allows
1848
+ # one to find the next free UID value. This method returns the next free
1849
+ # UID value found while searching from the AD root and any current UID
1850
+ # values for UNIXUser objects that might not be in the Active Directory yet.
1851
+ # If nothing is found, the min_uid attribute is returned.
1852
+ def load_next_uid
1853
+ all_uids = load_ldap_uids
1854
+ next_uid = 0
1855
+
1856
+ # This accounts for any GIDs that might not be in Active Directory yet
1857
+ # as well.
1858
+ (all_uids + @uids).uniq.sort.each do |uid|
1859
+ if next_uid == 0 || next_uid + 1 == uid
1860
+ next_uid = uid
1861
+ else
1862
+ break
1863
+ end
1864
+ end
1865
+
1866
+ if next_uid == 0
1867
+ @min_uid
1868
+ else
1869
+ next_uid + 1
1870
+ end
1871
+ end
1872
+
1873
+ # Load the next free GID value. This is a convenince method that allows
1874
+ # one to find the next free GID value. This method returns the next free
1875
+ # GID value found while searching from the AD root and any current GID
1876
+ # values for UNIXGroup objects that might not be in the Active Directory
1877
+ # yet. If nothing is found, the min_gid attribute is returned.
1878
+ def load_next_gid
1879
+ all_gids = load_ldap_gids
1880
+ next_gid = 0
1881
+
1882
+ # This accounts for any GIDs that might not be in Active Directory yet
1883
+ # as well.
1884
+ (all_gids + @gids).uniq.sort.each do |gid|
1885
+ if next_gid == 0 || next_gid + 1 == gid
1886
+ next_gid = gid
1887
+ else
1888
+ break
1889
+ end
1890
+ end
1891
+
1892
+ if next_gid == 0
1893
+ @min_gid
1894
+ else
1895
+ next_gid + 1
1896
+ end
1897
+ end
1898
+
1899
+ # Synchronize all modified Container, User, UNIXUser, Group, and UNIXGroup
1900
+ # objects to Active Directory. This will create entries as needed after
1901
+ # checking to make sure they do not already exist. New attributes will be
1902
+ # added, unset attributes will be removed, and modified attributes will be
1903
+ # updated automatically. Removed objects will be deleted from Active
1904
+ # Directory.
1905
+ #
1906
+ # You generally should call AD#load to ensure the RADUM system has a valid
1907
+ # representation of the Active Directory objects. You can call AD#sync
1908
+ # without calling AD#load first, but your object values are authoritative.
1909
+ # Unless you set every attribute correctly, unset object attributes will
1910
+ # overwrite current values in Active Directory. Note that AD#sync will
1911
+ # not touch Active Directory group memberships it does not know about
1912
+ # explicitly, so AD#sync will not remove group and user memberships in
1913
+ # groups that were not explicitly removed in RADUM. The general RADUM
1914
+ # pattern is:
1915
+ #
1916
+ # * AD.new(...)
1917
+ # * Container.new(...) [for any Containers of interest]
1918
+ # * AD#load()
1919
+ # * Create, update, or remove existing loaded objects.
1920
+ # * AD#sync()
1921
+ def sync
1922
+ RADUM::logger.log("[AD #{self.root}] entering sync()", LOG_DEBUG)
1923
+ users_to_destroy = []
1924
+ containers_to_destroy = []
1925
+
1926
+ # First, delete any users that have been removed from a container here.
1927
+ # We need to remove users first because a group cannot be removed if
1928
+ # a user has it as their primary Windows group. Just in case, we remove
1929
+ # the removed users first. The same applies if the group is some other
1930
+ # user's UNIX main group. The code in this module makes sure that doesn't
1931
+ # happen for objects it knows about, but there could be others in Active
1932
+ # Directory the module does not know about.
1933
+ removed_users.each do |user|
1934
+ delete_user user
1935
+ # The delete_user() method cannot destroy a user because the system
1936
+ # still needs to reference the user when update_group() is called so
1937
+ # that the deleted user will end up having their UNIX attributes removed
1938
+ # from a UNIXGroup if that applies when updating the specific group.
1939
+ # This is not needed for Group or UNIXGroup objects because they only
1940
+ # have membership from the Windows perspective - and that's updated by
1941
+ # simply deleting the Group or UNIXGroup.
1942
+ users_to_destroy.push user
1943
+ end
1944
+
1945
+ # Second, remove any groups that have been removed from a contianer here.
1946
+ removed_groups.each do |group|
1947
+ # This method checks if the group is some other user's primary Windows
1948
+ # group by searching the entire Active Directory. A group cannot be
1949
+ # removed if it is any user's primary Windows group. The same applies
1950
+ # if the group is some other user's UNIX main group. The code in this
1951
+ # module makes sure that doesn't happen for objects it knows about, but
1952
+ # there could be others in Active Directory the module does not know
1953
+ # about, hence the checks in delete_group().
1954
+ delete_group group
1955
+ end
1956
+
1957
+ # Third, remove any containers that have been removed. This can only be
1958
+ # done after all the user and group removals hae been dealt with. This
1959
+ # can still fail if there are any objects in Active Directory inside of
1960
+ # the container (such as another container). Note that the
1961
+ # AD#remove_container method makes sure that a container is not removed
1962
+ # if it contains another container in the first place.
1963
+ @removed_containers.each do |container|
1964
+ delete_container container
1965
+ # The delete_container() method cannot destroy a container because the
1966
+ # system still needs a reference when update_group() is called for
1967
+ # users that have been deleted from the system to ensure their
1968
+ # membership in UNIX groups from the UNIX perspective is deleted.
1969
+ # The removed_users() method depends on the container still being
1970
+ # there for this later processing.
1971
+ containers_to_destroy.push container
1972
+ end
1973
+
1974
+ # Fourth, create any containers or organizational units that do not
1975
+ # already exist.
1976
+ @containers.each do |container|
1977
+ # This method only creates containers that do not already exist. Since
1978
+ # containers are not loaded directly at first, their status is directly
1979
+ # tested in the method.
1980
+ create_container container
1981
+ end
1982
+
1983
+ # Fifth, make sure any groups that need to be created are added to Active
1984
+ # Directory.
1985
+ groups.each do |group|
1986
+ # This method checks if the group actually needs to be created or not.
1987
+ create_group group
1988
+ end
1989
+
1990
+ # Sixth, make sure any users that need to be created are added to Active
1991
+ # Directory.
1992
+ users.each do |user|
1993
+ # This method checks if the user actually needs to be created or not.
1994
+ create_user user
1995
+ end
1996
+
1997
+ # Seventh, update any modified attributes on each group.
1998
+ groups.each do |group|
1999
+ # This method figures out what attributes need to be updated when
2000
+ # compared to Active Directory. All objects should exist in Active
2001
+ # Directory at this point, but the method handles cases where the
2002
+ # object is not in Active Directory by skipping the update in that
2003
+ # case.
2004
+ update_group group
2005
+ end
2006
+
2007
+ # Eighth, update any modified attributs on each user.
2008
+ users.each do |user|
2009
+ # This method figures out what attributes need to be updated when
2010
+ # compared to Active Directory. All objects should exist in Active
2011
+ # Directory at this point, but the method handles cases where the
2012
+ # object is not in Active Directory by skipping the update in that
2013
+ # case.
2014
+ update_user user
2015
+ end
2016
+
2017
+ # Finally, destroy any user and container objects that were deleted.
2018
+ users_to_destroy.each do |user|
2019
+ RADUM::logger.log("[AD #{self.root}] destroying user" +
2020
+ " <#{user.username}> at end of sync().", LOG_DEBUG)
2021
+ user.container.destroy_user user
2022
+ end
2023
+
2024
+ # Container objects are destroyed even if they were not removed from
2025
+ # Active Directory through LDAP. If they were destroyed or removed RADUM,
2026
+ # it is safe to discard their reference here. If they were not deleted
2027
+ # from Active Directory, that means there was some reference we were not
2028
+ # aware of in Active Directory - but we can still forget about them.
2029
+ containers_to_destroy.each do |container|
2030
+ RADUM::logger.log("[AD #{self.root}] destroying container" +
2031
+ " <#{container.name}> at end of sync().", LOG_DEBUG)
2032
+ destroy_container container
2033
+ end
2034
+
2035
+ RADUM::logger.log("[AD #{self.root}] exiting sync()", LOG_DEBUG)
2036
+ end
2037
+
2038
+ # The String representation of the AD object.
2039
+ def to_s
2040
+ "AD [#{@root} #{@server}:#{@port}]"
2041
+ end
2042
+
2043
+ private
2044
+
2045
+ # Unpack a RID from the SID value in the LDAP objectSid attribute for a
2046
+ # user or group in Active Directory.
2047
+ #
2048
+ # === Parameter Types
2049
+ #
2050
+ # * sid [binary LDAP attribute value]
2051
+ def sid2rid_int(sid)
2052
+ sid.unpack("qV*").pop.to_i
2053
+ end
2054
+
2055
+ # Convert a string to UTF-16LE. For ASCII characters, the result should be
2056
+ # each character followed by a NULL, so this is very easy. Windows expects
2057
+ # a UTF-16LE string for the unicodePwd attribute. Note that the password
2058
+ # Active Directory is expecting for the unicodePwd attribute has to be
2059
+ # explicitly quoted.
2060
+ #
2061
+ # === Parameter Types
2062
+ #
2063
+ # * str [String]
2064
+ def str2utf16le(str)
2065
+ ('"' + str + '"').gsub(/./) { |c| "#{c}\0" }
2066
+ end
2067
+
2068
+ # Return a random number character as a string.
2069
+ def random_number
2070
+ srand
2071
+ (rand 10).to_s
2072
+ end
2073
+
2074
+ # Return a random lowercase letter as a string.
2075
+ def random_lowercase
2076
+ srand
2077
+ sprintf "%c", ((rand 26) + 97)
2078
+ end
2079
+
2080
+ # Return a random uppercase letter as a string.
2081
+ def random_uppercase
2082
+ random_lowercase.swapcase
2083
+ end
2084
+
2085
+ # Return a random symbol as a string.
2086
+ def random_symbol
2087
+ srand
2088
+
2089
+ case rand 4
2090
+ when 0
2091
+ code = rand(15) + 33
2092
+ when 1
2093
+ code = rand(7) + 58
2094
+ when 2
2095
+ code = rand(6) + 91
2096
+ when 3
2097
+ code = rand(4) + 123
2098
+ end
2099
+
2100
+ sprintf "%c", code
2101
+ end
2102
+
2103
+ # Return a random 8 character password as a string. There is a formula to
2104
+ # try and avoid any problems with Active Directory password requirements.
2105
+ # If you don't like this, supply your own password for User and UNIXUser
2106
+ # objects.
2107
+ def random_password
2108
+ random_lowercase + random_number + random_lowercase + random_uppercase +
2109
+ random_symbol + random_number + random_uppercase + random_symbol
2110
+ end
2111
+
2112
+ # Return an array of all GID values in Active Directory.
2113
+ def load_ldap_gids
2114
+ all_gids = []
2115
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
2116
+
2117
+ @ldap.search(:base => @root, :filter => group_filter) do |entry|
2118
+ begin
2119
+ gid = entry.gidNumber.pop.to_i
2120
+ all_gids.push gid
2121
+ rescue NoMethodError
2122
+ end
2123
+ end
2124
+
2125
+ all_gids
2126
+ end
2127
+
2128
+ # Return an array of all UID values in Active Directory.
2129
+ def load_ldap_uids
2130
+ all_uids = []
2131
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
2132
+
2133
+ @ldap.search(:base => @root, :filter => user_filter) do |entry|
2134
+ begin
2135
+ uid = entry.uidNumber.pop.to_i
2136
+ all_uids.push uid
2137
+ rescue NoMethodError
2138
+ end
2139
+ end
2140
+
2141
+ all_uids
2142
+ end
2143
+
2144
+ # Return a hash with an Active Directory group's base LDAP attributes. The
2145
+ # key is the RADUM group attribute name and the value is the computed value
2146
+ # from the group's attributes in Active Directory.
2147
+ #
2148
+ # === Parameter Types
2149
+ #
2150
+ # * entry [String]
2151
+ def group_ldap_entry_attr(entry)
2152
+ attr = {}
2153
+ # These are attributes that might be empty. If they are empty,
2154
+ # a NoMethodError exception will be raised. We have to check each
2155
+ # individually and set an initial indicator value (nil). All the
2156
+ # other attributes should exist and do not require this level of
2157
+ # checking.
2158
+ attr[:gid] = nil
2159
+ attr[:nis_domain] = nil
2160
+ attr[:unix_password] = nil
2161
+
2162
+ begin
2163
+ attr[:gid] = entry.gidNumber.pop.to_i
2164
+ rescue NoMethodError
2165
+ end
2166
+
2167
+ begin
2168
+ attr[:nis_domain] = entry.msSFU30NisDomain.pop
2169
+ rescue NoMethodError
2170
+ end
2171
+
2172
+ begin
2173
+ attr[:unix_password] = entry.unixUserPassword.pop
2174
+ rescue NoMethodError
2175
+ end
2176
+
2177
+ attr[:name] = entry.name.pop
2178
+ attr[:rid] = sid2rid_int(entry.objectSid.pop)
2179
+ attr[:type] = entry.groupType.pop.to_i
2180
+ return attr
2181
+ end
2182
+
2183
+ # Return a hash with an Active Directory user's base LDAP attributes. The
2184
+ # key is the RADUM user attribute name and the value is the computed value
2185
+ # from the user's attributes in Active Directory.
2186
+ #
2187
+ # === Parameter Types
2188
+ #
2189
+ # * entry [String]
2190
+ def user_ldap_entry_attr(entry)
2191
+ attr = {}
2192
+ # These are attributes that might be empty. If they are empty,
2193
+ # a NoMethodError exception will be raised. We have to check each
2194
+ # individually and set an initial indicator value (nil). All the
2195
+ # other attributes should exist and do not require this level of
2196
+ # checking.
2197
+ attr[:first_name] = nil
2198
+ attr[:initials] = nil
2199
+ attr[:middle_name] = nil
2200
+ attr[:surname] = nil
2201
+ attr[:script_path] = nil
2202
+ attr[:profile_path] = nil
2203
+ attr[:local_path] = nil
2204
+ attr[:local_drive] = nil
2205
+ attr[:uid] = nil
2206
+ attr[:gid] = nil
2207
+ attr[:nis_domain] = nil
2208
+ attr[:gecos] = nil
2209
+ attr[:unix_password] = nil
2210
+ attr[:shadow_expire] = nil
2211
+ attr[:shadow_flag] = nil
2212
+ attr[:shadow_inactive] = nil
2213
+ attr[:shadow_last_change] = nil
2214
+ attr[:shadow_max] = nil
2215
+ attr[:shadow_min] = nil
2216
+ attr[:shadow_warning] = nil
2217
+ attr[:shell] = nil
2218
+ attr[:home_directory] = nil
2219
+
2220
+ begin
2221
+ attr[:first_name] = entry.givenName.pop
2222
+ rescue NoMethodError
2223
+ end
2224
+
2225
+ begin
2226
+ attr[:initials] = entry.initials.pop
2227
+ rescue NoMethodError
2228
+ end
2229
+
2230
+ begin
2231
+ attr[:middle_name] = entry.middleName.pop
2232
+ rescue NoMethodError
2233
+ end
2234
+
2235
+ begin
2236
+ attr[:surname] = entry.sn.pop
2237
+ rescue NoMethodError
2238
+ end
2239
+
2240
+ begin
2241
+ attr[:script_path] = entry.scriptPath.pop
2242
+ rescue NoMethodError
2243
+ end
2244
+
2245
+ begin
2246
+ attr[:profile_path] = entry.profilePath.pop
2247
+ rescue NoMethodError
2248
+ end
2249
+
2250
+ begin
2251
+ attr[:local_path] = entry.homeDirectory.pop
2252
+ rescue NoMethodError
2253
+ end
2254
+
2255
+ begin
2256
+ attr[:local_drive] = entry.homeDrive.pop
2257
+ rescue NoMethodError
2258
+ end
2259
+
2260
+ begin
2261
+ attr[:uid] = entry.uidNumber.pop.to_i
2262
+ rescue NoMethodError
2263
+ end
2264
+
2265
+ begin
2266
+ attr[:gid] = entry.gidNumber.pop.to_i
2267
+ rescue NoMethodError
2268
+ end
2269
+
2270
+ begin
2271
+ attr[:nis_domain] = entry.msSFU30NisDomain.pop
2272
+ rescue NoMethodError
2273
+ end
2274
+
2275
+ begin
2276
+ attr[:gecos] = entry.gecos.pop
2277
+ rescue NoMethodError
2278
+ end
2279
+
2280
+ begin
2281
+ attr[:unix_password] = entry.unixUserPassword.pop
2282
+ rescue NoMethodError
2283
+ end
2284
+
2285
+ begin
2286
+ attr[:shadow_expire] = entry.shadowExpire.pop.to_i
2287
+ rescue NoMethodError
2288
+ end
2289
+
2290
+ begin
2291
+ attr[:shadow_flag] = entry.shadowFlag.pop.to_i
2292
+ rescue NoMethodError
2293
+ end
2294
+
2295
+ begin
2296
+ attr[:shadow_inactive] = entry.shadowInactive.pop.to_i
2297
+ rescue NoMethodError
2298
+ end
2299
+
2300
+ begin
2301
+ attr[:shadow_last_change] = entry.shadowLastChange.pop.to_i
2302
+ rescue NoMethodError
2303
+ end
2304
+
2305
+ begin
2306
+ attr[:shadow_max] = entry.shadowMax.pop.to_i
2307
+ rescue NoMethodError
2308
+ end
2309
+
2310
+ begin
2311
+ attr[:shadow_min] = entry.shadowMin.pop.to_i
2312
+ rescue NoMethodError
2313
+ end
2314
+
2315
+ begin
2316
+ attr[:shadow_warning] = entry.shadowWarning.pop.to_i
2317
+ rescue NoMethodError
2318
+ end
2319
+
2320
+ begin
2321
+ attr[:shell] = entry.loginShell.pop
2322
+ rescue NoMethodError
2323
+ end
2324
+
2325
+ begin
2326
+ attr[:home_directory] = entry.unixHomeDirectory.pop
2327
+ rescue NoMethodError
2328
+ end
2329
+
2330
+ attr[:disabled?] = (entry.userAccountControl.pop.to_i ==
2331
+ UF_NORMAL_ACCOUNT + UF_ACCOUNTDISABLE ? true : false)
2332
+ attr[:must_change_password?] = (entry.pwdLastSet.pop.to_i == 0)
2333
+ attr[:primary_group] = find_group_by_rid(entry.primaryGroupID.pop.to_i)
2334
+ attr[:rid] = sid2rid_int(entry.objectSid.pop)
2335
+ attr[:username] = entry.sAMAccountName.pop
2336
+ attr[:distinguished_name] = entry.distinguishedName.pop
2337
+ return attr
2338
+ end
2339
+
2340
+ # Check the LDAP operation result code for an error message. This method
2341
+ # raises a RuntimeError if the operation result code is 49, which indicates
2342
+ # an authentication credentials error.
2343
+ def check_ldap_result
2344
+ unless @ldap.get_operation_result.code == 0
2345
+ RADUM::logger.log("LDAP ERROR: " + @ldap.get_operation_result.message,
2346
+ LOG_NORMAL)
2347
+ RADUM::logger.log("[Error code: " +
2348
+ @ldap.get_operation_result.code.to_s + "]",
2349
+ LOG_NORMAL)
2350
+ if @ldap.get_operation_result.code == 49
2351
+ raise "LDAP authentication credentials error."
2352
+ end
2353
+ end
2354
+ end
2355
+
2356
+ # Delete a Container from Active Directory. The Container is only deleted
2357
+ # if it is found.
2358
+ #
2359
+ # === Parameter Types
2360
+ #
2361
+ # * container [Container]
2362
+ def delete_container(container)
2363
+ RADUM::logger.log("[AD #{self.root}]" +
2364
+ " delete_container(<#{container.name}>)", LOG_DEBUG)
2365
+
2366
+ if container.name =~ /^[Oo][Uu]=/
2367
+ type = "organizationalUnit"
2368
+ elsif container.name =~ /^[Cc][Nn]=/
2369
+ type = "container"
2370
+ else
2371
+ RADUM::logger.log("SYNC ERROR: " + container.name +
2372
+ " - unknown Container type.", LOG_NORMAL)
2373
+ return
2374
+ end
2375
+
2376
+ container_filter = Net::LDAP::Filter.eq("objectclass", type)
2377
+ found = @ldap.search(:base => container.distinguished_name,
2378
+ :filter => container_filter,
2379
+ :scope => Net::LDAP::SearchScope_BaseObject,
2380
+ :return_result => false)
2381
+
2382
+ if found == false
2383
+ RADUM::logger.log("\t#{container.distinguished_name} not found" +
2384
+ " - not deleting.", LOG_DEBUG)
2385
+ return
2386
+ end
2387
+
2388
+ @ldap.delete :dn => container.distinguished_name
2389
+ check_ldap_result
2390
+ # Destroying the container is delayed until the end of the AD#sync call
2391
+ # because we still need a reference indicating that a user was removed
2392
+ # from Active Directory to ensure the user's UNIX group membership
2393
+ # attributes are updated properly in AD#update_group if necessary. The
2394
+ # user's Windows group memberships are handled automatically when they
2395
+ # are deleted from Active Directory. The AD class uses references to
2396
+ # containers to figure this out.
2397
+ end
2398
+
2399
+ # Create a Container in Active Directory. Each Container is searched for
2400
+ # directly and created if it does not already exist. This method also
2401
+ # automatically creates parent containers as required. This is safe to
2402
+ # do, even if one of those was also passed to this method later (since it
2403
+ # would then be found).
2404
+ #
2405
+ # === Parameter Types
2406
+ #
2407
+ # * container [Container]
2408
+ def create_container(container)
2409
+ RADUM::logger.log("[AD #{self.root}]" +
2410
+ " create_container(<#{container.name}>)", LOG_DEBUG)
2411
+ distinguished_name = @root
2412
+ # This depends on the fact that the Container name had all spaces stripped
2413
+ # out in the initialize() method of the Container class.
2414
+ container.name.split(/,/).reverse.each do |current_name|
2415
+ # We have to keep track of the current path so that we have a
2416
+ # distinguished name to work wtih.
2417
+ distinguished_name = "#{current_name},#{distinguished_name}"
2418
+
2419
+ if current_name =~ /^[Oo][Uu]=/
2420
+ type = "organizationalUnit"
2421
+ elsif current_name =~ /^[Cc][Nn]=/
2422
+ type = "container"
2423
+ else
2424
+ RADUM::logger.log("SYNC ERROR: " + container.name +
2425
+ " - unknown Container type.", LOG_NORMAL)
2426
+ return
2427
+ end
2428
+
2429
+ container_filter = Net::LDAP::Filter.eq("objectclass", type)
2430
+ # The return value will be false explicitly if the search fails,
2431
+ # otherwise it will be an array of entries. Therefore it is important
2432
+ # to check for false explicitly for a failure. A failure indicates
2433
+ # that the container needs to be created.
2434
+ found = @ldap.search(:base => distinguished_name,
2435
+ :filter => container_filter,
2436
+ :scope => Net::LDAP::SearchScope_BaseObject,
2437
+ :return_result => false)
2438
+
2439
+ if found == false
2440
+ RADUM::logger.log("\t#{distinguished_name} not found - creating.",
2441
+ LOG_DEBUG)
2442
+
2443
+ # Note that all the attributes need to be strings in the attr hash.
2444
+ if type == "organizationalUnit"
2445
+ attr = {
2446
+ :name => current_name.split(/,/)[0].gsub(/[Oo][Uu]=/, ""),
2447
+ :objectclass => ["top", "organizationalUnit"]
2448
+ }
2449
+ elsif type == "container"
2450
+ name = current_name.split(/,/)[0].gsub(/[Cc][Nn]=/, "")
2451
+
2452
+ attr = {
2453
+ :name => name,
2454
+ :objectclass => ["top", "container"]
2455
+ }
2456
+ else
2457
+ RADUM::logger.log("SYNC ERROR: " + container.name +
2458
+ " (#{current_name}) - unknown Container type.",
2459
+ LOG_NORMAL)
2460
+ return
2461
+ end
2462
+
2463
+ @ldap.add :dn => distinguished_name, :attributes => attr
2464
+ check_ldap_result
2465
+ else
2466
+ RADUM::logger.log("\t#{distinguished_name} found - not creating.",
2467
+ LOG_DEBUG)
2468
+ end
2469
+ end
2470
+ end
2471
+
2472
+ # Determine if the group is anyone's primary Windows group in Active
2473
+ # Directory. Returns true if the group is anyone's primary Windows group,
2474
+ # false otherwise. This works for Group and UNIXGroup objects.
2475
+ #
2476
+ # === Parameter Types
2477
+ #
2478
+ # * group [Group or UNIXGroup]
2479
+ def ldap_is_primary_windows_group?(group)
2480
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
2481
+
2482
+ @ldap.search(:base => @root, :filter => user_filter) do |entry|
2483
+ rid = entry.primaryGroupID.pop.to_i
2484
+ return true if rid == group.rid
2485
+ end
2486
+
2487
+ false
2488
+ end
2489
+
2490
+ # Determine if the group is anyone's UNIX main group in Active Directory.
2491
+ # Returns true if the group is anyone's UNIX main group, false otherwise.
2492
+ # This works for UNIXGroup objects and returns false for Group objects.
2493
+ #
2494
+ # === Parameter Types
2495
+ #
2496
+ # * group [UNIXGroup]
2497
+ def ldap_is_unix_main_group?(group)
2498
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
2499
+
2500
+ if group.instance_of?(UNIXGroup)
2501
+ @ldap.search(:base => @root, :filter => user_filter) do |entry|
2502
+ begin
2503
+ gid = entry.gidNumber.pop.to_i
2504
+ return true if gid == group.gid
2505
+ rescue NoMethodError
2506
+ end
2507
+ end
2508
+ end
2509
+
2510
+ false
2511
+ end
2512
+
2513
+ # Delete a Group or UNIXGroup from Active Directory.
2514
+ #
2515
+ # === Parameter Types
2516
+ #
2517
+ # * group [Group or UNIXGroup]
2518
+ def delete_group(group)
2519
+ RADUM::logger.log("[AD #{self.root}]" +
2520
+ " delete_group(<#{group.name}>)", LOG_DEBUG)
2521
+ # First check to make sure the group is not the primary Windows group
2522
+ # or UNIX main group for any user in Active Directory. We could probably
2523
+ # rely on the attempt to delete the group failing, but I don't like doing
2524
+ # that. Yes, it is much less efficient this way, but removing a group is
2525
+ # not very common in my experience. Also note that this would probably not
2526
+ # fail for a UNIX main group because that's pretty much "tacked" onto
2527
+ # the standard Windows Active Directory logic (at least, I have been
2528
+ # able to remove a group that was someone's UNIX main group before, but
2529
+ # not their primary Windows group).
2530
+ found_primary = ldap_is_primary_windows_group?(group)
2531
+ found_unix = ldap_is_unix_main_group?(group)
2532
+
2533
+ unless found_primary || found_unix
2534
+ RADUM::logger.log("\tDeleted group <#{group.name}>.", LOG_DEBUG)
2535
+ @ldap.delete :dn => group.distinguished_name
2536
+ check_ldap_result
2537
+ # Now that the group has been removed from Active Directory, it is
2538
+ # destroyed from the Container it belongs to. There is no need to
2539
+ # care about it anymore.
2540
+ RADUM::logger.log("\tDestroying group <#{group.name}>.", LOG_DEBUG)
2541
+ group.container.destroy_group group
2542
+ else
2543
+ RADUM::logger.log("\tCannot delete group <#{group.name}>:", LOG_DEBUG)
2544
+
2545
+ if found_primary
2546
+ RADUM::logger("\t<#{group.name}> is the primary Windows group for a" +
2547
+ " user in Active Directory.", LOG_DEBUG)
2548
+ end
2549
+
2550
+ if found_unix
2551
+ RADUM::logger.log("\t<#{group.name}> is the UNIX main group for a" +
2552
+ " user in Active Directory.", LOG_DEBUG)
2553
+ end
2554
+ end
2555
+ end
2556
+
2557
+ # Create a Group or UNIXGroup in Active Directory. The Group or UNIXGroup
2558
+ # must have its loaded attribute set to false, which indicates it was
2559
+ # manually created. This method checks that, so it is not necessary to
2560
+ # worry about checking first. This method also makes sure the group is not
2561
+ # already in Active Directory, in case someone created a group that would
2562
+ # match one that already exists. Therefore, any Group or UNIXGroup can be
2563
+ # passed into this method.
2564
+ #
2565
+ # === Parameter Types
2566
+ #
2567
+ # * group [Group or UNIXGroup]
2568
+ def create_group(group)
2569
+ unless group.loaded?
2570
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
2571
+ # The return value will be false explicitly if the search fails,
2572
+ # otherwise it will be an array of entries. Therefore it is important
2573
+ # to check for false explicitly for a failure. A failure indicates
2574
+ # that the group needs to be created.
2575
+ found = @ldap.search(:base => group.distinguished_name,
2576
+ :filter => group_filter,
2577
+ :scope => Net::LDAP::SearchScope_BaseObject,
2578
+ :return_result => false)
2579
+
2580
+ # The group should not already exist of course. This is to make sure
2581
+ # it is not already there in the case it was manually created but
2582
+ # matches a group that already exists.
2583
+ if found == false
2584
+ RADUM::logger.log("[AD #{self.root}]" +
2585
+ " create_group(<#{group.name}>)", LOG_DEBUG)
2586
+
2587
+ # Note that all the attributes need to be strings in this hash.
2588
+ attr = {
2589
+ :groupType => group.type.to_s,
2590
+ # All groups are of the objectclasses "top" and "group".
2591
+ :objectclass => ["top", "group"],
2592
+ :sAMAccountName => group.name
2593
+ }
2594
+
2595
+ attr.merge!({
2596
+ :gidNumber => group.gid.to_s,
2597
+ :msSFU30Name => group.name,
2598
+ :msSFU30NisDomain => group.nis_domain,
2599
+ :unixUserPassword => group.unix_password
2600
+ }) if group.instance_of?(UNIXGroup)
2601
+
2602
+ if group.instance_of?(UNIXGroup)
2603
+ attr.merge!({ :description => "UNIX group #{group.name}" })
2604
+ else
2605
+ attr.merge!({ :description => "Group #{group.name}" })
2606
+ end
2607
+
2608
+ RADUM::logger.log("\n" + attr.to_yaml + "\n\n", LOG_DEBUG)
2609
+ @ldap.add :dn => group.distinguished_name, :attributes => attr
2610
+ check_ldap_result
2611
+ end
2612
+
2613
+ # At this point, we need to pull the RID value back out and set it
2614
+ # because it is needed later. This is needed even if the group was
2615
+ # found and not created because it had not been loaded yet. Later
2616
+ # calls like create_user() look for the RID of the primary_group.
2617
+ entry = @ldap.search(:base => group.distinguished_name,
2618
+ :filter => group_filter,
2619
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
2620
+ group.set_rid sid2rid_int(entry.objectSid.pop)
2621
+ # Note: unlike a user, the group cannot be considered loaded at this
2622
+ # point because we have not handled any group memberships that might
2623
+ # have been set. Users at this poing in the create_user() method
2624
+ # can be considered loaded. Just noting this for my own reference.
2625
+ end
2626
+ end
2627
+
2628
+ # Update a Group or UNIXGroup in Active Directory. This method automatically
2629
+ # determines which attributes to update. The Group or UNIXGroup object must
2630
+ # exist in Active Directory or this method does nothing. This is checked, so
2631
+ # it is safe to pass any Group or UNIXGroup object to this method.
2632
+ #
2633
+ # === Parameter Types
2634
+ #
2635
+ # * group [Group or UNIXGroup]
2636
+ def update_group(group)
2637
+ if group.modified?
2638
+ RADUM::logger.log("[AD #{self.root}]" +
2639
+ " update_group(<#{group.name}>)", LOG_DEBUG)
2640
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
2641
+ entry = @ldap.search(:base => group.distinguished_name,
2642
+ :filter => group_filter,
2643
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
2644
+ attr = group_ldap_entry_attr(entry)
2645
+ ops = []
2646
+ RADUM::logger.log("\tKey: AD Value =? Object Value", LOG_DEBUG)
2647
+
2648
+ attr.keys.each do |key|
2649
+ # All keys in the attr hash apply to UNIXGroups, but some do not apply
2650
+ # to Groups. This is the easiest way to filter out inappropriate
2651
+ # checking.
2652
+ begin
2653
+ obj_value = group.send(key)
2654
+ rescue NoMethodError
2655
+ next
2656
+ end
2657
+
2658
+ ad_value = attr[key]
2659
+ RADUM::logger.log("\t#{key}: #{ad_value} =? #{obj_value}", LOG_DEBUG)
2660
+
2661
+ # Some attributes are integers and some are Strings, but they are
2662
+ # always Strings coming out of Active Directory. This is a safe
2663
+ # step to ensure a correct comparision.
2664
+ if ad_value.to_s != obj_value.to_s
2665
+ case key
2666
+ when :gid
2667
+ ops.push [:replace, :gidNumber, obj_value.to_s]
2668
+ when :nis_domain
2669
+ ops.push [:replace, :msSFU30NisDomain, obj_value]
2670
+ when :unix_password
2671
+ ops.push [:replace, :unixUserPassword, obj_value]
2672
+ end
2673
+ end
2674
+ end
2675
+
2676
+ begin
2677
+ entry.member.each do |member|
2678
+ # Groups can contain users and groups, so we need to check both
2679
+ # just in case. You can't tell from the DN which is which here.
2680
+ # First we remove any DNs that we've explicitly removed. Anything
2681
+ # we don't know about will be ignored.
2682
+ #
2683
+ # This check finds users or groups that were removed from the
2684
+ # container. This means the user has been deleted from Active
2685
+ # Directory in the AD object. It also finds users or groups that
2686
+ # were explicitly removed from the group.
2687
+ removed_group = find_group_by_dn(member, true)
2688
+ removed_user = find_user_by_dn(member, true)
2689
+ # The _membership versions find removed memberships for groups or
2690
+ # users who have not actually been removed from the AD object. This
2691
+ # reflects simple group and user membership changes, not removing
2692
+ # a user or group.
2693
+ removed_group_membership = find_group_by_dn(member)
2694
+
2695
+ unless group.removed_groups.include?(removed_group_membership)
2696
+ removed_group_membership = false
2697
+ end
2698
+
2699
+ removed_user_membership = find_user_by_dn(member)
2700
+
2701
+ unless group.removed_users.include?(removed_user_membership)
2702
+ removed_user_membership = false
2703
+ end
2704
+
2705
+ if removed_group || removed_user || removed_group_membership ||
2706
+ removed_user_membership
2707
+ ops.push [:delete, :member, member]
2708
+ user = removed_user || removed_user_membership
2709
+
2710
+ # There is a special case here with the last condition. If the
2711
+ # user is a UNIX member of the group and the group is the primary
2712
+ # Windows group, meaning we've removed their Windows membership
2713
+ # above (because it is implicit), we don't want to blow away the
2714
+ # UNIX membership too.
2715
+ if user && user.instance_of?(UNIXUser) &&
2716
+ group.instance_of?(UNIXGroup) && group != user.primary_group
2717
+ # There is a chance the user was never a UNIX member of this
2718
+ # UNIXGroup. This happens if the UNIX main group is changed
2719
+ # and then the user is removed from the group as well. We
2720
+ # should really also search the memberUid attribute, but
2721
+ # really... it should always match up with msSFU30PosixMember.
2722
+ begin
2723
+ found = entry.msSFU30PosixMember.find do |member|
2724
+ user.distinguished_name.downcase == member.downcase
2725
+ end
2726
+
2727
+ if found
2728
+ ops.push [:delete, :memberUid, user.username]
2729
+ ops.push [:delete, :msSFU30PosixMember, member]
2730
+ end
2731
+ rescue NoMethodError
2732
+ end
2733
+ end
2734
+ end
2735
+ end
2736
+ rescue NoMethodError
2737
+ end
2738
+
2739
+ # Now add any users or groups that are not already in the members
2740
+ # attribute array. We don't want to add the same thing twice because
2741
+ # this actually seems to duplicate the entries.
2742
+ (group.users + group.groups).each do |item|
2743
+ # As in the above begin block, the member attribute might not exist.
2744
+ # We have to take that into account.
2745
+ found = false
2746
+
2747
+ begin
2748
+ found = entry.member.find do |member|
2749
+ item.distinguished_name.downcase == member.downcase
2750
+ end
2751
+ rescue NoMethodError
2752
+ end
2753
+
2754
+ # There is an order of operations issue here. This method is called
2755
+ # before update_user() is called. If this group was the previous
2756
+ # user's primary Windows group (meaning we changed it), then this
2757
+ # code would try and add the user as a member of that group - as it
2758
+ # should. However, since we've not actually updated the user yet,
2759
+ # they are still a member of this group by way of their user current
2760
+ # account primaryGroupID attribute. When that attribute is updated
2761
+ # in the update_user() method, the group membership we are trying
2762
+ # to do here will be done implicitly. Trying to add the user as a
2763
+ # member here will not cause the sync() method to die, but it will
2764
+ # generate an LDAP error return message. We should avoid that.
2765
+ # The solution is to check if the object represented by the member
2766
+ # variable (which is a distinguished name) is:
2767
+ #
2768
+ # 1. A user account.
2769
+ # 2. A user account that has this group as its primaryGroupID still.
2770
+ #
2771
+ # If those two cases are true, we won't add the user as a member here
2772
+ # to avoid an LDAP error return message. Instead, the membership will
2773
+ # be implicitly dealt with when update_user() updates the user account
2774
+ # attributes. If this is not the case, we do add them as a member.
2775
+ if item.instance_of?(User) || item.instance_of?(UNIXUser)
2776
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
2777
+ obj = @ldap.search(:base => item.distinguished_name,
2778
+ :filter => user_filter,
2779
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
2780
+ curr_primary_group_id = obj.primaryGroupID.pop.to_i
2781
+
2782
+ unless found || curr_primary_group_id == group.rid
2783
+ ops.push [:add, :member, item.distinguished_name]
2784
+ end
2785
+ else
2786
+ # This is a Group or UNIXGroup member, which can just be added if
2787
+ # not already there.
2788
+ ops.push [:add, :member, item.distinguished_name] unless found
2789
+ end
2790
+
2791
+ # UNIX main group memberships are handled in update_user() when there
2792
+ # are changes if necessary.
2793
+ if item.instance_of?(UNIXUser) && group.instance_of?(UNIXGroup) &&
2794
+ group != item.unix_main_group
2795
+ # As with the member attribute, the msSFU30PosixMember attribute
2796
+ # might not exist yet either.
2797
+ found = false
2798
+
2799
+ begin
2800
+ # We should really also search the memberUid attribute, but
2801
+ # really... it should always match up with msSFU30PosixMember.
2802
+ found = entry.msSFU30PosixMember.find do |member|
2803
+ item.distinguished_name.downcase == member.downcase
2804
+ end
2805
+ rescue NoMethodError
2806
+ end
2807
+
2808
+ unless found
2809
+ ops.push [:add, :memberUid, item.username]
2810
+ ops.push [:add, :msSFU30PosixMember, item.distinguished_name]
2811
+ end
2812
+ end
2813
+ end
2814
+
2815
+ unless ops.empty?
2816
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
2817
+ @ldap.modify :dn => group.distinguished_name, :operations => ops
2818
+ check_ldap_result
2819
+ # At this point the group is the equivalent of a loaded group. Calling
2820
+ # this flags that fact as well as setting the hidden modified
2821
+ # attribute to false since we are up to date now.
2822
+ group.set_loaded
2823
+ else
2824
+ # The group did not need to be updated, so it can also be considered
2825
+ # loaded.
2826
+ group.set_loaded
2827
+ RADUM::logger.log("\tNo need to update group <#{group.name}>.",
2828
+ LOG_DEBUG)
2829
+ end
2830
+ end
2831
+ end
2832
+
2833
+ # Delete a User or UNIXUser from Active Directory.
2834
+ #
2835
+ # === Parameter Types
2836
+ #
2837
+ # * user [User or UNIXUser]
2838
+ def delete_user(user)
2839
+ RADUM::logger.log("[AD #{self.root}]" +
2840
+ " delete_user(<#{user.username}>)", LOG_DEBUG)
2841
+ @ldap.delete :dn => user.distinguished_name
2842
+ check_ldap_result
2843
+ # Destroying the user is delayed until the end of the AD#sync call because
2844
+ # we still need a reference indicating the user was removed from Active
2845
+ # Directory to ensure the user's UNIX group membership attributes are
2846
+ # updated properly in AD#update_group if necessary. The user's Windows
2847
+ # group mebershpips were taken care of simply because it was deleted from
2848
+ # Active Directory.
2849
+ end
2850
+
2851
+ # Create a User or UNIXUser in Active Directory. The User or UNIXUser
2852
+ # must have its loaded attribute set to false, which indicates it was
2853
+ # manually created. This method checks that, so it is not necessary to
2854
+ # worry about checking first. This method also makes sure the user is not
2855
+ # already in Active Directory, in case someone created a user that would
2856
+ # match one that already exists. Therefore, any User or UNIXUser can be
2857
+ # passed into this method.
2858
+ #
2859
+ # === Parameter Types
2860
+ #
2861
+ # * user [User or UNIXUser]
2862
+ def create_user(user)
2863
+ unless user.loaded?
2864
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
2865
+ # The return value will be false explicitly if the search fails,
2866
+ # otherwise it will be an array of entries. Therefore it is important
2867
+ # to check for false explicitly for a failure. A failure indicates
2868
+ # that the user needs to be created.
2869
+ found = @ldap.search(:base => user.distinguished_name,
2870
+ :filter => user_filter,
2871
+ :scope => Net::LDAP::SearchScope_BaseObject,
2872
+ :return_result => false)
2873
+
2874
+ # The user should not already exist of course. This is to make sure
2875
+ # it is not already there.
2876
+ if found == false
2877
+ RADUM::logger.log("[AD #{self.root}]" +
2878
+ " create_user(<#{user.username}>)", LOG_DEBUG)
2879
+ # We need the RID of the user's primary Windows group, and at this
2880
+ # point the create_group() method has grabbed the RID for any group
2881
+ # that was not loaded. Only groups in the RADUM environment can be
2882
+ # specified as the primary group, so this should always give the
2883
+ # primary group RID.
2884
+ rid = user.primary_group.rid
2885
+
2886
+ # We want to be sure though! The RID stuff is here so that we don't
2887
+ # even create a user if there is a primary Windows group RID issue.
2888
+ if rid.nil?
2889
+ RADUM::logger.log("SYNC ERROR: RID of " +
2890
+ " <#{user.primary_group.name}> was nil.",
2891
+ LOG_NORMAL)
2892
+ return
2893
+ end
2894
+
2895
+ # Note that all the attributes need to be strings in this hash.
2896
+ attr = {
2897
+ # All users are of the objectclasses "top", "person",
2898
+ # "orgainizationalPerson", and "user".
2899
+ :objectclass => ["top", "person", "organizationalPerson", "user"],
2900
+ :sAMAccountName => user.username,
2901
+ :userAccountControl => (UF_NORMAL_ACCOUNT + UF_PASSWD_NOTREQD +
2902
+ UF_ACCOUNTDISABLE).to_s
2903
+ }
2904
+
2905
+ description = ""
2906
+
2907
+ # These are optional attributes.
2908
+ unless user.first_name.nil?
2909
+ attr.merge!({ :givenName => user.first_name })
2910
+ description += "#{user.first_name}"
2911
+ end
2912
+
2913
+ unless user.initials.nil?
2914
+ attr.merge!({ :initials => user.initials })
2915
+ description += " #{user.initials}."
2916
+ end
2917
+
2918
+ unless user.middle_name.nil?
2919
+ attr.merge!({ :middleName => user.middle_name })
2920
+ end
2921
+
2922
+ unless user.surname.nil?
2923
+ attr.merge!({ :sn => user.surname })
2924
+ description += " #{user.surname}"
2925
+ end
2926
+
2927
+ # We should set these to something in case they were not set.
2928
+ if description == ""
2929
+ description = user.username
2930
+ end
2931
+
2932
+ realm = user.username + "@#{@domain}"
2933
+
2934
+ attr.merge!({
2935
+ :displayName => description,
2936
+ :description => description,
2937
+ :userPrincipalName => realm
2938
+ })
2939
+
2940
+ unless user.script_path.nil?
2941
+ attr.merge!({ :scriptPath => user.script_path })
2942
+ end
2943
+
2944
+ unless user.profile_path.nil?
2945
+ attr.merge!({ :profilePath => user.profile_path })
2946
+ end
2947
+
2948
+ if user.local_drive && user.local_path
2949
+ attr.merge!({
2950
+ :homeDrive => user.local_drive,
2951
+ :homeDirectory => user.local_path
2952
+ })
2953
+ elsif user.local_path
2954
+ attr.merge!({ :homeDirectory => user.local_path })
2955
+ end
2956
+
2957
+ attr.merge!({
2958
+ :gecos => user.gecos,
2959
+ :gidNumber => user.unix_main_group.gid.to_s,
2960
+ :loginShell => user.shell,
2961
+ :msSFU30Name => user.username,
2962
+ :msSFU30NisDomain => user.nis_domain,
2963
+ :uidNumber => user.uid.to_s,
2964
+ :unixHomeDirectory => user.home_directory,
2965
+ :unixUserPassword => user.unix_password
2966
+ }) if user.instance_of?(UNIXUser)
2967
+
2968
+ # The shadow file attributes are all optional, so we need to check
2969
+ # each one. The other UNIX attributes above are set to something
2970
+ # by default.
2971
+ if user.instance_of?(UNIXUser)
2972
+ unless user.shadow_expire.nil?
2973
+ attr.merge!({:shadowExpire => user.shadow_expire.to_s})
2974
+ end
2975
+
2976
+ unless user.shadow_flag.nil?
2977
+ attr.merge!({:shadowFlag => user.shadow_flag.to_s})
2978
+ end
2979
+
2980
+ unless user.shadow_inactive.nil?
2981
+ attr.merge!({:shadowInactive => user.shadow_inactive.to_s})
2982
+ end
2983
+
2984
+ unless user.shadow_last_change.nil?
2985
+ attr.merge!({:shadowLastChange => user.shadow_last_change.to_s})
2986
+ end
2987
+
2988
+ unless user.shadow_max.nil?
2989
+ attr.merge!({:shadowMax => user.shadow_max.to_s})
2990
+ end
2991
+
2992
+ unless user.shadow_min.nil?
2993
+ attr.merge!({:shadowMin => user.shadow_min.to_s})
2994
+ end
2995
+
2996
+ unless user.shadow_warning.nil?
2997
+ attr.merge!({:shadowWarning => user.shadow_warning.to_s})
2998
+ end
2999
+ end
3000
+
3001
+ RADUM::logger.log("\n" + attr.to_yaml + "\n\n", LOG_DEBUG)
3002
+ @ldap.add :dn => user.distinguished_name, :attributes => attr
3003
+ check_ldap_result
3004
+
3005
+ # Modify the attributes for the user password and userAccountControl
3006
+ # value to enable the account.
3007
+ user_status = UF_NORMAL_ACCOUNT
3008
+ user_status += UF_ACCOUNTDISABLE if user.disabled?
3009
+
3010
+ if user.password.nil?
3011
+ user.password = random_password
3012
+ RADUM::logger.log("\tGenerated password #{user.password} for" +
3013
+ " <#{user.username}>.", LOG_DEBUG)
3014
+ end
3015
+
3016
+ ops = [
3017
+ [:replace, :unicodePwd, str2utf16le(user.password)],
3018
+ [:replace, :userAccountControl, user_status.to_s]
3019
+ ]
3020
+
3021
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
3022
+ @ldap.modify :dn => user.distinguished_name, :operations => ops
3023
+ check_ldap_result
3024
+
3025
+ # Set the user's password to nil. When a password has a value, that
3026
+ # means we need to set it, otherwise it should be nil. We just
3027
+ # set it, so we don't want the update set to try and set it again.
3028
+ user.password = nil
3029
+
3030
+ # If the user has to change their password, it must be done below
3031
+ # and not in the previous step that set their password because it
3032
+ # will ignore the additional flag (which I've commented out near
3033
+ # the top of this file because it does not work now). This works.
3034
+ if user.must_change_password?
3035
+ ops = [
3036
+ [:replace, :pwdLastSet, 0.to_s]
3037
+ ]
3038
+
3039
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
3040
+ @ldap.modify :dn => user.distinguished_name, :operations => ops
3041
+ check_ldap_result
3042
+ end
3043
+
3044
+ # The user already has the primary Windows group as Domain Users
3045
+ # based on the default actions above. If the user has a different
3046
+ # primary Windows group, it is necessary to add the user to that
3047
+ # group first (as a member in the member attribute for the group)
3048
+ # before attempting to set their primaryGroupID attribute or Active
3049
+ # Directory will refuse to do it. Note that there is no guarentee
3050
+ # that AD#load() has been called yet, so the Domain Users group
3051
+ # might not even be in the RADUM system. The safest way to check
3052
+ # if the user's primary Windows group is Domain Users is as done
3053
+ # below.
3054
+ unless user.primary_group.name == "Domain Users"
3055
+ ops = [
3056
+ [:add, :member, user.distinguished_name]
3057
+ ]
3058
+
3059
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
3060
+ @ldap.modify :dn => user.primary_group.distinguished_name,
3061
+ :operations => ops
3062
+ check_ldap_result
3063
+
3064
+ ops = [
3065
+ [:replace, :primaryGroupID, rid.to_s]
3066
+ ]
3067
+
3068
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
3069
+ @ldap.modify :dn => user.distinguished_name, :operations => ops
3070
+ check_ldap_result
3071
+ # The user has now been made a regular member of the Domain Users
3072
+ # Windows group. This has been handled in Active Directory for us,
3073
+ # but now we want to reflect that in the Domain Users Group object
3074
+ # here. There is a problem however. It is possible that the
3075
+ # Domain Users group has not been loaded into the RADUM environment
3076
+ # yet. Therefore, we check first before trying.
3077
+ domain_users = find_group_by_name("Domain Users")
3078
+ domain_users.add_user user if domain_users
3079
+ end
3080
+
3081
+ # At this point, we need to pull the RID value back out and set it
3082
+ # because it is needed later. Actually, it isn't for users, but
3083
+ # I am pretending it is just as important because I am tracking
3084
+ # RIDs anyway (they are in a flat namespace).
3085
+ entry = @ldap.search(:base => user.distinguished_name,
3086
+ :filter => user_filter,
3087
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
3088
+ user.set_rid sid2rid_int(entry.objectSid.pop)
3089
+ # At this point the user is the equivalent as a loaded user.
3090
+ # Calling this flags that fact as well as setting the hidden
3091
+ # modified attribute to false since we are up to date now. Note
3092
+ # that the groups attribute is still not 100% accurate. It will
3093
+ # be dealt with later when groups are dealt with.
3094
+ user.set_loaded
3095
+ end
3096
+ end
3097
+ end
3098
+
3099
+ # Update a User or UNIXUser in Active Directory. This method automatically
3100
+ # determines which attributes to update. The User or UNIXUser object must
3101
+ # exist in Active Directory or this method does nothing. This is checked, so
3102
+ # it is safe to pass any User or UNIXUser object to this method.
3103
+ #
3104
+ # === Parameter Types
3105
+ #
3106
+ # * user [User or UNIXUser]
3107
+ def update_user(user)
3108
+ if user.modified?
3109
+ RADUM::logger.log("[AD #{self.root}]" +
3110
+ " update_user(<#{user.username}>)", LOG_DEBUG)
3111
+ user_filter = Net::LDAP::Filter.eq("objectclass", "user")
3112
+ entry = @ldap.search(:base => user.distinguished_name,
3113
+ :filter => user_filter,
3114
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
3115
+ attr = user_ldap_entry_attr(entry)
3116
+ ops = []
3117
+ # This for the UNIX group membership corner case below. Note that 0
3118
+ # is the case where the GID has been removed (UNIXUser converted to
3119
+ # a User). No one should have a GID of 0, so this is just to make the
3120
+ # check to proceed easier.
3121
+ old_gid = 0
3122
+ RADUM::logger.log("\tKey: AD Value =? Object Value", LOG_DEBUG)
3123
+
3124
+ attr.keys.each do |key|
3125
+ # All keys in the attr hash apply to UNIXUsers, but most do not apply
3126
+ # to Users. This is the easiest way to filter out inappropriate
3127
+ # checking.
3128
+ begin
3129
+ obj_value = user.send(key)
3130
+ rescue NoMethodError
3131
+ next
3132
+ end
3133
+
3134
+ ad_value = attr[key]
3135
+ RADUM::logger.log("\t#{key}: #{ad_value} =? #{obj_value}", LOG_DEBUG)
3136
+
3137
+ # Some attributes are integers and some are Strings, but they are
3138
+ # always Strings coming out of Active Directory. This is a safe
3139
+ # step to ensure a correct comparision.
3140
+ #
3141
+ # Note that in this case there is a comparision of the primary_group
3142
+ # value, which is represented as a Group/UNIXGroup object, but it
3143
+ # has a to_s() method that will work fine here. So yes, what I said
3144
+ # at first is not strictly true, but by "magic" this all works fine.
3145
+ # I mean, have you read this code? :-)
3146
+ if ad_value.to_s != obj_value.to_s
3147
+ case key
3148
+ when :disabled?
3149
+ user_status = UF_NORMAL_ACCOUNT
3150
+ user_status += UF_ACCOUNTDISABLE if obj_value
3151
+ ops.push [:replace, :userAccountControl, user_status.to_s]
3152
+ when :first_name
3153
+ ops.push [:replace, :givenName, obj_value]
3154
+ when :initials
3155
+ ops.push [:replace, :initials, obj_value]
3156
+ when :middle_name
3157
+ ops.push [:replace, :middleName, obj_value]
3158
+ when :surname
3159
+ ops.push [:replace, :sn, obj_value]
3160
+ when :script_path
3161
+ ops.push [:replace, :scriptPath, obj_value]
3162
+ when :profile_path
3163
+ ops.push [:replace, :profilePath, obj_value]
3164
+ when :local_path
3165
+ ops.push [:replace, :homeDirectory, obj_value]
3166
+ when :local_drive
3167
+ ops.push [:replace, :homeDrive, obj_value]
3168
+ when :primary_group
3169
+ @ldap.modify :dn => user.primary_group.distinguished_name,
3170
+ :operations => [[:add, :member,
3171
+ user.distinguished_name]]
3172
+ check_ldap_result
3173
+ ops.push [:replace, :primaryGroupID, user.primary_group.rid.to_s]
3174
+ when :shell
3175
+ ops.push [:replace, :loginShell, obj_value]
3176
+ when :home_directory
3177
+ ops.push [:replace, :unixHomeDirectory, obj_value]
3178
+ when :nis_domain
3179
+ ops.push [:replace, :msSFU30NisDomain, obj_value]
3180
+ when :gecos
3181
+ ops.push [:replace, :gecos, obj_value]
3182
+ when :unix_password
3183
+ ops.push [:replace, :unixUserPassword, obj_value]
3184
+ when :shadow_expire
3185
+ ops.push [:replace, :shadowExpire, obj_value.to_s]
3186
+ when :shadow_flag
3187
+ ops.push [:replace, :shadowFlag, obj_value.to_s]
3188
+ when :shadow_inactive
3189
+ ops.push [:replace, :shadowInactive, obj_value.to_s]
3190
+ when :shadow_last_change
3191
+ ops.push [:replace, :shadowLastChange, obj_value.to_s]
3192
+ when :shadow_max
3193
+ ops.push [:replace, :shadowMax, obj_value.to_s]
3194
+ when :shadow_min
3195
+ ops.push [:replace, :shadowMin, obj_value.to_s]
3196
+ when :shadow_warning
3197
+ ops.push [:replace, :shadowWarning, obj_value.to_s]
3198
+ when :gid
3199
+ old_gid = ad_value.to_i
3200
+ ops.push [:replace, :gidNumber, obj_value.to_s]
3201
+ when :uid
3202
+ ops.push [:replace, :uidNumber, obj_value.to_s]
3203
+ when :must_change_password?
3204
+ if obj_value
3205
+ ops.push [:replace, :pwdLastSet, 0.to_s]
3206
+ else
3207
+ ops.push [:replace, :pwdLastSet, -1.to_s]
3208
+ end
3209
+ end
3210
+ end
3211
+ end
3212
+
3213
+ # Update the LDAP description and displayName attributes. This only
3214
+ # updates them if they are different than what is currently there.
3215
+ description = ""
3216
+
3217
+ unless user.first_name.nil?
3218
+ description = "#{user.first_name}"
3219
+ end
3220
+
3221
+ unless user.initials.nil?
3222
+ description += " #{user.initials}."
3223
+ end
3224
+
3225
+ unless user.surname.nil?
3226
+ description += " #{user.surname}"
3227
+ end
3228
+
3229
+ curr_description = curr_display_name = nil
3230
+
3231
+ begin
3232
+ curr_description = entry.description.pop
3233
+ rescue NoMethodError
3234
+ end
3235
+
3236
+ begin
3237
+ curr_display_name = entry.displayName.pop
3238
+ rescue NoMethodError
3239
+ end
3240
+
3241
+ if description != curr_description
3242
+ ops.push [:replace, :description, description]
3243
+ end
3244
+
3245
+ if description != curr_display_name
3246
+ ops.push [:replace, :displayName, description]
3247
+ end
3248
+
3249
+ # If the password is set, change the user's password. Otherwise this
3250
+ # will be nil.
3251
+ unless user.password.nil?
3252
+ ops.push [:replace, :unicodePwd, str2utf16le(user.password)]
3253
+ # Set the user's password to nil. When a password has a value, that
3254
+ # means we need to set it, otherwise it should be nil. We just
3255
+ # set it, so we don't want the update set to try and set it again.
3256
+ user.password = nil
3257
+ end
3258
+
3259
+ # This is a corner case with the UNIX main group. Due to the
3260
+ # complications in implicit UNIX group membership, primary Windows
3261
+ # groups having users as implicit members, etc. we just make sure
3262
+ # the user is made a UNIX member of the previous UNIX main group
3263
+ # when it was changed just in case they are not already a member.
3264
+ #
3265
+ # Note that when converting a UNIXUser to a User, there will be a
3266
+ # gid change, but the gid will be "".to_i (0). In that case, we don't
3267
+ # want to proceed with this logic. This isn't C, so "if 0" is true.
3268
+ # The old_gid variable is initialized to 0 too in order to make this
3269
+ # check easier. None should have a GID of 0.
3270
+ unless old_gid == 0
3271
+ group_ops = []
3272
+ group_filter = Net::LDAP::Filter.eq("objectclass", "group")
3273
+ group = find_group_by_gid old_gid
3274
+ entry = @ldap.search(:base => group.distinguished_name,
3275
+ :filter => group_filter,
3276
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
3277
+ # Double check to make sure they are not already members. Since this
3278
+ # logic is difficult to deal with, the algorithm is simply to make
3279
+ # sure the UNIXUser is a member of their previous UNIX main group
3280
+ # if that has not been done by the update_group() method.
3281
+ found = false
3282
+
3283
+ begin
3284
+ found = entry.msSFU30PosixMember.find do |member|
3285
+ user.distinguished_name.downcase == member.downcase
3286
+ end
3287
+ rescue NoMethodError
3288
+ end
3289
+
3290
+ unless found
3291
+ group_ops.push [:add, :memberUid, user.username]
3292
+ group_ops.push [:add, :msSFU30PosixMember, user.distinguished_name]
3293
+ RADUM::logger.log("\nSpecial case 1: updating old UNIX main group" +
3294
+ " UNIX membership for group <#{group.name}>.",
3295
+ LOG_DEBUG)
3296
+ RADUM::logger.log("\n" + group_ops.to_yaml, LOG_DEBUG)
3297
+ @ldap.modify :dn => group.distinguished_name,
3298
+ :operations => group_ops
3299
+ check_ldap_result
3300
+ RADUM::logger.log("\nSpecial case 1: end.\n\n", LOG_DEBUG)
3301
+ end
3302
+
3303
+ # In this case, we also have to make sure the user is removed
3304
+ # from the new UNIX main group with respect to UNIX group membership.
3305
+ # This is because there is also a case where the UNIX main group is
3306
+ # being set to the primary Windows group, and thus would not cause
3307
+ # an update because the Windows group membership is implicit.
3308
+ group_ops = []
3309
+ group = user.unix_main_group
3310
+ entry = @ldap.search(:base => group.distinguished_name,
3311
+ :filter => group_filter,
3312
+ :scope => Net::LDAP::SearchScope_BaseObject).pop
3313
+ found = false
3314
+
3315
+ begin
3316
+ found = entry.msSFU30PosixMember.find do |member|
3317
+ user.distinguished_name.downcase == member.downcase
3318
+ end
3319
+ rescue NoMethodError
3320
+ end
3321
+
3322
+ if found
3323
+ group_ops.push [:delete, :memberUid, user.username]
3324
+ group_ops.push [:delete, :msSFU30PosixMember,
3325
+ user.distinguished_name]
3326
+ RADUM::logger.log("\nSpecial case 2: removing UNIX main group" +
3327
+ " UNIX membership for group <#{group.name}>.",
3328
+ LOG_DEBUG)
3329
+ RADUM::logger.log("\n" + group_ops.to_yaml, LOG_DEBUG)
3330
+ @ldap.modify :dn => group.distinguished_name,
3331
+ :operations => group_ops
3332
+ check_ldap_result
3333
+ RADUM::logger.log("\nSpecial case 2: end.\n\n", LOG_DEBUG)
3334
+ end
3335
+ end
3336
+
3337
+ unless ops.empty?
3338
+ RADUM::logger.log("\n" + ops.to_yaml + "\n\n", LOG_DEBUG)
3339
+ @ldap.modify :dn => user.distinguished_name, :operations => ops
3340
+ check_ldap_result
3341
+ # At this point the user is the equivalent as a loaded user. Calling
3342
+ # this flags that fact as well as setting the hidden modified
3343
+ # attribute to false since we are up to date now.
3344
+ user.set_loaded
3345
+ else
3346
+ # The user did not need to be updated, so it can also be considered
3347
+ # loaded.
3348
+ user.set_loaded
3349
+ RADUM::logger.log("\tNo need to update user <#{user.username}>.",
3350
+ LOG_DEBUG)
3351
+ end
3352
+ end
3353
+ end
3354
+ end
3355
+ end