Hey, Scripting Guy! Determining a User’s Group Memberships
The Microsoft Scripting Guys
Download the code for this article: ScriptingGuy03.exe (102KB)
Groucho Marx once said, "I don’t want to belong to any club that will accept me as a member." (The Scripting Guys hold a similar philosophy...or we would if we could ever find a group that would accept us as members.)
If you’ve ever run a script to try and determine the Active Directory® groups a user belongs to, then you know exactly what Groucho was talking about. After all, it’s entirely possible to run a script that retrieves the group memberships for a user, yet doesn’t retrieve all the groups that the user belongs to. In other words, as far as the script is concerned, the user is apparently not a member of a group that was willing to accept them as a member.
Note: our favorite Groucho remark is still, "This morning I shot an elephant in my pajamas. How he got in my pajamas I’ll never know." But try working that into a column on system administration scripting!
So what are we talking about here? (Boy, if we had a dollar for every time we’ve been asked that question.) Let’s put it this way. Suppose Ken Myer is a member of Active Directory Groups A and B. And suppose we run this script that returns the names (technically the common names or CNs) of all the groups that Ken belongs to:
Set objUser = GetObject("LDAP://CN=Ken Myer," & _ "OU=Finance,DC=fabrikam,DC=com") Set colGroups = objUser.Groups For Each objGroup in colGroups Wscript.Echo objGroup.CN Next
The following shows what we get back when we run the script:
OK, true—that’s exactly what we should get back. Ah, but try this: make Group A a member of Group C. As you know, this is referred to as nesting groups, the process of making one group a member of another group. When you do this, all members of nested Group A automatically become members of Group C. In other words, Ken is now a member of three Active Directory Groups: A, B, and C.
So what happens after we make Group A a member of Group C? Well, here’s what we get back when we run our script:
Uh-oh, what happened to Group C? As it turns out, the Groups method (which we used in our script) returns only the list of groups in which the user is mentioned by name; in this case that’s Groups A and B. Ken is a member of Group C, but his name doesn’t show up in the Group C membership list because he’s a member only by virtue of his membership in Group A. As a result, the script doesn’t pick up his membership in Group C.
Due to the way the Groups method and Active Directory work, our script can’t return all of Ken Myer’s group memberships unless it checks to see whether Groups A and B belong to any other groups. Our script doesn’t do that, so we have no knowledge of nested groups and we end up with an incomplete picture of Ken’s group membership. And that could be a problem, especially if we’re checking group membership in order to allow or deny access to resources. To paraphrase Groucho, "Either this script is dead or my watch has stopped." Regardless, we are not getting back a list of all the groups Ken Myer actually belongs to.
Figure 1Getting Group Members
On Error Resume Next SetobjUser=GetObject("LDAP://CN=Ken Myer," & _ "OU=Finance,DC=fabrikam,DC=com") Set colGroups = objUser.Groups For Each objGroup in colGroups Wscript.Echo objGroup.CN GetNested(objGroup) Next Function GetNested(objGroup) On Error Resume Next colMembers = objGroup.GetEx("memberOf") For Each strMember in colMembers strPath = "LDAP://" & strMember Set objNestedGroup = _ GetObject(strPath) WScript.Echo objNestedGroup.CN GetNested(objNestedGroup) Next End Function
The moral of the story? If you use nested groups within your organization, then the script we showed you at the beginning of this column is of limited use. (If you don’t use nested groups then this particular script works just fine.) A much better, albeit slightly more complicated, script is one that looks like Figure 1.
A few caveats here. First, you won’t be able to get at universal group memberships unless you have a global catalog server up and running. But you already knew that. Second, this script returns all the groups the user belongs to except for the user’s primary group. In most organizations that’s not a big deal, but if you’d like to know the primary group you can use the following script from the TechNet Script Center: List the Primary Group for a User Account. Finally, we wanted to keep the code as simple as possible; therefore, we don’t deal with so-called "circular group nesting." That’s a case where Group A is a member of Group B which is a member of Group C which is a member of Group A. To deal with that you could simply include code that keeps track of each group as its membership is retrieved: if you’ve already retrieved information from Group A, then you don’t try to retrieve that same information a second time.
So what’s different about this second script? Well, as you can see, it starts out pretty much the same as our first script: it binds to the Ken Myer user account in Active Directory and then uses this line of code to return a list of all the groups in which Ken Myer is listed, by name, as a member:
Set colGroups = objUser.Groups
Following that, the script uses a For Each loop to walk through the collection of groups. For each group in the collection the script echoes back the group CN:
It’s at this point that the two scripts diverge. Our first script did nothing more than echo back the group names. Our second script not only echoes back the group names but also does something else—it uses this line of code to call a recursive function named GetNested:
A recursive function? You’re right, that is pretty fancy, at least for a Hey, Scripting Guy! column.
As it turns out, a recursive function is a function that can call itself. (Think of those Russian nesting dolls, in which there’s a doll inside a doll inside a doll inside...well, you get the idea.) We won’t be able to discuss recursion in much detail today; for that you might want to take a look at the Recursion section of the Microsoft Windows 2000 Scripting Guide, or watch the classic Scripting Guys webcast "Inactive" Directory? Not When You Use Scripts to Help Manage AD.
For now, all we really care about is what the recursive function does. Well, it determines whether the groups that Ken Myer belongs to happen to be members of other groups. You might have noticed that, when we call the function, we pass along the distinguished name of the group that we just echoed to the screen (using the variable objGroup). The first thing we do inside our function is use that object reference variable and the GetEx method to retrieve a list of all the groups that this group belongs to:
colMembers = objGroup.GetEx("memberOf")
Yes, very observant of you; in this case we’re retrieving the value of the memberOf attribute rather than using the Groups method. Why? Because we’re now working with the Group object, and the Group object doesn’t support the Groups method. (Sounds weird, but it’s true.) Therefore, we use the memberOf attribute, which returns the groups that the entity belongs to. But memberOf, like Groups, retrieves only the group memberships that list our entity by name. If Group C is a member of Group D, we’ll need to call our recursive function to see if Group C belongs to any other groups.
Next we set up a For Each loop to walk through the collection of groups, if any, that our group is a member of. Here’s what the code inside that For Each loop looks like:
strPath = "LDAP://" & strMember Set objNestedGroup = GetObject(strPath) WScript.Echo objNestedGroup.CN GetNested(objNestedGroup)
Complicated? Not really. The memberOf attribute returns the distinguished name of each group that the group is a member of. Therefore, we can use this line of code to bind, in turn, to each of those groups:
Set objNestedGroup = GetObject(strPath)
After binding to a group, we echo back the group CN, then run this line of code:
This is where things get kind of Matrix-like. Even though we’re already in the GetNested function, we’re calling the GetNested function. This is hard to visualize, so let’s try talking our way through it. At the start of the script we call the Groups method. This returns a collection consisting of the two groups that Ken Myer belongs to, Groups A and B. The script then sets up a For Each loop to walk through that collection. For the first group in the collection (Group A), the script echoes back the group name. It then calls the function GetNested, passing the variable objGroup (which holds the distinguished name of Group A) as a parameter.
Yes, we’re taking a little side trip here. But don’t worry. VBScript essentially marks its place, and knows that we haven’t looked at Group B yet. After we finish with our side trip, VBScript will automatically return and start the process over with Group B.
Inside the GetNested function we bind to the Group A account in Active Directory. We then use the GetEx method and the memberOf attribute to return a collection of all the groups that Group A belongs to. What if Group A doesn’t belong to any other groups? No problem. In that case the script exits the function and returns back to the original For Each loop, the loop we’re using to walk through the collection of groups returned by the Groups method. The script then repeats the process for Group B, the next group that Ken Myer belongs to.
Got that? Good, you’re halfway there.
Now, what if Group A is a member of another group? In that case the script will bind to the first group that Group A belongs to. The script echoes back the CN of that group (in our example, C), then calls the function GetNested. GetNested binds to Group C and gets a list of all the groups that C belongs to. If Group C is not a member of any other groups, then the script simply goes back to where it came from, walking through the collection of groups that Group A belongs to. If C does belong to a group (say, Group D), then the function GetNested is called again, and it binds to Group D and checks its group membership. Ay carumba!
As Groucho would say, "A child of five would understand this. Send someone to fetch a child of five." Of course, things like this are easy for a child of five to understand. After all, five-year-olds believe in magic. But recursive functions seem magical only because VBScript is doing the hard work for us. For example, imagine doing this all by hand. You’d probably start by writing down a list of all the groups that Ken Myer belongs to. Then you’d check each of those groups to see if they were members of other groups. If they were, then you’d have to write down those groups and check to see if they belonged to other groups. And so on.
To write code to keep track of where you were and what you were doing would be a nightmare. But you don’t have to write that code. Instead, you can simply call a recursive function and let VBScript take care of all that. This is one of those times when it’s best just to have faith that VBScript knows what it’s doing. And this is also one of those times when that faith will be rewarded.
If you study the script and try to figure out how recursion works, you’ll find that this is an incredibly useful technique, one that can be applied to all sorts of situations (getting a list of the files stored in a folder and its subfolders, getting a list of all the values stored in a registry key and its subkeys, and so on). If that’s more than you want to deal with, it’s OK. Just use the code as-is and you’ll get back a list of all the groups that the user belongs to. Yes, we know: "I’ve had a perfectly wonderful evening. But this wasn’t it." Unlike Groucho, at least you got a useful script out of the deal.
About the Microsoft TechNet Script Center
As you read through the Hey, Scripting Guy! column, do you find yourself asking questions such as: What is the Script Center? Who are the Scripting Guys (and why "Guys" rather than "People")? Why is my dog barking at the wall?
Sorry, that last question was ours, and we know there’s no logical answer to it. But there are logical answers to the rest, and I’m sure you can’t wait to hear them, so here goes.
What Is the Script Center?
We’ll set aside "what" for a moment and first start with "where." You’ll find the Script Center here. We know that because it’s our default home page. We strongly urge (encourage, recommend, beg) you to make it yours, too. OK, at least add it to your Favorites and remember to come back every day for new content.
So what is the Script Center? It is the place to go if you’re a system administrator and you want to make your job easier. The purpose of the Script Center is to teach system administrators how to write scripts that automate tasks within their Microsoft Windows environments. You’ll find hundreds of articles, dozens of on-demand webcasts, and many other resources that introduce administrators to scripting and help them become increasingly skilled at automating tasks.
In addition, the Script Center contains a Script Repository. The Script Repository contains thousands of ready-made scripts that you can either use as-is or customize for your own personal solutions.
Who Are the Scripting Guys?
The Scripting Guys are the people who provide everything on the Script Center. Why "Guys?" Well, the Scripting Guys started with a few guys. Now they’re a few guys and a woman, but, well, the name just stuck. (She insists it’s not so bad.)
Back to School with the Scripting Guys
Are you new to scripting? If so, you won’t want to miss going back to school with the Scripting Guys. Lectures, supplemental materials, and even homework. (OK, maybe we shouldn’t have mentioned the homework.) Sign up now for the upcoming TechNet Webcast Series, presented by the Scripting Guys. This is a series of weekly webcasts designed to teach you the basics of scripting.
The Scripting Educational Series begins in early April, 2006. Go to the Scripting Educational Series for details.
The Microsoft Scripting Guys spend most of their time at the beach...oh, um, no, we mean: The Scripting Guys are hard-working members of the Microsoft Windows Server User Assistance team. To find out more, go to: microsoft.com/technet/community/columns/scripts/sgwho.mspx.
© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.