This article explains how to use native Active Directory Services Interface (ADSI) components to determine the primary group membership of a user.
How the primary group is stored on a user object in the Active Directory
Every security context object (such as users and security groups) in the Active Directory has a security identifier (SID) attribute associated with it. The SID has several components, as described in the "SID Components" reference included in the "Reference" section of this article. Two of these components on the SID are the domain relative identifier (RID) and the RID specific to the object within the domain.
The primary group of a user is stored (as the group's RID in the user's domain) on the PrimaryGroupID
attribute of a user object. A user's primary group can only be a group that exists in the same domain as the user, and this group has to be a group that the user is a member of. Also, this Group
object in the Active Directory has an attribute called PrimaryGroupToken
, which stores the RID for this group within the domain.
Both Windows NT and Lightweight Directory Access Protocol (LDAP) providers allow programmers to change a user's primary group by setting the PrimaryGroupID
attribute value to the RID of a group that the user is a member of. If the user is not a member of a group, the user's PrimaryGroupID
cannot be set to the RID of the group.
When it uses the Windows NT provider in ADSI, the primary group is included as an entry in the user's IADsUser::Groups
With the LDAP provider, the primary group is not
included as an entry in the IADsUser::Groups
collection, nor is the group's Distinguished Name (DN) part of the MemberOf
attribute of the user object in the directory. The group's RID in the PrimaryGroupID
is the only place in which the primary group for a user is referenced on the LDAP user object.
Neither provider defines a mechanism to determine the name of the primary group for a user directly from their respective user objects. The problem is made even more complex by the fact that each group object provided by each provider supports a different set of attributes, as follows:
- The Windows NT-provided group object does not support the PrimaryGroupToken attribute, nor does the Windows NT provider support any other way to retrieve the group's RID by using native ADSI code.
- The PrimaryGroupToken attribute of an LDAP provider group object is a calculated attribute. This attribute does not exist on the group object in the directory. The attribute is, in fact, created when it is requested by the client with an IADs::GetInfoEx method call. It is not possible to perform LDAP searches based on constructed attributes in the Active Directory. Therefore, you cannot search by using the LDAP provider for a group based on the PrimaryGroupToken attribute that matches the PrimaryGroupID attribute on the user object whose primary group you want to determine. This also rules out a pure ADSI solution that is not dependent on an external COM object to help you determine the primary group.
Ways to determine the primary group of a user
The following are three known methods to determine the name of the primary group of a user:
- Build an SID bind string for the group object in the Active Directory from the Domain RID component of the user's SID and the group's RID stored on the PrimaryGroupID attribute on the user object.
This is the method described in Microsoft KB article Q297951 (included in the "Reference" section of this article). The major problem with this method is that to build the SID bind string, the programmer must rely on the ADsSID object to convert the binary security descriptor of the domain object into its SDDL form. The ADsSID object is hosted by the ADsSecurity.dll file, which must be copied to and registered on the client before the code can be successfully run.
- Use an LDAP query to search for all the groups in a domain and return their PrimaryGroupToken attribute.
This method is a pure LDAP solution. However, the scripted solution is not very efficient because this search returns every group in the domain, even those that the user is not a member of; and because the PrimaryGroupToken attribute is constructed on the client as the record set is traversed, this search is quite slow. This method can be particularly time consuming if there are a large number of groups in the domain. Examples of how to create LDAP dialect ADO queries abound, and therefore, they are not included in this article.
- Take advantage of the features of each provider to build a hybrid solution.
This solution takes advantage of the features of the different providers to determine the primary group of the user. To do this, follow these steps:
The advantages of this method are immediately obvious. All of the objects used are native to ADSI and do not require additional components. Additionally, the enumeration includes only those groups that might be the primary group.
- Bind to the user object with the Windows NT provider.
The Windows NT user object provides a group collection that is guaranteed to contain the primary group of the user. Also, the PrimaryGroupID of the user object is stored away in a temporary location for use later in this algorithm.
- Enumerate the IADsUser::Groups collection.
- Extract the SamAccountName property from the ADsPath for each group in this collection, and then build an LDAP dialect query string to search for all groups with the SamAccountName property listed in this collection, returning their PrimaryGroupToken and DistinguishedName attribute values.
- Run the ADO ADSI search, and then loop through the record set, comparing each group's PrimaryGroupToken with the PrimaryGroupID attribute value cached earlier.
- If you find a match, stop and display the Distinguished Name for this group as the primary group for this user.
- If you do not find a match, continue looping through the search result.
However, note the following disadvantage: When the IADsUser::Groups collection is enumerated, the object returned is an IADs interface to the member in the group. If, for some reason, the user performing the enumeration does not have the necessary permissions to open a particular object, the enumeration stops without indicating an error. The following code illustrates how you can implement the preceding algorithm:
dim oUsr dim oGrp '' ToDo: Change the following variables to specific values for your domain.' DomainName = "myDomain"UserLoginName = "myUserLoginName"'' Bind to the user object with the Windows NT provider.' set oUsr = GetObject("WinNT://" & DomainName & "/" & UserLoginName & ",user") set grp = oUsr.Groups GrpID = oUsr.PrimaryGroupID GrpName = "" '' Building Query Filter for the search for all the groups that the user is a member of.'QueryFilter = "(|"for each Item in Grp NT4Name = replace(Item.ADsPath,"WinNT://","") tempArray = split(nt4Name,"/") NT4Name = tempArray(1) QueryFilter = QueryFilter & "(samAccountName=" & NT4Name & ")"nextQueryFilter = QueryFilter & ")"'' Building LDAP dialect Query String.'QueryString = "<LDAP://" & DomainName & ">;" & QueryFilter & ";PrimaryGroupToken,distinguishedName;subtree"'' Performing Query against the Active Directory for all the groups that ' the user belongs to and retrieving the RID of the group object off' the PrimaryGroupToken attribute on the user.'Dim oConnection, oCommand, oRecordsetSet oConnection = CreateObject("ADODB.Connection")Set oCommand = CreateObject("ADODB.Command")oConnection.Provider = "ADsDSOObject"oConnection.Open "Active Directory Provider"Set oCommand.ActiveConnection = oConnectionoCommand.CommandText = QueryStringoCommand.Properties("Page Size") = 900Set oRecordset = oCommand.Execute'' Looping through all the records in the search result to determine whether' any of these group's PrimaryGroupToken attribute value match the ' PrimaryGroupID attribute value stored on the user object.'While ((NOT oRecordset.EOF) and (Not bGroupFound)) if (GrpID = oRecordset.Fields("PrimaryGroupToken").value) then GrpName = oRecordset.Fields("DistinguishedName").Value bGroupFound = True End If oRecordset.moveNextWendSet oRecordset = NothingSet oCommand = NothingSet oConnection = Nothing'' Displaying Results of the search.' if( bGroupFound ) then WScript.Echo "Primary Group for " & oUsr.AdsPath WScript.Echo "Is: " & GrpName else WScript.Echo "Primary Group Not Found" end if