Active Directory – Search

One thing in common for most of my scripts is that they need / use Active Directory (aka Active Directory Domain Services). Microsoft developed PowerShell commandlets for Active Directory, but they require a Windows Server 2008 or higher and Active Directory Web Services installed. I usually can’t get this.

Quest developed Active Roles Management Shell which includes commandlets to manage AD. Although there are very comprehensive and good, I would need to install it. And sometimes I do not have local admin rights.

Both of above are relatively new (well relative to my engagements requiring AD tools).

My oldest function, which is still in use, is Search-AD, which does exactly what it called; searching AD. It’s a reincarnation of a function I have first written in Perl.  It’s not as powerful than the Quest pendant, nor as feature complete than the standalone tool ADFind.exe. But it’s robust and flexible in case I need something in addition. I need this function in many other functions. This is why I want to start with this oldtimer. It will return either a System.DirectoryServices.SearchResultCollection or a System.DirectoryServices.SearchResult which means you need to decode some attributes after receiving the result. For instance translating “pwdLastSet” attribute to dateTime. I intentionally do not decode it in the function itself to keep it flexible. It might be worth to have a blog entry about attribute decoding, what do you think?

OK, I have to step back a bit. I need two helper functions which are used in Search-AD: Add-Enum (and just for convenience Get-Enum Members) and Test-LDAPPath.

I do use enumerations for parameters whenever possible. Originally it was from the PowerShell Guy. Like usual I added a proper AutoHelp.

Here I added also the possibility to change value type as it happened that [int] was not always enough.

It uses CSharp code to create new enumeration via Add-Type.

