New information has been added to this article since publication.
Refer to the Editor's Update below.

Windows PowershellWorking with Active Directory

Don Jones

If you’ve spent any time playing with Windows PowerShell, odds are you’ve tried the Get-WMIObject cmdlet—also known by its convenient alias, gwmi. As I described in last month’s column, this cmdlet highlights what a major improvement Windows PowerShell is over previous scripting languages and command-line

shells (see technetmagazine.com/issues/2007/05/PowerShell). Gwmi enables fast, easy access to Windows® Management Instrumentation (WMI), allowing you to do an incredible number of things without having to write complex scripts.

Get-WMIObject is such a good example of what Windows PowerShell™ can do that whenever I show it to a class, students invariably extrapolate its functionality to other areas of Windows administration. One of the first questions I’m typically asked is along the lines of "Is there a Get-ADSIObject cmdlet?"

ADSI Support in Windows PowerShell

ADSI stands for Active Directory® Services Interface. Despite the name, ADSI isn’t actually specific to Active Directory. A more accurate way to read the name would be "Active (pause) Directory Services Interface," because ADSI can connect to a variety of directory services, including Windows NT®-style directories—not just Windows NT domains, mind you, but also to the local Security Accounts Manager (SAM) on standalone and member computers. With such flexibility, a Get-ADSIObject cmdlet would certainly be tremendously useful. Unfortunately, that’s a cmdlet the Windows PowerShell team wasn’t able to squeeze into its first version.

That’s not to say Windows PowerShell lacks ADSI support, though. In fact, the ADSI support in Windows PowerShell is so good that you might not even care about the lack of a Get-ADSIObject cmdlet!

It all starts with the [ADSI] type adapter. Type adapters need a little explanation. Remember that Windows PowerShell is built on the Microsoft® .NET Framework (2.0 or higher, to be specific). As a result, pretty much everything in Windows PowerShell—from WMI objects to files and folders—is represented as an object from the Framework. However, the Windows PowerShell team wasn’t comfortable just exposing raw Framework objects to Windows administrators. For example, many of the Framework objects work somewhat inconsistently, and consistency was a major design goal for Windows PowerShell. Another reason is that some Framework objects are overkill when it comes to Windows administration, and simplicity was also a major Windows PowerShell design consideration. As a result, type adapters were born.

As the name implies, a type adapter "adapts" a .NET Framework object type into a more consistent, and sometimes simpler, format. For example, suppose you create a new string variable like this:

[string]$var = “Hello”

You’ve used an instance of the System.String type which is handled by the .NET adapter. The [ADSI] type adapter is connected to the .NET Framework DirectoryServices classes, including the DirectoryEntry type.

Retrieving Objects from the Directory

Connecting to directory objects is fairly straightforward. For instance, to connect to a user named Don, in the IT organizational unit (OU) of the Contoso.com domain, you’d use something like this:

$user = [ADSI]”LDAP://cn=Don,ou=IT,dc=Contoso,dc=com”

If you’ve worked with ADSI in VBScript or another language, this should look pretty familiar. It’s a standard Lightweight Directory Access Protocol (LDAP) query string, which is the native means for accessing Active Directory. You don’t need to specify a domain controller (DC) name or any other details; there’s enough information in this Distinguished Name (DN) to locate a DC and retrieve the object (assuming you have permission to do so, of course).

