Searching Active Directory with Windows PowerShell

Searching Active Directory with Windows PowerShell

At heart, Active Directory is nothing more than a database (a Jet database, to be exact). Big deal, you say? Well, as a matter of fact, it is a big deal: the fact that Active Directory is a database means that you can use scripts to search Active Directory. Need a list of all your user accounts? Write an Active Directory search script. Need a list of all your computer accounts? Write an Active Directory search script. Need a list of all your color printers or all your contacts from the Fabrikam Corporation? Write an Active Directory– well, you know how it goes by now.

Of course, it’s one thing to suggest that someone write an Active Directory search script; it’s quite another thing to actually sit down and write that Active Directory search script. That’s not because these scripts are hard to write; it’s because it’s very difficult to find documentation and examples that show you how to write Active Directory search scripts using Windows PowerShell.

Well, check that: it used to be very difficult to find documentation and examples that show you how to write Active Directory search scripts using Windows PowerShell.

The purpose of this article is straightforward: combined with 100+ sample scripts recently added to the Script Center Script Repository, this article provides an introduction to the fine art of writing Active Directory search scripts using Windows PowerShell. Does this article contain everything you’ll ever need to know about writing Active Directory search scripts? Probably not. But it does include enough information to help you get started.

Note. So where can you find more information on writing Active Directory search scripts? Good question. The Scripting Guys did a webcast on searching Active Directory a few years back, and they also put together a two-part Tales From the Script series entitled Dude, Where’s My Printer? Both the webcast and the columns use VBScript in their examples (after all, there was no such thing as PowerShell back in those days), but you still might find some of the “generic” information about Activity Directory (What’s a scope? What’s an attribute? What’s a page size?) to be useful.

By the way, these days all the excitement in the PowerShell world revolves around PowerShell 2.0 and the November 2007 Community Technology Preview release. Because of that, we thought it was important to stress that the ability to writes scripts that search Active Directory does not require PowerShell 2.0. All the sample code you’ll see today works equally well on both versions of PowerShell. If you’ve got either version of Windows PowerShell (1.0 or 2.0) installed then you’re ready to write Active Directory search scripts.

Writing Active Directory Search Scripts

OK, now that you’re ready, how do you write Active Directory search scripts? To tell you the truth, there are probably several different ways you could go about this task. But here’s how the Scripting Guys write Active Directory search scripts:

$strFilter = "(&(objectCategory=User)(Department=Finance))"

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "Subtree"

$colProplist = "name"
foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}

$colResults = $objSearcher.FindAll()

foreach ($objResult in $colResults)
    {$objItem = $objResult.Properties; $objItem.name}

So how does this script work? Don’t worry; we’re going to go over this script inch-by-inch and line-by-line … except for the first line; it will be a few minutes before we get to that (but we will get to that, promise). Instead, let’s start with line 2:

$objDomain = New-Object System.DirectoryServices.DirectoryEntry

Technically, what we’re doing here is creating an instance of the System.DirectoryServices.DirectoryEntry class; this class represents an object in Active Directory. In more practical terms, what we’re doing is identifying the Active Directory location where we want the search to begin. As you might have noticed, however, we didn’t specify an Active Directory location; there’s not an ADsPath to be found here. But that’s OK; if we create a DirectoryEntry object without any additional parameters we’ll automatically be bound to the root of the current domain. You say you don’t want your search to start in the domain root? That’s fine; in that case, go ahead and include the ADsPath of the desired start location when creating the object. For example, this line of code binds us to the Finance OU rather than the domain root:

$objOU = New-Object System.DirectoryServices.DirectoryEntry("LDAP://OU=Finance,dc=fabrikam,dc=com")

In turn, that means our search will start in the Finance OU rather than the domain root. (A good thing to know if you want to search for objects in just the Finance OU and its child OUs.)

After we have a DirectoryEntry object (and a starting location for our search) we use this line of code to create an instance of the System.DirectoryServices.DirectorySearcher class:

$objSearcher = New-Object System.DirectoryServices.DirectorySearcher

As you probably guessed, this is the object that actually performs an Active Directory search. What if you didn’t guess that? Well, don’t worry about it; now you know.

Before we can begin using our DirectorySearcher object we need to assign values to several different properties of this object:

$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "Subtree"

The SearchRoot tells the DirectorySearcher where to begin its search. As you might recall, back in line 2 we connected to the domain root, something that occurred when we created a DirectoryEntry object named $objDomain. Thanks to that, we can simply assign the value of $objDomain to the SearchRoot property. Oh, and before you ask, no, we can’t do something along the lines of this:

$objSearcher.SearchRoot = "LDAP://dc=fabrikam, dc=com"

