One nice benefit of Active Directory (AD) over Windows NT is that AD supports complex group nesting. Nested groups give you flexibility in designing your group structure and applying ACLs to resources. This feature becomes available as soon as you raise an AD domain to the Windows Server 2003 functional level or the Windows 2000 native domain mode.
Nested groups are powerful, but they also add complexity. Because some group members might be groups themselves, you often can't simply look at the members of a group and determine which users will be affected by granting the group access to a resource. The Microsoft Management Console (MMC) Active Directory Users and Computers snap-in doesn't provide much help because it displays only the users that are direct members of a group. You have to double-click each member group to view its membership individually. But a simple script can take the legwork out of drilling down through nested-group membership.
To illustrate group nesting, consider a group structure that mimics the hierarchy of a university's Computer Science department. Each class in the department is a group consisting of the students who have signed up for the class. Together, all the departmental class groups make up the Computer Science department group. Finally, the Computer Science department group is a member of the Engineering school group. This nested group structure provides the flexibility to grant access to resources to as broad an audience as everyone in the Engineering school or to as limited an audience as only the students in a particular Computer Science class.
Group objects in AD have two attributes that pertain to group membership. One is the member attribute, which stores references to objects that are direct members of the group. The other is the memberOf attribute, which stores references to other groups to which the group belongs.
These two attributes are linked in AD. You don't modify the memberOf attribute to change group membership; you can modify only a group's member attribute. AD automatically computes the memberOf attribute for a group (or other type of object, such as a user) from all the groups of which the object is a direct member. The memberOf attribute lets you identify the groups that an object is a member of without having to search all groups. For example, if the student1 user ID is a member of the Engineering group, student1's memberOf attribute contains a reference (specifically the distinguished name—DN) to the Engineering group. The Engineering group's member attribute in turn has a reference (again a DN) that points to student1.
Using the Script
Now that you have a handle on group attributes, consider a script that enumerates nested-group membership. Listing 1 shows the code for enum_groups.vbs, which displays nested-group membership in a hierarchically indented list. To benefit from the output's nested display of the results, you need to use Windows Script Host's (WSH's) CScript engine to run enum_groups.vbs.
To use the script, copy it to a local directory on your computer and run the following command:
C:\Scripts> cscript enum_groups.vbs
This command causes the script to print the group membership of the Domain Admins group of the domain that authenticated you. Another way to run the script is to pass the DN of the group whose membership you want to enumerate. For example,
C:\Scripts> cscript enum_groups.vbs cn=engineering,cn=users,dc=foo,dc=edu
enumerates the members of the Engineering school group and produces the sample output that Figure 1 shows.
Retrieving the Group from AD
Within enum_groups.vbs, the code that callout A in Listing 1 shows determines which group to enumerate. To find out whether the user passed an argument to the script, the script creates a WScript.Arguments object, which provides access to all command-line arguments. If the number of arguments passed to the script doesn't equal 1, the script assumes that the user either didn't specify any arguments or specified too many.
In either case, the script makes a serverless bind to the RootDSE object. The DC Locator process automatically binds to a domain controller (DC) that's in the same domain as the user who's running the script. The script then constructs the DN of the Domain Admins group, so if someone runs the script without specifying a group to enumerate, the script will at least do something. When the user passes one parameter, the script puts that parameter in the strGroupDN variable.
The code at callout B starts by creating a VBScript dictionary object called dicSeenGroupMember. The script uses the dictionary object to keep track of groups it's already seen so that circular group nesting doesn't result in an infinite loop. Circular group nesting occurs when you have a loop in the group membership chain. For example, if groupA is a member of groupB, groupB is a member of groupC, and groupC is a member of groupA, the membership chain contains a loop. Circular group nesting isn't necessarily bad as long as you're aware of it when you're dealing with nested groups.
Finally, the code at callout B calls the DisplayMembers function. DisplayMembers uses three parameters: the ADsPath of the group to enumerate, the number of spaces to indent when printing members, and a reference to the dictionary object. I explain each of these parameters in more detail later.
The DisplayMembers Function
Callout C shows the script's workhorse code—the code for the DisplayMembers function. DisplayMembers is a recursive function that enumerates the members of a group, then calls itself to enumerate all those members that are groups. Then, DisplayMembers begins a For Each...Next loop. The loop iterates over all members of the group, calling the Members method for each member. Only IADsGroup objects have the Members method; if the ADsPath doesn't belong to a group object, the script will die rather ungracefully at the first line of the loop. When the ADsPath belongs to a group object, the script returns an IADsMember object for each member of the group.
Within the loop, enum_groups.vbs first uses the Get method to retrieve the member's DN, then prints it. At this point in the script, DisplayMembers' second parameter—the number of spaces to indent—comes into play. Because I want the script to show the hierarchy of group nesting, the script prints spaces before it prints each member. Later, when DisplayMembers calls itself, it increases the number of spaces to indent for each level in the members hierarchy, as Figure 1 shows.
Next, enum_groups.vbs uses the Class method to check the member object's class type. For groups, the Class method returns group, which indicates that group nesting occurs and that the script needs to enumerate the members of the nested group as well. However, before enum_groups.vbs enumerates the nested group's membership, it needs to determine whether it's already enumerated that group. I don't want the script to enumerate a group more than once because doing so could cause an infinite loop, as I explained earlier. To determine whether it's already enumerated the group, enum_groups.vbs checks whether the group has been added to the dictionary object. If it hasn't, the script hasn't enumerated it.
Whenever enum_groups.vbs enumerates a group, the script first adds the group to the dictionary object so that the script won't enumerate the group again. Then, the script calls DisplayMembers again, and recursion comes into play. Remember that DisplayMembers' first parameter is the ADsPath of the group to enumerate, which the script obtains by using the ADsPath method. The second DisplayMembers parameter is the number of spaces to indent, and the script adds two spaces to the indentation for the nested group. The final parameter is a copy of the dictionary object.
That's all there is to enum_groups.vbs. Recursion works in this script because the script keeps track of groups that have been enumerated and calls DisplayMembers only when a group hasn't been enumerated. With recursive functions, you always need exit criteria. In enum_groups.vbs, the dictionary object serves that purpose.
Stay Turned for More Group Fun
Another task that's helpful when you use group nesting is viewing a user's complete group membership, including the user's membership in nested groups. You can accomplish this task by using the user's memberOf attribute to view all groups to which the user belongs and traversing the membership hierarchy of each of those groups. In an upcoming article, I'll describe how you can use a short script to accomplish this task.