231 end if end if next End Function 7.3.3 Discussion As described in Recipe 7.2, group membership is stored in the multivalued member attribute on group objects. But that attribute will not show the complete picture because group nesting is allowed in Active Directory after you've transitioned from mixed mode. To view the complete group membership, you have to recurse through each group's members. In the VBScript example, I used a dictionary object (referred to as a hash or associative array in other languages) to ensure I did not get in an infinite loop. The dictionary object stores each group member; before the DisplayMembers function is called a check is performed to determine if the group has already been evaluated. If so, a message is displayed indicating the group will not be processed again. If this type of checking was not employed and you had a situation where group A was a member of group B, group B was a member of group C, and group C was a member of group A, the loop would repeat without terminating. 7.3.4 See Also Recipe 7.2 for viewing group membership and MSDN: IADsMember Recipe 7.4 Adding and Removing Members of a Group 7.4.1 Problem You want to add or remove members of a group. 7.4.2 Solution 7.4.2.1 Using a graphical user interface 1. Follow the same steps as in Recipe 7.2 to view the members of the group. 2. To remove a member, click on the member name, click the Remove button, click Yes, and click OK. 3. To add a member, click on the Add button, enter the name of the member, and click OK twice. 7.4.2.2 Using a command-line interface The -addmbr option adds a member to a group: > dsmod group "<GroupDN>" -addmbr "<MemberDN>" 232 The -rmmbr option removes a member from a group: > dsmod group "<GroupDN>" -rmmbr "<MemberDN>" The -chmbr option replaces the complete membership list: > dsmod group "<GroupDN>" -chmbr "<Member1DN Member2DN . . . >" 7.4.2.3 Using VBScript ' This code adds a member to a group. ' SCRIPT CONFIGURATION strGroupDN = "<GroupDN>" ' e.g. cn=SalesGroup,ou=Groups,dc=rallencorp,dc=com strMemberDN = "<MemberDN>" ' e.g. cn=jsmith,cn=users,dc=rallencorp,dc=com ' END CONFIGURATION set objGroup = GetObject("LDAP://" & strGroupDN) ' Add a member objGroup.Add("LDAP://" & strMemberDN) ' This code removes a member from a group. ' SCRIPT CONFIGURATION strGroupDN = "<GroupDN>" ' e.g. cn=SalesGroup,ou=Groups,dc=rallencorp,dc=com strMemberDN = "<MemberDN>" ' e.g. cn=jsmith,cn=users,dc=rallencorp,dc=com ' END CONFIGURATION set objGroup = GetObject("LDAP://" & strGroupDN) ' Remove a member objGroup.Remove("LDAP://" & strMemberDN) 7.4.3 Discussion Since there are no restrictions on what distinguished names you put in the member attribute, you can essentially have any type of object as a member of a group, which makes groups very useful. While Organizational Units (OUs) are typically used to structure objects that share certain criteria, group objects can be used to create loose collections of objects. The benefit of using group objects as a collection mechanism is that the same object can be a member of multiple groups whereas an object can only be a part of a single OU. Another key difference is that you can assign permissions on resources to groups because they are considered security principals in Active Directory, whereas OUs are not. This is different from some other directories, such as Novel Netware, where OUs act more like security principals. 7.4.4 See Also Recipe 7.2 for viewing group membership, MSDN: IADsGroup::Add, and MSDN: IADsGroup::Remove 233 Recipe 7.5 Moving a Group 7.5.1 Problem You want to move a group to a different OU or domain. 7.5.2 Solution To move a group to a different OU, follow the instructions in Recipe 4.17. To move a group to a different domain, follow the instructions in Recipe 4.18. 7.5.3 Discussion The only type of group that can be moved between domains are universal groups. If you want to move a global or domain local group to a different domain, first convert it to a universal group, move the group, then convert it back to a global or domain local group. When you convert a group between types, you may encounter problems because different groups have different membership restrictions. See Introduction in Chapter 7 for more information on grou type membership restrictions. A much easier way to accomplish inter-domain group moves is by using the Active Directory Migration Tool (ADMT). With ADMT, you can move and restructure groups without needing to go to all the trouble of converting the group to a universal and modifying the group membership. For more information on ADMT, see the following site: http://www.microsoft.com/windows2000/downloads/tools/admt/default.asp 7.5.4 See Also Recipe 4.17 for moving an object to a different OU, Recipe 4.18 for moving an object to a different domain, and Recipe 7.6 for changing group scope and type Recipe 7.6 Changing the Scope or Type of a Group 7.6.1 Problem You want to change the scope or type of a group. 7.6.2 Solution 7.6.2.1 Using a graphical user interface 1. Open the Active Directory Users and Computers snap-in. 234 2. If you need to change domains, right-click on Active Directory Users and Computers in the left pane, select Connect to Domain, enter the domain name, and click OK. 3. In the left pane, right-click on the domain and select Find. 4. Enter the name of the group you want to modify and click Find Now. 5. Double-click on the group in the results pane. 6. In the group properties dialog box, select the new scope or type and click OK. 7.6.2.2 Using a command-line interface The following example changes the group scope for <GroupDN> to <NewScope>, which should be l for domain local group, g for global group, or u for universal group. > dsmod group "<GroupDN>" -scope <NewScope> The following example changes the group type for <GroupDN>. For the -secgrp switch, specify yes to change to a security group or no to make the group a distribution list. > dsmod group "<GroupDN>" -secgrp yes|no 7.6.2.3 Using VBScript ' This code sets the scope and type of the specified group ' to a universal security group. ' SCRIPT CONFIGURATION strGroupDN = "<GroupDN>" ' e.g. cn=SalesGroup,ou=Groups,dc=rallencorp,dc=com ' END CONFIGURATION ' Constants taken from ADS_GROUP_TYPE_ENUM ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP = 1 ADS_GROUP_TYPE_GLOBAL_GROUP = 2 ADS_GROUP_TYPE_LOCAL_GROUP = 4 ADS_GROUP_TYPE_SECURITY_ENABLED = -2147483648 ADS_GROUP_TYPE_UNIVERSAL_GROUP = 8 set objGroup = GetObject("LDAP://" & strGroupDN ) objGroup.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP _ Or ADS_GROUP_TYPE_SECURITY_ENABLED objGroup.SetInfo 7.6.3 Discussion Group scope and type are stored as a flag in the groupType attribute on group objects. To directly update groupType, you must logically OR the values associated with each type and scope, as shown in the API solution. Note that there is no specific value for the distribution list type. If you want to create a distribution list, just do not include the ADS_GROUP_TYPE_SECURITY_ENABLED flag when setting groupType. For a good description of the usage scenarios for each group type, see Chapter 11 in Active Directory, Second Edition. 235 7.6.4 See Also MS KB 231273 (Group Type and Scope Usage in Windows), MSDN: ADS_GROUP_TYPE_ENUM, and MSDN: What Type of Group to Use Recipe 7.7 Delegating Control for Managing Membership of a Group 7.7.1 Problem You want to delegate control of managing the membership of a group. 7.7.2 Solution 7.7.2.1 Using a graphical user interface This is a new feature of Windows Server 2003 version of ADUC. 1. Open the Active Directory Users and Computers snap-in. 2. If you need to change domains, right-click on Active Directory Users and Computers in the left pane, select Connect to Domain, enter the domain name, and click OK. 3. In the left pane, right-click on the domain and select Find. 4. Enter the name of the group and click Find Now. 5. Double-click on the group in the results pane. 6. Select the Managed By tab. 7. Click the Change button. 8. Locate the group or user to delegate control to and click OK. 9. Check the box beside Manager can update membership list. 10. Click OK. 7.7.2.2 Using a command-line interface > dsacls <GroupDN> /G <GroupName>@DomainName:WP;member; In the following example, the SalesAdmin group will be given rights to modify membership of the PreSales group. > dsacls cn=presales,ou=sales,dc=rallencorp,dc=com /G salesadmins@rallencorp.com:[RETURN] WP;member; 7.7.2.3 Using VBScript ' This code grants write access to the member attribute of a group. ' SCRIPT CONFIGURATION strGroupDN = "<GroupDN>" ' e.g. cn=SalesGroup,ou=Sales,dc=rallencorp,dc=com" 236 strUserOrGroup = "<UserOrGroup>" ' e.g. joe@rallencorp.com or RALLENCORP\joe ' END CONFIGURATION set objGroup = GetObject("LDAP://" & strGroupDN) '############################ ' Constants '############################ ' ADS_ACETYPE_ENUM Const ADS_ACETYPE_ACCESS_ALLOWED_OBJECT = &h5 Const ADS_FLAG_OBJECT_TYPE_PRESENT = &h1 Const ADS_RIGHT_DS_WRITE_PROP = &h20 ' From schemaIDGUID of member attribute Const MEMBER_ATTRIBUTE = "{bf9679c0-0de6-11d0-a285-00aa003049e2}" '############################ ' Create ACL '############################ set objSD = objGroup.Get("ntSecurityDescriptor") set objDACL = objSD.DiscretionaryAcl ' Set WP for member attribute set objACE = CreateObject("AccessControlEntry") objACE.Trustee = strUserOrGroup objACE.AccessMask = ADS_RIGHT_DS_WRITE_PROP objACE.AceFlags = 0 objACE.Flags = ADS_FLAG_OBJECT_TYPE_PRESENT objACE.AceType = ADS_ACETYPE_ACCESS_ALLOWED_OBJECT objACE.ObjectType = MEMBER_ATTRIBUTE objDACL.AddAce objACE '############################ ' Set ACL '############################ objSD.DiscretionaryAcl = objDACL objGroup.Put "ntSecurityDescriptor", objSD objGroup.SetInfo WScript.Echo "Delegated control of member attribute for " & _ strGroupDN & " to " & strUserOrGroup 7.7.3 Discussion To grant a user or group the ability to manage group membership, you have to grant the write property (WP) permission on the member attribute of the target group. You can add this ACE directly using dsacls or more indirectly with ADUC. ADUC in Windows Server 2003 has a new feature that allows you to simply check a box to grant the ability to modify group membership to the object represented by the managedBy attribute. If you want to configure additional permissions, such as the ability to modify the description attribute for the group, you will need to go to the Security tab in ADUC, or specify the appropriate attribute with the /G switch with dsacls. For example, this will grant write property on the description attribute: 237 /G <GroupName>@DomainDNSName:WP;description; 7.7.4 See Also Recipe 14.10 for delegating control in Active Directory Recipe 7.8 Resolving a Primary Group ID 7.8.1 Problem You want to find the name of a user's primary group. 7.8.2 Solution 7.8.2.1 Using a graphical user interface 1. Open the Active Directory Users and Computers snap-in. 2. If you need to change domains, right-click on Active Directory Users and Computers in the left pane, select Connect to Domain, enter the domain name, and click OK. 3. In the left pane, right-click on the domain and select Find. 4. Type the name of the user and click Find Now. 5. In the Search Results, double-click on the user. 6. Click the Member Of tab. 7. The Primary Group name is shown on the bottom half of the dialog box. 7.8.2.2 Using VBScript ' This code prints the group name of a user's primary group ' SCRIPT CONFIGURATION strNTDomain = "<DomainName>" ' NetBios Name of the AD domain, e.g. RALLENCORP strUser = "<UserName>" ' e.g. Administrator ' END CONFIGURATION ' Iterate over the user's groups and create a search filter ' that contains each group set objUser = GetObject("WinNT://" & strNTDomain & "/" & strUser & ",user") strFilter = "" for each objGroup in objUser.Groups strFilter = strFilter & "(samAccountName=" & objGroup.Name & ")" next strFilter = "(|" & strFilter & ")" ' Now need to perform a search to retrieve each group ' and their primaryGroupToken strBase = "<LDAP://" & strNTDomain & ">;" strFilter = "(&(objectcategory=group)" & strFilter & ");" strAttrs = "name,primaryGroupToken,cn;" strScope = "subtree;" set objConn = CreateObject("ADODB.Connection") objConn.Provider = "ADsDSOObject" objConn.Open "Active Directory Provider" 238 set objComm = CreateObject("ADODB.Command") set objComm.ActiveConnection = objConn objComm.CommandText = strBase & strFilter & strAttrs & strScope ' Be sure to enable paging in case number of groups > 1000 objComm.Properties("Page Size") = 1000 set objRS = objComm.Execute ' Iterate over each group again and stop after a match with the user's ' primaryGroupID has been made strPrimaryGroup = "" while ( (not objRS.EOF) and (strPrimaryGroup = "") ) if (objUser.PrimaryGroupID = objRS.Fields("primaryGroupToken").value) then strPrimaryGroup = objRS.Fields("name").Value end if objRS.moveNext wend objConn.Close WScript.Echo "Primary Group for " & strUser & " is " & strPrimaryGroup & _ " (" & objUser.PrimaryGroupID & ")" 7.8.3 Discussion When trying to determine a user's group membership, you have to look at both user's memberOf attribute, which contains a list of DNs for each group the user is a member of, and the user's primary group. By default, all users are assigned Domain Users as their primary group. Therefore, by default all users in a domain are implicitly members of the Domain Users group. Unfortunately, a user's primary group will not show up in the memberOf attribute unless explicitly added. Services for Macintosh and POSIX-based applications are the main users of primary groups. If you don't use either of those, you don't need to worry about changing a user's primary group. The primary group is stored in the primaryGroupID attribute on user objects. Unfortunately, the RID of the group is stored in that attribute, not the DN or even sAMAccountName as you might expect. group objects have a primaryGroupToken attribute, which contains the same value, but is a constructed attribute. Because Active Directory dynamically constructs it, you cannot utilize it in search filters. So even if you have the primaryGroupID of a user, e.g., 513, you cannot do a simple query to find out which group it is associated with. You can find the name of a user's primary group relatively easily using the Active Directory Users and Computers snap-in as I described in the GUI solution. Finding it via a script, on the other hand, is considerably more complicated. There are a few different ways to go about determining a group given a primary group ID and they are covered pretty well in MS KB 321360 and 297951. For the API solution, I use the approach I feel is the most efficient. I first used the WinNT: provider to retrieve a user's groups. The difference between using the WinNT: provider and using the LDAP: provider is that the WinNT: provider returns the primary 239 group as part of the IADsGroup collection whereas the LDAP: provider does not. Unfortunately, there is no indication which of the groups is the primary group. So I needed to iterate over each group and build an LDAP filter that will be used later to retrieve each group using ADO. After I execute the ADO query, I then iterate over each group and check the primaryGroupToken attribute of that group to see if it matches the user's primaryGroupID attribute. If it does, I've found the user's primary group. 7.8.4 See Also MS KB 297951 (HOWTO: Use the PrimaryGroupID Attribute to Find the Primary Group for a User) and MS KB 321360 (How to Use Native ADSI Components to Find the Primary Group) Recipe 7.9 Enabling Universal Group Membership Caching This recipe requires the Windows Server 2003 forest functional level. 7.9.1 Problem You want to enable universal group membership caching so that a global catalog server is not needed during user logins. 7.9.2 Solution 7.9.2.1 Using a graphical user interface 1. Open the Active Directory Sites and Services snap-in. 2. In the left pane, browse to the site you want to enable group caching for and click on it. 3. In the right pane, double-click on the NTDS Site Settings object. 4. Under Universal Group Membership Caching, check the box beside Enable Universal Group Caching. 5. If you want to force the cache refresh from a particular site, select a site or else leave the default set to <Default>. 6. Click OK. 7.9.2.2 Using a command-line interface You can use a combination of the dsquery site and dsget site commands to find if a site has group caching enabled. > dsquery site -name <SiteName> | dsget site -dn -cachegroups -prefGCSite 240 You can use ldifde to enable group caching. Create a file called enable_univ_cache.ldf with the following contents, but change <SiteName> to the name of the site you want to enable, and <ForestRootDN> with the distinguished name of the forest root domain: dn: cn=NTDS Site Settings,cn=<SiteName>,cn=sites,cn=configuration,<ForestRootDN> changetype: modify replace: options options: 32 - Then use the following command to import the change: > ldifde -i -f enable_univ_cache.ldf 7.9.2.3 Using VBScript ' This code enables universal group caching for the specified site. ' SCRIPT CONFIGURATION strSiteName = "<SiteName>" ' e.g. Default-First-Site-Name ' END CONFIGURATION set objRootDSE = GetObject("LDAP://RootDSE") set objSite = GetObject("LDAP://cn=NTDS Site Settings,cn=" & strSiteName & _ ",cn=sites," & objRootDSE.Get("configurationNamingContext") ) objSite.Put "options", 32 objSite.SetInfo WScript.Echo "Successfully enabled universal group caching for " & _ strSiteName 7.9.3 Discussion When a client logs on to a Windows 2000 Active Directory domain controller, the domain controller must contact a global catalog server (if it is not one itself) in order to fully authenticate the client. This is necessary because of universal groups. Universal groups can be created and used anywhere in a forest. Objects located anywhere in a forest can be added as members of a universal group. Since a universal group could be created in a domain other than where the user object resides, it is necessary to store universal group membership in the global catalog. That way, during logon, domain controllers can query a global catalog to determine all universal groups a user is a member of. Microsoft's primary reason for making this a requirement during logon is that a user could be part of a universal group that has been explicitly denied access to certain resources. If universal groups aren't evaluated, a user could gain access to resources that were previously restricted. To remove this limitation in Windows Server 2003 Active Directory, universal group caching was introduced. Universal group caching can be enabled on a per site basis and allows domain controllers to cache universal group information locally, therefore, removing the need to query the global catalog during client logon. . is a new feature of Windows Server 2003 version of ADUC. 1. Open the Active Directory Users and Computers snap-in. 2. If you need to change domains, right-click on Active Directory Users and. caching for " & _ strSiteName 7.9.3 Discussion When a client logs on to a Windows 2000 Active Directory domain controller, the domain controller must contact a global catalog server. Using a graphical user interface 1. Open the Active Directory Users and Computers snap-in. 234 2. If you need to change domains, right-click on Active Directory Users and Computers in the left