Why not? Because the SearchRoot property will only accept an instance of the DirectoryEntry class; it won’t accept a string value.

Which, needless to say, seems like good enough reason to assign a DirectoryEntry object to SearchRoot.

Next we assign the value 1000 to the PageSize property. By default, an Active Directory search returns only 1000 items; if your domain includes 1001 items that last item will not be returned. The way to get around that issue is to assign a value to the PageSize property. When you do that, your search script will return (in this case) the first 1,000 items, pause for a split second, then return the next 1,000. This process will continue until all the items meeting the search criteria have been returned.

After taking care of the PageSize we next assign a value to the Filter property. We actually defined our filter in line 1, but told you we’d discuss this line later. And now we’re going to tell you that again: we’ll discuss this line later. For now, we’ll simply note that the Filter property is the spot where we define our search criteria; that’s where we tell the script exactly what to search for. Although it might not look like it, the following filter retrieves a collection of all the users in the Finance department:

$strFilter = "(&(objectCategory=User)(Department=Finance))"

But, again, we’ll talk about this later.

That’s a good question: Why didn’t we use a SQL query along the lines of this:

"SELECT Name FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user' " & _
    "AND Department='Finance'"

Well, we have a pretty good reason for that: the Filter property won’t accept a SQL query. Instead, we have to assign this property an LDAP search property. But that’s something that – um, that’s right, that’s something we’ll talk about later.

Finally, we assign the string Subtree to the SearchScope property. Subtree is the default value for search scripts, which means we didn’t actually have to set the SearchScope to Subtree. Instead, we did this just so we’d have an excuse to discuss the search scope.

OK, so then what is the search scope? Well, if the search root determines the location where a search will begin, the search scope determines how much of that location will be searched. As it turns out, there are three different search scopes you can use when searching Active Directory:

Scope Type

Description

Base

Limits the search to the base object. The result contains a maximum of one object. 

OneLevel

Searches the immediate child objects of the base object, excluding the base object. 

Subtree

Searches the whole subtree, including the base object and all its child objects. If the scope of a directory search is not specified, a Subtree type of search is performed.

What does all that mean? Well, suppose we have an Active Directory which has a domain root, a few OUs, and then, under one of those OUs, a couple of child OUs. In other words, suppose we have an Active Directory that looks like this:

Let’s further suppose that we define our search root as the domain root and set the SearchScope to Base. What will we end up searching? As shown below, we’ll search only the domain root; we won’t even look at the OUs and child OUs:

OK, what about a OneLevel search targeted at the domain root? In that case we won’t search the root at all, nor will we search the child OUs. Instead, we’ll search only the immediate child objects of the target object. You know, like this:

That leaves us with the Subtree search, which searches an object and all of its child objects (and their child objects, and their child-child objects, and ….). Want to search an entire domain? Then start the search in the domain root, and set the SearchScope to Subtree:

By the way, setting the SearchScope to Subtree is half the equation for searching only an OU and its child OUs; the other half is to create a DirectoryEntry object that binds you to that particular OU. (Remember when we did that with the Finance OU?) Do both of those things and you’ll be able to search just an OU and its child OUs:

Or, set the SearchScope to Base and search just the OU itself, sidestepping any child OUs.

But enough about that; let’s get back to the script, and to this little block of code:

$colProplist = "name"
foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)}

These two lines are where we define the properties we want returned when we conduct our search. In the first line we create an array named $colProplist, an array that contains each attribute we want the search to return. In this simple example we’re interested in only one attribute (Name); hence we assign only a single value to $colProplist. What if we wanted to retrieve more than one attribute value? Then we’d assign more than one attribute to $colProplist:

$colProplist = "name", "jobTitle", "telephoneNumber"

This, by the way, is standard Windows PowerShell syntax for creating an array of string values: we simply assign each value to the array, enclosing individual values in double quote marks and separating each value by a comma. In other words, if you’re a PowerShell user (and we assume you are), this is something you’ve probably done a million times by now.

In the second line, we set up a foreach loop to loop through each of the attributes in $colProplist. For each of these attributes we call the Add method to add the attribute to the DirectorySearcher’s PropertiesToLoad property. The attributes assigned to PropertiesToLoad are the only attributes that will be returned by our search. Suppose we assign name, jobTitle, and telephoneNumber to the PropertiesToLoad property. If we then try to echo back the value of the homeDirectory attribute that will trigger an error; that’s because the homeDirectory attribute won’t be included in the collection of information returned by the script.

Note. There is one exception: the ADsPath attribute is always returned by a script, regardless of whether it was added to PropertiesToLoad or not. However, that’s the only exception to this rule.