#region Test-LDAPPath
function Test-LDAPPath {
<#
.SYNOPSIS
Tests if an Object in AD exists.

.DESCRIPTION
Tests if an Object in AD exists. Valid inputs are a DN or a DirectoryEntry object.
With parameter you can force an input type. Without a parameter the script figures out if an input was a string or
directoryEntry object.

.PARAMETER  DistinguishedName
The distinguished name of the object to be tested. The DN may be specified using aDSPath or distinguished name.
If an incorrect DN syntax is supplied, it will return $false

.PARAMETER  DirectoryEntry
A DirectoryEntry object to be tested.
If a string value is passed, it will be casted to a DirectoryEntry.

.PARAMETER  isValid
Verifies that the syntax of the DN is correct.
Returns $true if valid and otherwise $false

.EXAMPLE
PS C:> Test-LDAPPath  “CN=myOU,DC=test,DC=intra”

——————————————————-

Test if an object with the DN CN=myOU,DC=test,DC=intra exists.

.EXAMPLE
PS C:> Test-LDAPPath  “CN=myOU,DC=test,DC=intra” -isValid

——————————————————-

Test if the DN CN=myOU,DC=test,DC=intra is correct.

.EXAMPLE
PS C:> Test-LDAPPath -DirectoryEntry ([DirectoryServices.DirectoryEntry]”LDAP://CN=myOU,DC=test,DC=intra”

——————————————————-

Test if an object represented by the directoryEntry of CN=myOU,DC=test,DC=intra exists

.EXAMPLE
PS C:> Test-LDAPPath “LDAP://CN=myOU,DC=test,DC=intra”

——————————————————-

Test if an object with the DN CN=myOU,DC=test,DC=intra exists.

.EXAMPLE
PS C:> Test-LDAPPath ([DirectoryServices.DirectoryEntry]”LDAP://CN=myOU,DC=test,DC=intra”

——————————————————-

Test if an object represented by the directoryEntry of CN=myOU,DC=test,DC=intra exists

.INPUTS
System.String,DirectoryServices.DirectoryEntry

.OUTPUTS
System.Bool

.Notes
NAME:      Test-LDAPPath
AUTHOR:    Patrick Sczepanski
Version:   20110712
#Requires -Version 2.0

#>

[Cmdletbinding()]

Param(
[Alias(‘dn’)]
[ValidateNotNull()]
[Parameter(ValueFromPipeline=$true,Mandatory=$True,Position=1,ParameterSetName=”String”)]
[string]$DistinguishedName,

[Parameter(ParameterSetName=”String”)]
[switch]$isValid,

[ValidateNotNull()]
[Parameter(ValueFromPipeline=$true,Mandatory=$True,Position=1,ParameterSetName=”DirectoryEntry”)]
[System.DirectoryServices.DirectoryEntry]
$DirectoryEntry,

[Parameter(ParameterSetName=”String”)]
[string]
$connection,

[Parameter()]
[switch]
$Passthru

)
# the function uses different return points due to multiple decisions taken
# it seemed easier to do it this way than try to collect all variations and evaluate again at the end
switch ($pscmdlet.ParameterSetName) {
“String”    {
if ( $connection ) {
$connection = $connection + “/”
} else {
$connection = “”
}
switch  -regex ( $DistinguishedName ) {
‘^(((LDAP|GC)://)(([w.-])+/)*)+((CN|OU)=.*)*(DC=.*)+$’ {
if ( $isValid ) {
# only verified syntax
return $true
} else {
$LDAPpath = $DistinguishedName
}
break
}
‘^(([w.-])+/)*((CN|OU)=.*)*(DC=.*)+$’ {
$LDAPpath = “LDAP://$connection$DistinguishedName”
break
}
Default { return $false }
}
if ( [DirectoryServices.DirectoryEntry]::exists(“$LDAPpath”) ) {
if ( $Passthru ) {
return  [DirectoryServices.DirectoryEntry]”$LDAPpath”
} else {
return $true
}
} else {
return $false
}
break
}
“DirectoryEntry” {
if ( [string]::IsNullOrEmpty( $DirectoryEntry.objectGUID ) ) {
return $false
} else {
if ( $Passthru ) {
return $DirectoryEntry
} else {
return $true
}
}
break
}
}
}
#endregion Test-LDAPPath

And finally this is the search function. Reading the help should already explain most of it. Type Get-Help -full to read the auto help.
You can see how the enumerations are used in the Param section:

[CATools.AD.Provider]$Provider

makes sure you can only enter the values of CATools.AD.Provider enum.

[CATools.AD.Provider]::LDAP

is setting the default to LDAP search. Alternatively GC could be used

function Search-AD  {
<#
.Synopsis
Searching Active Directory

.Description
Searching Active Directory

.Parameter provider
Provider to use to connect. Allowed are GC and LDAP
Default: LDAP

.Parameter connection
Optional domain controller name used to connect to execute the search
Default: Any (closest)

.Parameter Searchbase
Distinguished name of the search base to start the search from
Default: Current Domain

.Parameter Filter
LDAP filter
Default: (objectclass=*)

.Parameter Attributes
Attributes to be returned
Default distinguishedname

.Parameter Scope
Search scope. Allowed scopes are base, onelevel, subtree
Default: base

.Parameter PageSize
Number of objects returned per page. In standard AD user a number below 1000 which is the default maximum object returned in one step
Default: 1000

.Parameter SizeLimit
Maximum number of objects returned. Enter 0 for unlimited numer
Default: 1000

.Parameter ChooseItem
Allows to choose a single item out of multiple items returned by the search

.Parameter PropertyNamesOnly
Returns only the property names without values

.Parameter FindOne
Returns the first object matching

.Example
$Group = Search-AD -connection “DC1.mydomain.com” -filter “(&(objectcategory=group)(samaccountname=group51)))” -searchbase “DC=mydomain,DC=com” -scope “subtree”

.OUTPUTS
System.DirectoryServices.SearchResultCollection; System.DirectoryServices.SearchResult

.INPUTS
NA

.Link
Search-AD

.Notes
NAME:      Search-AD
AUTHOR:    Patrick Sczepanski
Version:   20110714a
#Requires -Version 2.0
#>
[Cmdletbinding(DefaultParameterSetName=”DN”)]
Param (
[Parameter(ValueFromPipelineByPropertyName=$true,Position=0)]
[CATools.AD.Provider]$Provider   = [CATools.AD.Provider]::LDAP,

[Parameter(ValueFromPipelineByPropertyName=$true)]
[string]
$Connection,

[Alias(‘base’,’b’)]
[Parameter(ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName=”DN”)]
[ValidateScript( {Test-LDAPPath $_ -IsValid} )]
[string]
$Searchbase = ([DirectoryServices.DirectoryEntry]”LDAP://RootDSE”).DefaultNamingContext,

[Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName=”Domain”)]
[switch]
$Domain,

[Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName=”Forest”)]
[switch]
$Forest,

[Alias(‘f’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[string]
$Filter        = “(objectclass=*)”,

[Alias(‘a’,’attribute’,’attrib’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[string[]]
$Attributes,

[Alias(‘s’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[DirectoryServices.SearchScope]
$Scope         = “base”,

[Parameter(ValueFromPipelineByPropertyName=$true)]
[int32]$PageSize = 1000,

[Parameter(ValueFromPipelineByPropertyName=$true)]
[int32]$SizeLimit = 1000,

[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]$PropertyNamesOnly,

[Alias(‘choose’,’select’,’c’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]
$ChooseItem,

[Alias(‘One’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]
$FindOne,

[Alias(‘Chasing’)]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[DirectoryServices.ReferralChasingOption]
$ReferralChasing = [DirectoryServices.ReferralChasingOption]::None

)
Begin {
if ( $connection ) {
$connection = $connection + “/”
} else {
$connection = “”
}

if ( $attributes -notcontains “distinguishedname” ) {
$attributes +=  “distinguishedname”
}
if ( $SearchBase -match “^(LDAP|GC)://.*” ) {
} else {
$SearchBase =  $provider.ToString() + “://$Connection$SearchBase”
}
}
Process {
[DirectoryServices.DirectoryEntry]$searchbaseURI = $SearchBase
[DirectoryServices.DirectorySearcher]$Searcher = new-object DirectoryServices.DirectorySearcher($searchbaseURI)
$Searcher.filter = $filter
$Searcher.CacheResults = $true
$Searcher.SearchScope = $scope
$Searcher.PageSize = $PageSize
$Searcher.PropertiesToLoad.AddRange($attributes)
$Searcher.ReferralChasing = $ReferralChasing
if ( $FindOne ) {
[System.DirectoryServices.SearchResult]$result = $searcher.FindOne()
} else {
[System.DirectoryServices.SearchResultCollection]$result = $searcher.FindAll()
}

switch ( $result ) {
{ ($_ -is [System.DirectoryServices.SearchResultCollection]) -and ($_.count -lt 1) } { Write-Debug “[Search-AD] :: Cannot find an object in $searchbase using filter $filter”
return $null
break }
{ ($_ -is [System.DirectoryServices.SearchResult]) } { Write-Debug “[Search-AD] :: Found 1 Object”
return $result
break }
{ ($_ -is [System.DirectoryServices.SearchResultCollection]) -and ($_ -gt 1) } {
if ( $chooseitem ) {
[int]$count = -1
foreach($object in $result) {
$count = $count + 1
Write-Host “[$count]: ” $object.Properties.distinguishedname
}
$selection = Read-Host “Please select object”
if ( $($selection -lt 0) -or $($selection -gt $count) -or $($count -isnot [int])  ) { Write-Error “Selection ‘$selection’ out of scope.`r`nPlease enter an integer value between 0 and $count.”; exit(0)  }
return $result[$selection]
} else {
return $result
}
break }
default { Write-Error -message “Issue with switch statement. Please check code – Search-AD function. Unexpected Error.”;
exit()
}
}
}
End {

}
}
#endregion Search-AD

Next time I’ll explain the details of the Test-LDAPPath and Search-AD functions.

Stay tuned…

Patrick Sczepanski ist seit 11 Jahren im Bann der IT Industrie. Er hat schon für Kunden verschiedenster Grössen gearbeitet (von unter 200 bis über 200.000 Mitarbeitern). Bei der redtoo ist er, als Senior Consultant, Experte für den Bereich Infrastructure Services.