Executive Summary:
You can extend PowerShell's capabilities by taking advantage of the Microsoft .NET Framework. If you work with non-Microsoft LDAP directories, one particularly useful .NET tool is the System.DirectoryServices.Protocols (S.DS.P) namespace. Here's an example of how to use the System.DirectoryServices.Protocols namespace in a PowerShell script. The PowerShell script produces a comma-delimited list of groups in a directory container, including all nested group relationships. It works with Active Directory (AD) and non-Microsoft LDAP directories.

At face value, the request was simple. A colleague asked if we could provide him with a tool that would produce a comma-delimited list of groups in a directory container, including all nested group relationships. For example, if GroupA contains GroupB as a member and GroupB contains GroupC as a member, then show the group hierarchy as

GroupA, GroupB, GroupC

Then, for GroupB, the hierarchy would be

GroupB, GroupC

To get a better idea of what he wanted, see Figure 1. To complicate matters just a bit more, he wanted to use the script with Active Directory (AD) and a non-Microsoft LDAP directory.

Figure 1: Nested group output

Because our colleague was interested in only groups (and not user account members) and wanted to include nested group relationships, the tool had to perform both filtering and recursion operations. And because he wanted to use the tool with a non-Microsoft LDAP directory, it had to use an LDAP-compliant directory services namespace.

We decided to use the System.DirectoryServices.Protocols (S.DS.P) namespace, which was introduced in Microsoft .NET Framework 2.0. S.DS.P is one of the coolest namespaces released by the Microsoft Directory Services team. It's faster and significantly more compatible with heterogeneous LDAP directories than other directory services namespaces. Its high performance and compatibility are achieved by removing Active Directory Service Interfaces (ADSI) from the picture. The ADSI layer is a suite of COM interfaces upon which all the other directory services .NET namespaces (System.DirectoryServices, System.DirectoryServices.ActiveDirectory, and System.DirectoryServices.AccountManagement) rely.

Because System.DirectoryServices.Protocols is free of ADSI, its classes make calls directly to the Win32 classes in Wldap32.dll. To see an architectural diagram of this namespace, see MSDN's "Introduction to System.DirectoryServices.Protocols (S.DS.P)" web page.

You might be wondering why anyone would use the other directory services namespaces when S.DS.P is faster and much more compatible with heterogeneous LDAP directories. Although the ADSI layer reduces performance and compatibility, it simplifies directory services code creation from managed code and provides access to COM-based scripting languages such as VBScript.

While it's true that using the classes in S.DS.P is more difficult than using the classes in the other directory services namespaces, you'll notice code patterns that emerge once you're familiar with S.DS.P. Being able to use the code with any LDAP-compliant directory more than offsets the steeper learning curve.

Although you can't use the S.DS.P namespace with VBScript, you can use it with PowerShell. So, we decided to use PowerShell rather than managed code to create our colleague's tool. GetGroupRelationships.ps1 is the result. After we show you how to use this script, we'll tell you how it works.

How to Use the Script
To obtain GetGroupRelationships.ps1, click the Download the Code Here button at the top of the page. Place the script on the machine you intend to run it from. The script accesses a domain controller (DC) in the current domain, so make sure that the machine is joined to a domain. You don't need to modify the script before you use it.

When you run the script, you need to specify two command-line parameters: the Fully Qualified Domain Name (FQDN) of the AD or LDAP server you want to connect to and the distinguished name (DN) of the organizational unit (OU) or container you want to evaluate. For example, the command might look like

GetGroupRelationships ou=Groups,dc=amer,dc=corp,dc=fabrikam,dc=com

After the script completes, you'll receive a list of groups. Figure 1 shows an example of what the output might look.

How the Script Works
The first thing that GetGroupRelationships.ps1 does is load the S.DS.P assembly. This .NET assembly contains all the properties and methods needed to write code for LDAP. PowerShell relies on the .NET System.Reflection.Assembly class to load this and other .NET assemblies.

There are a number of ways to use the Assembly class to load a .NET assembly. The two you'll see most often in PowerShell are using the Assembly class's Load method and using the Assembly class's LoadWithPartialName method.

