Fine Tuning the Active Directory Searcher

computer search heroimg
Last time, we started looking at the Active Directory Searcher object and how to find a single user object. If you missed it, take a moment to get caught up.
 

 
If you recall, I created a simple searcher.

$searcher = New-Object system.DirectoryServices.DirectorySearcher
$searcher.filter = "samaccountname=jeff"

My Active Directory (AD) domain is not especially large, so the query does not take long to run. However, you will want to fine tune your search to be as specific and limited as possible. We will look at some filtering techniques later. Right now, I want to focus on search scope by discussing how much AD you will need to search.
When you create a search object, it defaults to the domain root for the current logged on user.

The searcher search root (Image Credit: Jeff Hicks)
The Searcher Search Root (Image Credit: Jeff Hicks)

When I invoked the FindOne() method, it searched the entire domain structure. That may be perfectly acceptable if you have no idea where the object is located. In my case, I know that all active user accounts are under the Employees organizational unit. I will need to know the distinguished name, and assuming that I do, I can reposition my search.

$searcher.SearchRoot = "LDAP://ou=employees,dc=globomantics,dc=local"

Now the search runs very quickly.

Searching for an object (Image Credit: Jeff Hicks)
Searching For an Object (Image Credit: Jeff Hicks)

This search took 27ms and it took 35ms when searching the entire domain. Sure, that is hardly earth shattering but this will make a difference when searching for many objects. If you have a large domain, it will make a bigger difference.

Another tweak you can make is to modify the set of object properties that get returned. If you recall from last time, the user account result came back with 75 properties. What if I know I only need a small subset? I can configure the searcher to only retrieve those properties.
This takes a little juggling to get the property names loaded.

$props = "distinguishedname","name","samaccountname","title","department","directreports"
foreach ($item in $props) {
    $searcher.PropertiesToLoad.Add($item) | out-null
}

Search Properties to Load (Image Credit: Jeff Hicks)
Search Properties to Load (Image Credit: Jeff Hicks)

The result is now limited to these properties and ran a little quicker at 22ms.
Limited Property Results (Image Credit: Jeff Hicks)
Limited Property Results (Image Credit: Jeff Hicks)

You can use the techniques from the previous article to clean this up.
You can also get the complete object with the GetDirectoryEntry() method. If you recall, the FindOne() and FindAll() methods write a result object to the pipeline, not the actual object.
Getting the full directory entry (Image Credit: Jeff Hicks)
Getting the Full Directory Entry (Image Credit: Jeff Hicks)

Here is another way to display properties cleanly:

$entry | Select @{Name="DN";Expression={$_.DistinguishedName.value}},
@{Name="SAM";Expression={$_.samAccountname.value}},
@{Name="Name";Expression={$_.name.value}},
@{Name="Title";Expression={$_.title.value}},
@{Name="Dept";Expression={$_.department.value}},
@{Name="DirectReports";Expression = {$_.directreports.value}}

Selected Properties (Image Credit: Jeff Hicks)
Selected Properties (Image Credit: Jeff Hicks)

I could have easily retrieved these properties alone in the searcher without having to get the full object. If you want to do a full directory entry, you do not have to resort to 100 Select-Object expressions.

$entry.Properties.GetEnumerator()|
Foreach -begin {
  $h = @{}
} -process {
    $h.add($_.PropertyName,$_.value)
} -end {
    new-object psobject -Property $h
}

A better property display (Image Credit: Jeff Hicks)
A Better Property Display (Image Credit: Jeff Hicks)

Let’s put everything we have looked at into a function that will get the full user object from AD with nothing but the SamAccountname.

Function Get-MyADUserObject {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$SamAccountname,
[ValidatePattern("^LDAP://")]
[string]$SearchRoot
)
$searcher = New-Object system.DirectoryServices.DirectorySearcher
$searcher.filter = "samaccountname=$SamAccountName"
#limit search properties since we're going to get the complete user object
$searcher.PropertiesToLoad.Add("distinguishedname") | out-null
if ($SearchRoot) {
    $searcher.SearchRoot = $SearchRoot
}
$user = $searcher.FindOne()
if ($user.Path) {
    $entry = $user.GetDirectoryEntry()
    $entry.Properties.GetEnumerator()|
    Foreach -begin {
      $h = @{}
    } -process {
        $h.add($_.propertyName,$_.value)
    } -end {
        new-object psobject -Property $h
    }
}
else {
    Write-Warning "Could not find user $samaccountname under $($searcher.SearchRoot.Path)"
}
} #end function

Feel free to expand upon this. Now, I have a tool to get a user from AD that writes an object to the pipeline. I can work with this.

Testing the function (Image Credit: Jeff Hicks)
Testing the Function (Image Credit: Jeff Hicks)


Everything we have been doing for a single object, we can also do for many. We will start down that path next time.