[Editor's Update - 5/31/2007: This sentence originally referred to the Distinguished Name (DN) as a Fully Qualified Domain Name (FQDN), which is not technically accurate. FQDN is a DNS term meant to refer to fully qualified host or domain names (such as server1.contoso.com). Distinguished Name is the proper term for a path shown in RFC 1779 or X.500 format, such as cn=Don,ou=IT,dc=Contoso,dc=com. This update has been made throughout the rest of the article.]

Working with local security accounts uses an almost identical process, except for using the WinNT provider instead of the LDAP provider:

$user = [ADSI]”WinNT://don-pc/don”

Note that both the LDAP and WinNT monikers are case-sensitive. While Windows PowerShell itself is generally case-agnostic, these ADSI resources are not—so be sure to pay attention to how you type them.

Figure 1 shows what happens if I take the resulting $user variable and pipe it to Get-Member: I get a list of the object’s properties. Unfortunately, this is not a complete list of all the user’s properties, and it includes none of the user object’s methods. The underlying System.DirectoryServices.DirectoryEntry Framework class works a bit oddly in this regard, and Windows PowerShell can only show me what the Framework class delivers.

Figure 1 Retrieving a list of an object’s properties

Figure 1 Retrieving a list of an object’s properties (Click the image for a larger view)

Retrieving objects is primarily a matter of knowing how to construct a query string. Using the WinNT provider is quite easy. Simply specify a computer or domain name, a slash, and then an object name. This technique works just fine with Active Directory domains (which are, after all, backward-compatible with Windows NT domains). And, in fact, using the WinNT provider to retrieve objects from Active Directory can sometimes be the easiest route since the WinNT provider gives you a flat view of the domain. That means you don’t need to know what OU an object is located in because the WinNT provider doesn’t see the OUs.

Retrieving LDAP objects requires somewhat more precision. You start with the most-specific information possible, such as an object’s canonical name (cn) or the name of an OU. Then you provide the complete path to the object, beginning with its parent container and working out to the domain itself. For example, the user Don, located in the Sales OU, which is in the West OU of the Contoso.com domain, would look like this:

LDAP://cn=Don,ou=Sales,ou=West,dc=Contoso,dc=com

This format takes a little bit of practice and requires that you know where the object is located. Figure 2 summarizes the various components of a DN.

Figure 2 Distinguished Name components

Component Purpose Example
cn Canonical name of an object. Also used for non-OU containers, such as Users and Computers. cn=Don
ou Organizational Unit. ou=Sales
dc Domain name component. Each component of a DNS domain name gets its own “dc=” component. dc=domain, dc=com

A Little Help from a Friend

Creating and deleting objects typically requires that you first connect to the object’s container, such as an OU, and then ask the container to create or delete the object. I always forget the right syntax, but fortunately the Microsoft Scripting Guys can lend a helping hand in the form of ADSI Scriptomatic.

Though originally created for VBScript, ADSI Scriptomatic is still a useful guide for Windows PowerShell since the actual ADSI syntax is the same. For example, after downloading the tool, I select the option to create a new user. I ignore all the script code that the tool generates until after the "End connect to a container" code, which looks like this:

Set objUser = 
 objContainer.Create(“user”, “cn=” & strName)
objUser.Put “sAMAccountName”, strName
objUser.SetInfo

Translating to Windows PowerShell is easy. First, I connect to the container where I want the user to be created. To stay consistent with the Scriptomatic code, I’ll use the variable $objContainer to represent the OU. I’ll also create a variable, $strName, to hold the new user’s name:

$strName = “Don”
$objContainer = “LDAP//ou=Test,dc=Contoso,dc=com”

Next, I copy over the Scriptomatic code, remove all traces of any VBScript Set statements, and add $ to the variable names:

$objUser = $objContainer.Create(“user”, 
  “cn=” & $strName)
$objUser.Put “sAMAccountName”, $strName
$objUser.SetInfo()

That’s all it takes—a new user has been created in the Test OU of the Contoso .com domain.

Modifying Directory Objects

Once you’ve connected to an object—such as a user or a group—you can modify it. With the WinNT provider, you’re able to directly modify many properties, like those in Figure 1. For example, to display and then change a user’s Description property, you would do this:

Write-Host $user.Description 
$user.Description = “New Description”
$user.SetInfo()

The important thing to remember about this is that property changes are made to your local property cache, which is a temporary storage area that ADSI maintains on your computer. Calling the SetInfo method writes the cache back to the domain or computer that you retrieved the object from, essentially saving your changes back to the directory.

Active Directory objects don’t provide direct properties for most object attributes. Instead, you use the Get and Put methods:

Write-Host $user.Get(“Description”)
$user.Put(“Description”,”New Description”)
$user.SetInfo()

WinNT objects tend to be fairly simplistic, since Windows NT directory services didn’t store much information about objects like users and groups. Active Directory, on the other hand, stores a wealth of information and figuring out the correct attribute names can be difficult. For instance, if you want to change a user’s last name, you have to change the Sn property. Why Sn? It stands for surname.

Fortunately, Microsoft provides a good cross-reference, which is available online at msdn2.microsoft.com/ms677286.aspx. This reference shows the attribute names for each major area of the Active Directory Users & Computers console, so if you know what you want to change in the console, you can figure out the underlying attribute name. Note that a more complete console-to-attribute cross-reference is available in the book ADSI Scripting: TFM by Cade Fassett (SAPIEN Press, 2007).

What You Can’t Do

Unfortunately, ADSI support in Windows PowerShell isn’t as strong as support for other technologies, such as WMI. For example, it’s somewhat difficult to enumerate through the members of a local computer’s groups. A number of difficult situations crop up when dealing with Active Directory as well. My experience has been that basic object retrieval, creation, and deletion works well. Modifying object properties works fairly well, although some properties are inaccessible. I run into the most difficulties working with local objects and groups through the WinNT provider.

That’s not to say things can’t be done at all. Some things are just more difficult or less intuitive. Consider the following example:

$group = [ADSI]”LDAP://cn=Techs,cn=users,
  dc=contoso,dc=com” 
$group.Member.Add(“cn=user1,cn=users,
  dc=contoso , dc=com”) 
$group.psbase.CommitChanges()

This adds the user named User1 to the group Techs. It starts out straightforward enough. It retrieves the group by using the DN. Then, it uses the group’s Member collection—specifically, the collection’s Add method—to add the DN of the desired user. The last step, however, is not very intuitive at all. It calls on the hidden PSBase class’s CommitChanges method to save the changes.

PSBase is a special class that directly represents the base .NET Framework object. In this case, CommitChanges isn’t directly exposed in Windows PowerShell (actually, no methods are exposed by the ADSI adapter), so you have to go to the underlying class. This is not impossible, but it’s certainly not intuitive, either. It also illustrates that directory services capabilities haven’t yet been fully adapted into Windows PowerShell.

There are a number of reasons for this. First, the Windows PowerShell team added the current ADSI support to the shell somewhat late in the development cycle. As Jeffrey Snover, the architect of the shell, often says, "to ship is to choose." The team had a fixed amount of time to develop and ship the shell, and they had to make decisions as to what they had to save for the next version. Otherwise, they could have easily spent years adding features and never actually releasing a supported product.

Another reason, however, is the underlying .NET Framework. Its DirectoryServices class is pretty complex and adapting that complexity into Windows PowerShell will take a lot of time and careful planning. The Framework classes are also, in my opinion, a bit biased toward Active Directory and don’t provide stellar support for the WinNT provider—Windows PowerShell, of course, would inherit any such limitations from the Framework.

I expect Windows PowerShell to eventually have vastly expanded directory services support—support that may not even be through the layer of ADSI, but rather through base improvements in the .NET Framework. In the meantime, the ADSI support available in Windows PowerShell lets you perform the vast majority of administrative tasks, such as creating users, modifying common user properties, and so forth.

Don’t forget that many directory services tasks can be accomplished through WMI. Check out the scripts available at microsoft.com/technet/scriptcenter/scripts for some excellent examples that use WMI to provide more extensive information on a local group than ADSI itself provides.

Don Jones is the co-author of Windows PowerShell: TFM (SAPIEN Press) and frequently blogs about Windows PowerShell at ConcentratedTech.com. You can contact him through his Web site, ConcentratedTech.com.

© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.