Even though it works, you should steer clear of the LoadWithPartialName method. We found posts as far back as 2003 (e.g., Suzanne Cook's blog post "Avoid Partial Binds") that suggest you use Load method because the LoadWithPartialName method was deprecated with the release of the .NET Framework 2.0. Using the Load method ensures the correct version of the assembly loads, preventing any backward and forward incompatibilities.

As Listing 1 shows, we follow that advice and use the Load method to load the S.DS.P assembly.

Listing 1: Code That Loads the S.DS.P Assembly

Declaring \[Void\] when the code calls the Load method suppresses the output associated with loading it.

Next, we retrieve the groups from the OU or container specified on the command line using the code in Listing 2.

Listing 2: Code That Retrieves the Groups

Unique to using S.DS.P for LDAP searches is the pattern of connecting to the LDAP directory, creating a search request, and getting a search response.

Connecting to the LDAP directory. S.DS.P's LdapConnection object is used to set up a connection to the directory. Later, that object's SendRequest method is used to transmit the request to a directory server.

Creating a search request. The S.DS.P's SearchRequest object is used to specify where to start the search, the search filter, the search scope, and the attribute to return. In this case, we're starting the search in the OU or container specified on the command line. We want only groups, so we apply the filter (&(objectClass=group)(objectCategory=group)). We set the search scope to OneLevel so that the child objects of the specified OU or container are searched. The attribute we want returned is cn, which contains the common name. (Note that the variables for the search filter and attribute are set earlier in the script so that code doesn't appear in Listing 2.)

We want the groups sorted by their common names, so we use S.DS.P's SortRequestControl object to add a sort request control to the search request. Adding controls to an LDAP search request is a powerful mechanism for creating more advanced LDAP requests.

Getting a search response. The search request is sent to the LDAP server using the LdapConnection object's SendRequest method. In this case, we capture the response in the $searchResponse variable.

After sending a search request to an LDAP server, it's a good idea to check that:

  • The server successfully responded.
  • At least one record was returned.
  • The server responded to any submitted control requests.

So, before we iterate through the results in the $searchResponse variable, we perform these three checks, as callout A in Listing 3 shows.

Listing 3: Code That Checks and Processes the Search Results

First, we first check to see if the LDAP server returned a result code of "success". Then, we check to see if there's at least one record (i.e., an entry) in the search response. Finally, we check to see if the server returned a sort response control. If the server doesn't return a sort response control, it's probably incapable of sorting the results.

After performing these checks, we iterate through the returned entries, as callout B in Listing 3 shows. We take advantage of the last in/first out properties of a stack to maintain the parent/child relationship between the member groups. When a child member group is found, it's added to the stack after its parent. When it's time to render the relationships, the parent/child relationships are already in order and we just have to pop the entries off the stack. After the code adds the child member group to the stack, it calls the GetMembers function for each group.

The GetMembers function sets up a search request similar to the one used in Listing 2. However, the search scope and the control differ. Instead of setting the search scope to OneLevel, it's set to Base so that the member attribute of each AD group object is searched. The function uses an attribute scoped query (ASQ) control instead of the sort request control in order to search the member attribute of each group.

As we did in the first search operation, we then check to see if the LDAP server returned a result code of "success", returned at least one entry in the response, and returned an ASQ control. If the response contains any entries, we recursively call the GetMembers function.

If no entries are in the response, we render the output with the code in Listing 4.

Listing 4: Code That Outputs the Group Hierarchy

To properly render the group hierarchy, we invert the stack, as callout A in Listing 4 shows. Once that's completed, we use a .NET string object to assemble the output string. The entries are concatenated to the $stackOutputter variable as they are popped off the stack, as shown in callout B. This code renders the output to the screen, but you can easily modify it to send the output to a file or pipe the output to some other location.

Take Advantage of the .NET Framework
As GetGroupRelationships.ps1 demonstrates, you can extend PowerShell's capabilities by taking advantage of the .NET Framework. If you work with non-Microsoft LDAP directories, one particularly useful .NET tool is S.DS.P. This namespace frees you from the idiosyncrasies of ADSI and ultimately lets you work with any LDAP directory, not just AD.