We should probably point out that you don’t have to assign anything to the PropertiesToLoad property; you could simply leave these two lines of code out and you could still conduct a successful search of Active Directory. So why didn’t we just do that and be done with it? Well, if you don’t assign anything to PropertiesToLoad your search script will return all the attribute values for whatever it is you’re searching for. Suppose you have a search script that returns a collection of all the users in your domain. Each Active Directory user account has more than 200 attributes associated with it; if you have several thousand users in your domain that’s going to be a ton of data streaming across your network. In turn, that will: 1) clog up the network; 2) bog down the domain controller performing the search; and 3) slow your script down considerably. If you don’t need all the attribute values then there’s no reason to retrieve all the attribute values. Instead, assign only the attributes you do need to the PropertiesToLoad property.

Note. What’s that? You say you really, truly do need to return all the attribute values for all these users? That’s fine; in that case, you should search for just the ADsPath attribute, then use that value to individually bind to each user account and then retrieve the remaining attribute values one-by-one. Believe it or not, that will be faster than returning all these values in a search. Plus, it will save wear and tear on your network, your domain controller, and your sanity. We don’t have time to show you how to do that today, but we’ll cover that in a future article.

Promise.

Searching Active Directory

Yes, it did take awhile to explain all the ins and outs of setting up an Active Directory search*,* didn’t it*?* But that’s all behind us now; at long last, we’re ready to conduct a search:

$colResults = $objSearcher.FindAll()

As you can see, once you’ve configured the DirectorySearcher object actually carrying out a search is as easy as calling the FindAll method. Call that one method in that one line of code and your script will go out, search the requested portion of Active Directory, retrieve the requested attributes for the requested objects, and then store that information in a variable (in this case, a variable named $colResults).

Whew!

But wait, don’t relax just yet: we still need to display our results to the screen. Admittedly, we could simply echo back the value of $colResults, like this:

$colResults

So why don’t we do that? Well, as you can see, the resulting output can be a little difficult to deal with:

LDAP://CN=Ken Myer,OU=North American Headquarters,DC=... {name, adspath}
LDAP://CN=Pilar Ackerman,OU=UserAccounts,DC=fabrikam,... {name, adspath}
LDAP://CN=Jonathan Haas,OU=Finance,DC=fabrikam,DC=com... {name, adspath}

That’s why we decided to use this block of code to echo back our search results:

foreach ($objResult in $colResults)
    {$objItem = $objResult.Properties; $objItem.name}

All we’re doing is setting up a foreach loop to loop through each record in our recordset. For each of these records we use this command to grab the returned attribute values and assign them to a variable named $objItem:

$objItem = $objResult.Properties

And then we simply echo back the value of $objItem’s name attribute:

Ken Myer
Pilar Ackerman
Jonathan Haas

What if our script returned more than one attribute value? No problem; in that case we just echo back each of those values, like so:

foreach ($objResult in $colResults)
    {$objItem = $objResult.Properties
         $objItem.name
         $objItem.jobTitle
         $objItem.telephoneNumber
         $Write-Host
    }

Or, to make it easier to identify which value is which:

foreach ($objResult in $colResults)
    {$objItem = $objResult.Properties
         "Name: " + $objItem.name
         "Title: " + $objItem.jobTitle
         "Phone number: " + $objItem.telephoneNumber
         $Write-Host
    }

That gives us output similar to this:

Ken Myer
Manager
555-1123

Pilar Ackerman
Administrative Assistant
555-1341

Jonathan Haas
Vice-President
555-9381

And that’s all you need to know in order to use Windows PowerShell to search Active Directory!

Writing LDAP Filters

Oh, that’s right; we almost forgot. Turns out you still need to know one more thing before you can start writing Active Directory search scripts in Windows PowerShell: you also need to know how to write LDAP search filters. Let’s see if we can figure that out, too.

First, however, let’s refresh our memory by taking another look at the search filter we defined in our very first line of code:

$strFilter = "(&(objectCategory=User)(Department=Finance))"

This particular search filter combines two criteria: it searches for everything that has an objectCategory equal to User and a Department equal to Finance. We’ll explain how to combine criteria in a moment. Before we do that, however, let’s examine a simpler filter, one that searches for all user accounts regardless of the Department that user belongs to:

$strFilter = "(objectCategory=User)"

This example (as well as the picture below) illustrates the three parts of an LDAP search filter:

To begin with, note that the entire search filter must be enclosed within parentheses; that’s important. Therefore, when you sit down to write an LDAP search filter you might as well start with a set of parentheses:

()

Inside those parentheses we then specify the attribute we want to filter on. In our simple filter example we’re filtering on the objectCategory attribute; thus we put the name of that attribute in our filter:

(objectCategory)

Next we put in the operator. The DirectorySearcher Filter property allows us to use any of the following operators:

Operator

Description

<

Less than

<=

Less than or equal to

=

Equal to

>

Greater than

>=

Greater than or equal to

Notice there is no “not equal to” operator (e.g., <>). But don’t worry; before we go we’ll explain how to write a “not equal to” filter.

Hey, we’d never forgive ourselves if we didn’t.

As we noted, we’re looking for all objects where the objectCategory is equal to User; that means our filter now looks like this:

(objectCategory=)

Yes, it does look a little crowded in there, doesn’t it? But, whatever you do, resist the temptation to make the filter look “prettier” by inserting a blank space between the attribute and the operator. Why? Because a “pretty” filter like this one will fail:

(objectCategory = User)

Why does it fail? You got it: because we used blank spaces to separate the attribute, operator, and value. Don’t be like the Scripting Guys: make sure that you don’t put blank spaces between the attribute, operator, and value.

Speaking of value, that’s what comes next:

(objectCategory=User)

As you can see, there’s no need to put double quotes around the value. That’s true even if your value includes blank spaces (and, yes, blank spaces are allowed in the value). Need to search for a user with a Name equal to Ken Myer? No problem:

(Name=Ken Myer)

By the way, you can also use the asterisk as a wildcard character when specifying the value. Want to search for all the users who name starts with Ken? This filter should do the trick:

(Name=Ken*)

Meanwhile, this filter searches for all the users who have a Name value of some kind:

(Name=*)

See? These LDAP filters look weird. And to be honest, they are weird. But at least they’re easy enough to write.

"Special" Filters

Before we call it a day let’s take a quick look at three special types of filters:

  • AND filters

  • OR filters

  • NOT filter

You might not have realized it at the time, but the very first line of code we showed you in this article was an example of one of these special filters (an AND filter). Remember this line:

$strFilter = "(&(objectCategory=User)(Department=Finance))"

What we’re doing here is creating a filter that returns object that meet two criteria: the objectCategory must be equal to User and the Department must be equal to Finance. Note the syntax used to create this filter. As usual, the entire filter is enclosed in parentheses. We then have an ampersand (&); in the exciting world of LDAP filters, this symbol indicates that we want to create an AND filter. And then we simply have the two criteria, with each item enclosed in a set of parentheses. To make this a little easier to visualize, picture the query as being written like this:

(&
    (objectCategory=User)
    (Department=Finance)
)

See how that works? What if want to return objects that meet three criteria? That’s no problem; we just need to add the third item to the query:

(&
    (objectCategory=User)
    (Department=Finance)
    (jobTitle=Accountant)
)

Or, the way we’d type it in a script:

(&(objectCategory=User)(Department=Finance)(jobTitle=Accountant))

The OR filter is similar; we simply substitute the pipe separator (|) for the ampersand:

$strFilter = "(|(Department=Finance)(Department=Research))"

In this case, we’ll get back all objects where the Department is equal to Finance or where the Department is equal to Research. See the difference? In an AND filter we need to meet all the criteria; in an OR filter we simply need to meet any one of the specified criteria.

We can even combine these filter types to create a very finely-targeted filter. For example, this filter returns all the user accounts (objectCategory=User), provided that the user is a member of either the Finance or the Research department:

(&(objectCategory=User)(|(department=Finance)(department=Research)))

Or, to again put it a little more visually:

(&
    (objectCategory=User)
    (|
        (Department=Finance)
        (Department=Research)
    )
)

As you can see, with this filter we have both an AND filter and an OR filter. The AND filter looks like this, and specifies that we must have a user who comes from a specific department:

&(objectCategory=User)(|(department=Finance)(department=Research))

And then we have the OR filter, which indicates which department:

|(department=Finance)(department=Research)

Don’t feel bad; it is a little confusing, especially at first. But eventually you’ll learn to read and write these LDAP filters almost as easily as you read and write SQL queries.

Note. Well, we did say almost.

Last, but hardly least, we have the NOT filter. What do you suppose this filter does?

$strFilter = "(!(objectCategory=User))"

We won’t keep you in suspense: this filter returns all objects that are not user account objects. (The exclamation point – ! – indicates a NOT filter.) Need a list of users who do not have a telephone number? This filter should do the trick:

$strFilter = "(&(objectCategory=User)(!(telephoneNumber=*)))"

For more information, take a peek at the LDAP Filter Syntax on MSDN.

That’s All For Now

Like we said, this isn’t necessarily everything you need to know about writing Active Directory search scripts, but it should be enough to get you started. Let us know if you have additional questions, and we’ll see what we can do about addressing those issues sometime in the near future.