1 Einleitung
1.1 Wiederholung
     Beispiel 1b: [ADSI]
     Beispiel 1b: [ADSISearcher]
     Beispiel 2: LDAP_Queries vom PDCEmulator beantworten lassen
1.2 Einführungsbeispiele
1.2.1 Suche nach bestimmten Objekten
       Beispiel 1a: Suchen eines Accounts in der aktuellen Domäne und Auslesen einiger Eigenschaften des Treffers (ohne [ADSI]
       Beispiel 1b: Suchen eines Accounts in der aktuellen Domäne und Auslesen einiger Eigenschaften des Treffers (ohne [ADSI]

       Beispiel 1c: Auffinden genau eines Objects ([ADSISearcher]) ohne [ADSI]
       Beispiel 2: Suchen eines Accounts in einer fremden Domäne und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] -> [ADSI] )
       Beispiel 3a: Suchen eines Accounts in einem fremden Forest und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] -> [ADSI] )
       Beispiel 3b: Suchen eines Accounts in einem fremden Forest und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] ohne [ADSI]

1.2.2 Suche nach einem oder mehreren Objekten
        Beispiel 1: Aulisten aller Computerkonten (DomainController) innerhalb einer OU ([ADSISearcher)
        Beispiel 2: Auflisten aller Domaincontroller einer Domäne (dsquery)
        Beispiel 3: Auflisten aller User in einer OU ([ADSISearcher])
        Beispiel 4: Suchen nach einem Suchkriterium mit Platzhalter
1.3 Forest und Domänen nach Objekten Eigenschaften filten
       Beispiel 1: Verzeichnis nach User filtern (Passwordlastset, Accountdisabled, AccountExpires, WhenCreated, CreatedAfter)

2  LDAP- und GlobalCatalog Queries
    Beispiel 1: Bestimmen, ob ein Attribut im GlobalCatalog veröffentlicht ist
    Beispiel 2a: Auflisten aller Objekte einer OU (ohne Rekursion)
    Beispiel 2b: Suchen ab einem Eintiegspunkt nach einem Namensbestandteil (mit Rekursion)

3 Schema - Queries
3.1 Einleitung
3.2 Mit Powershell ins AD-Schema blicken
      Beispiel 1a: Anzeigen aller Klassen des ADSchemas
      Beispiel 1b: Vergleich des ActiveDirectorySchema auf einem W2k8R2 und einem Windows2012 Domaincontroller
      Beispiel 2: Anzeigen aller Eigenschaften der User-Klasse und ob diese "SingleValued","Indexed" und "InGlobalCatalog" sind
      Beispiel 3: indizierte oder im GC veröffentlichte Eigenschaften anzeigen
      Beispiel 4: Abfragen einer Klasseneigenschaft ("IsSingleValued)
      Beispiel 5: Anzeige der ActiveDirectorySchemaProperty-Eigenschaften

4 komplexere Queries mit [ADSISearcher] DirectorySearcher
4.1 Methoden von [ADSISearcher]
     Beispiel 1: Die Methode FindAll()
     Beispiel 2: Die Methode FindOne()
     Beispiel 3: Welche Properties kann man direkt aus einem mit Findall() oder FindOne() gefundenen Objekt lesen?
     Beispiel 4: Die Eigenschaften accountexpires und pwdlastset
     Beispiel 5:  Die Properties CanonicalName und IsDisabled
4.2 Eigenschaften von [ADSISearcher]
4.2.1 Eigenschaft Asynchronous
        Beispiel 1: Asynchrone Suche nach einem Useraccount
4.2.2 Eigenschaft CachedResults
        Beispiel 1: Eigenschaft CachedResults
4.2.3 Eigenschaft Filter
4.2.3.1 Hilfsmittel zum Filterbau
4.2.3.2 FilterAttribute ObjectCategory und ObjectClass
          Beispiel 1: ObjectCategory vs. ObjectClass
4.2.3.3 Resourcenverbrauch und Perfomance von Filtern
4.2.3.4 LDAPControls
4.2.3.4.1 bitweiser AND/ OR Vergleich
              Beispiel 1: Suche nach einem Gruppentyp mit bitweisem Vergleich / Anzahl der Gruppenmitglieder bestimmen
              Beispiel 2: Aufzählen aller Category 1 oder Category 2 Objekte des Schemas
4.2.3.4.2 Suche in allen VorgängerObjekten
              Beispiel 1: Suchen in einer Objektkette, Mitglieder einer Gruppe auslesen
              Beispiel 2: Suchen in einer Objektkette / Prüfen, in welchen Gruppen ein User Mitglied ist
4.2.3.4.3 extended LDAPControls
4.2.3.4.4 Zeit in LDAPFiltern (ADS_UTC_TIME)
              Beispiel 1: Alle Benutzer ausgeben, die nach dem 22.08.2013 angelegt wurden
4.2.3.4.5 SID und UAC
              Beispiel 1: SID und UAC bestimmen
4.2.4 Eigenschaft Pagesize
4.2.5 Eigenschaft Searchroot
        Beispiel 1: Eigenschaft SearchRoot
        Beispiel 2: Suche im Schemacontainer
4.2.6 Eigenschaft Searchscope
4.2.7 Eigenschaft Tombstone
        Beispiel 1: Anzeigen aller tombstoned User und Computer einer Domäne
4.3 Kombination aus [ADSISearcher] und [ADSI] / Objekte verändern
      Beispiel 1: Suchen und Verändern eines Userobjektes
      Beispiel 2: Suche nach Usern mit leeren Feldern (Homedirectory oder TerminalservicesProfilepath)
4.4 Existenzprüfung von AD Elementen
     Beispiel 1: Existenzprüfung von AD-Objekten (User, Computer, OUs) mit der FindOne-Methode der [ADSISearcher] (=DirectorySearcher)-Klasse
     Beispiel 2: Existenzprüfung von AD-Objekten (User, Computer) mit den CmdLets Get-ADUser und Get-ADComputer
     Beispiel 3: Existenzprüfung von AD-Objekten (User, Computer, OUs) mit der Exists-Methode der [ADSI] (=DirectoryEntry)-Klasse
     Beispiel 4: Anlage eines Users mit vorheriger Existenzprüfung mit Get-ADuser
4.5 Analyse des LDAP-Traffics
4.5.1 Tracelog.exe
4.5.2 LDAP-Pakete mit Netmon analysieren

 

1 Einleitung

LDAP-Queries sind in der ActiveDirectory-Datenbank ein sehr wichtiger Aspekt! Mittels LDAP-Queries

  • finden Directory Clients einen DomainController
  • finden Anwender Resourcen wie Drucker oder Freigaben
  • finden Administratoren Informationen zu ihrem Netzwerk, etwa wieviele Accounts seit 180 Tagen nicht mehr benutzt wurden
  • ... vieles mehr

So wie bei jeder Datenbank sind auch für die ActiveDirectory-Datenbank Abfragen (=Queries) ein Kernmerkmal. Eine Query liefert entweder kein, ein oder mehrere Ergebnis(se) zurück, die den Suchkriterien entsprechen. Typische Abfragen im AD sind beispielsweise

  • alle Useraccounts, die seit 6 Monaten nicht mehr benutzt wurden
  • alle Computeraccounts, die im Beschreibungsfeld (=Description) einen bestimmten Text enthalten
  • leere Gruppen
  • alle Mitglieder einer Gruppe
  • Existenz eines Objektes
  • Lokalisieren eines passenden DomainControllers

und vieles mehr.

Neben dieser knappen Einleitung gibt es natürlich viel mehr zu LDAP-Queries zu sagen, wie zum Beispiel in diesen lesenswerten Artiken

What Are Active Directory Searches?

How Active Directory Searches Work
 

Searching Active Directory with Windows PowerShell (viele Beispiele von MS)

 

1.1 Wiederholung

Bevor wir mit Queries richtig loslegen, noch eine kleine Erinnerung aus einem der letzten Kapitel: Provider (Moniker)

Beispiel 1a: [ADSI]
Die folgenden Schreibweisen, um ein Object der Klasse "System.DirectoryServices.DirectoryEntry" zu erstellen sind identisch!

$UserDN = "CN=Administrator,CN=Users,DC=dom1,DC=intern"

# gleichwertige Syntaxvarianten

$User = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$UserDN")
$User = [System.DirectoryServices.DirectoryEntry]"LDAP://$UserDN"
$User = [ADSI]"LDAP://$UserDN"

Üblicherweise wird die gelb hinterlegte Syntax genutzt, dennoch schadet es nicht die eigentliche Bedeutung des ShortAccelerators [ADSI] zu kennen

Beispiel 1b: [ADSISearcher]
Die folgenden Schreibweisen, um ein Object der Klasse "System.DirectoryServices.DirectorySearcher"  zu erstellen sind identisch!

Clear-Host
Set-StrictMode -Version "2.0"

#gleichwertige Syntaxvarainten

$SamAccountName = "Munich_Karl_10000*"

"`n"
$Users = ([ADSISearcher]"((SamaccountName=$SamAccountName))").FindAll()
$Users.Properties.samaccountname

"`n"
$Users = ([System.Directoryservices.Directorysearcher]"(SamaccountName=$SamAccountName)").FindAll()
$Users.Properties.samaccountname

"`n"
$DirectorySearcher = New-Object System.DirectoryServices.Directorysearcher
$DirectorySearcher.Filter = "(SamaccountName=$SamAccountName)"
$Users = $DirectorySearcher.Findall()
$Users.Properties.samaccountname

"`n"

$DirectorySearcher=([ADSISearcher]"LDAP://")  #meine favorisierte Schreibweise
$DirectorySearcher.Filter = "(SamaccountName=$SamAccountName)"
$Users = $DirectorySearcher.Findall()  
$Users.Properties.samaccountname


An einigen Stellen muss man sehr genau auf die Syntax aufpassen, da hier nicht die allgemeinen Powershellregeln gelten:
 

 In $DirectorySearcher.Filter = "(SamaccountName=$SamAccountName)" darf kein Leerzeichen in der Filterbedingung enthalten sein
 

 In  $Users.Properties.samaccountname muss der PropertyName mit einem Kleinbuchstaben beginnen

Ohne Beachtung dieser Regeln bekommt ihr bei allen Varianten keine Ergebnisse zurück


[ADSI] und [ADSISearcher] sind sich relativ ähnlich. Einige DatumsEigenschaften eines Objektes wie "pwdlastset" oder "lastlogontimestamp" sind über [ADSISearcher] jedoch abzufragen. Weiter unten dazu mehr.

Beispiel 2: LDAP_Queries vom PDCEmulator beantworten lassen
Oft ist es vorteilhaft die Queries nicht gegen einen beliebigen DomainController, sondern gegen den sogenannten PDCEmulator der Domäne zu schicken. Zum einen besitzt der PDCe oft die leistungsstärkste Hardware, zum Anderen hält er die aktuellsten Daten seiner Domäne.

Clear-Host
Set-Strictmode -Version "2.0"

$UserDN = "CN=Administrator,CN=Users,DC=dom1,DC=intern"

$DomainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe = $Domain.PdcRoleOwner.Name
#$PDCe = "WIN-VSKGC0FLE6E.Dom1.Intern"

$User = [ADSI]"LDAP://$PDCe/$UserDN"

Den PDCe, so wie jeden anderen DC, kann man fest eintragen, oder dynamisch mit der statischen Methode "GetCurrentDomain" under der Methode "PdcRoleOwner ermitteln.

 

1.2 Einführungsbeispiele

Zu Beginn einige Beispiele, die so auch immer wieder in der Praxis auftauchen können. Dieses Unterkapitel soll als übersichtliches SkriptRepository dienen, aber natürlich auch Appetit auf mehr Queries machen. Systematisch beschreiben werde ich die angewandten Techniken erst in den folgenden Kapiteln.
 

1.2.1 Suchen nach einzelnen Objekten

 

Beispiel 1a: Suchen eines Accounts in der aktuellen Domäne und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] -> [ADSI] )

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Filterkriterium SamAccountName
  $SamAccountName = "KarlNapf"
 
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
  $PdcRoleOwner = $Domain.PdcRoleOwner.Name
 
  #functioncall
  $LDAPUserName = Get-Dn $SamAccountName $Domain $PdcRoleOwner
 
  #verbinden mit [ADSI]
  [ADSI]$User = $LDAPUserName   #hier ist der wesentliche Unterschied zu Beispiel 1b)

  Write-Host "SamAccountName: $($User.SamAccountName)"
  Write-Host "Description: $($User.Description)"
  Write-Host "WhenCreated: $($User.WhenCreated)"
  Write-Host "LastLogonTimeStamp: $($User.LastLogonTimeStamp)"
  Write-Host "BadPwdCount: $($User.BadPwdCount)"
  Write-Host "LogonCount: $($User.LogonCount)"
  Write-Host "otherTelephone: $($User.otherTelephone)"
  Write-Host "AccountDisabled: $($User.AccountDisabled)"

}#end Function Main

Function Get-Dn {
    Param($SamAccountName,$Domain,$PdcRoleOwner)
           
    $SearchScope = "Subtree"
    $Searchrootpath = $Domain
    
    $DirectorySearcher = ([ADSISearcher]"LDAP://$PdcRoleOwner")
    $DirectorySearcher.Filter = "(SamAccountName=$SamAccountName)"
    $DirectorySearcher.SearchScope = $SearchScope
    $DirectorySearcher.Searchroot = "LDAP://$SearchrootPath"
      
    Try{
      $LDAPUserName = $DirectorySearcher.FindOne().Path
    }Catch{
      Write-Host "Error: $SamAccountName not found in $Domain"
      Break
    }
    
    Return $LDAPUserName
}#end Function GET-DN

Main
#mögliche Ausgabe

SamAccountName: Munich_Karl_1000001
Description: created by the children.add-method
WhenCreated: 11/13/2013 18:01:21
LastLogonTimeStamp: System.__ComObject
BadPwdCount: 2
LogonCount: 1
otherTelephone: 666666 12345
AccountDisabled: True

Das Skript holt sich selbst mit der statischen Methode "GetCurrentDomain" der Klasse "System.DirectoryServices.ActiveDirectory" die aktuelle Domäne des angemeldeten Users. Um die ComputerDomäne zu erhalten benutzt einfach die Methode "GetComputerDomain"

MSDN: Domain.GetCurrentDomain Method (User-Domain)

MSDN: Domain.GetComputerDomain Method (Computer-Domain)

Anschließend wird die gefundene Domäne per LDAP nach einem SamAccountName ("$DirectorySearcher.FindOne().Path") durchsucht. Das zurückgelieferte $DirectorySearcherObject "$LDAPuserName" wird in ein DirectoryEntryObject gecastet ("[ADSI]$ADSIUser"), aus dem weitere AD-Eigenschaften wie Description oder WhenCreated bequem auslesbar und vor allem beschreibbar(!) sind. Manche Eigenschaften wie LastLogontimeStamp sind leider als Systemobjekte gespeichert. Deren Umwandlung in lesbare Werte zeige ich weiter unten

Beispiel 1b: Suchen eines Accounts in der aktuellen Domäne und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher]) ohne [ADSI]
Im Gegensatz zu Beispiel 1a)  benutze ich direkt das von der FindOne()- Methode zurückgelieferte UserObject zum Auslesen der  Properties. Die übrigen Skriptzeilen sind im Beispiel 1a) beschrieben

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Filterkriterium SamAccountName
  $SamAccountName = "Munich_Karl_1000001"
 
  #get current domain /PDC
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
  $PdcRoleOwner = $Domain.PdcRoleOwner.Name

  #SearchRoot
  $SearchRoot = $Domain
  #$SearchRoot = "OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
 
  #functioncall
  $User = FindOne-Object $SamAccountName $SearchRoot $PdcRoleOwner
 
  #output
  Write-Host "SamAccountName: $($User.Properties.Item('SamAccountName'))"
  Write-Host "Description: $($User.Properties.Item('Description'))"
  Write-Host "WhenCreated: $($User.Properties.Item('WhenCreated'))"
  Write-Host "LastLogonTimeStamp: $($User.Properties.Item('LastLogonTimeStamp'))"
  Write-Host "BadPwdCount: $($User.Properties.Item('BadPwdCount'))"
  Write-Host "LogonCount: $($User.Properties.item('LogonCount'))"
  Write-Host "otherTelephone: $($User.Properties.item('otherTelephone'))"
 
  #scriptblock
  $Get_AccountDisabled = {
    $UAC = $($User.Properties.item('userAccountControl'))
    if(($UAC -BAND 2) -eq 0){
      Return "False" }
    Else{ "True" }}
 
 $AccountDisabled = &($Get_AccountDisabled)
 Write-Host "AccountDisabled: $AccountDisabled"

 #$User.psbase.Properties #show all properties  #zeige alle Eigenschaften mit Werten
 
}#end Function Main

Function FindOne-Object {
    Param($SamAccountName,$SearchRoot,$PdcRoleOwner)
           
    $SearchScope = "Subtree"
        
    $DirectorySearcher = ([ADSISearcher]"LDAP://$PdcRoleOwner")
    $DirectorySearcher.Filter = "(SamAccountName=$SamAccountName)"
    $DirectorySearcher.SearchScope = $SearchScope
    $DirectorySearcher.Searchroot = "LDAP://$Searchroot"
      

    $User = $DirectorySearcher.FindOne()
    If([Bool]$User){
      Return $User
      }Else{
        Write-Host "Error: $SamAccountName not found in $Domain"
        Exit
    }
}#end Function FindOne-Object

Main
#mögliche Ausgabe

SamAccountName: Munich_Karl_1000001
Description: created by the children.add-method
WhenCreated: 11/13/2013 18:01:21
LastLogonTimeStamp: 130374053257416172
BadPwdCount: 2
LogonCount: 1
otherTelephone: 666666 12345
AccountDisabled: True

Die Ergebnisse zum obigen Beispiel sind meist identisch , manchmal ähnlich, manchmal leider aber auch ganz anders. Vergleicht einfach mal die Ausgaben von Beispiel 1a und 1b.

Etwas Tricksen muss man zum Bestimmen der AccountDisabled-Eigenschaft, da diese im Userobject nicht direkt enthalten ist. Daher der Umweg über einen Skriptblock und die Eigenschaft "UserAccountControl"
Leider werden Datumswerte nicht konsistent. (whenCreated <-> LastLogonTimeStamp) zurückgeliefert. Die Umwandlung des Wertes von LastlogontimeStamp in ein lesbares Format zeige ich später, daher nur kurz vorweg

$LastLogonTimeStamp = $([DateTime]$($User.Properties.Item('LastLogonTimeStamp')))
$LastLogonTimeStamp = $( $LastLogonTimeStamp).AddYears(1600) ).ToString("yyyy-MM-dd")
 


Beispiel 1c: Auffinden genau eines Objects ([ADSISearcher]) ohne [ADSI]
Im Prinzip mache ich hier dasselbe wie in Beispiel 1b), nur ohne den Einsatz einer Function und einer etwas anderen Syntax zum Auslesen der Properties. Vielleicht ist es so etwas verständlicher

Set-StrictMode -Version "2.0"
Clear-Host

$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$Pdce = $Domain.PdcRoleOwner.Name

$SearchRoot = "DC=Dom1,DC=Intern"
$Filter = "(&(SamAccountName=Munich_Karl_1000001)(ObjectCategory=User))"
 
$DirectorySearcher = ([ADSISearcher]"LDAP://$PDCe")  
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.SearchRoot = "LDAP://$Searchroot"

$DirectorySearcher.Findone().properties.samaccountname
$DirectorySearcher.Findone().properties.whenchanged
#mögliche Ausgabe

Munich_Karl_1000001
Mittwoch, 13. November 2013 20:56:33

Die FindOne-Methode beendet die Suche, sobald der erste Treffer gefunden wurde

       Beachtet wieder die Syntaxhinweise aus Kapitel 1.1 Beispiel 1b).
 

Beispiel 2: Suchen eines Accounts in einer fremden Domäne und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] -> [ADSI] )

Man möchte nicht immer in seiner eigenen Domäne des Forests suchen. Das stellt mit .Net ebenfalls keine größere Herausforderung dar

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Filterkriterium SamAccountName
  $SamAccountName = "KarlNapf"
  $DomainName = "dom1.root.intern"
 
  $Context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$DomainName)
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context)
    
  $PdcRoleOwner=$Domain.PdcRoleOwner.Name
 
  #Funktionsaufruf
  $LDAPUserName = Get-Dn $SamAccountName $Domain $PdcRoleOwner
 
  [ADSI]$ADSIUser = $LDAPUserName
  Write-Host "Description: $($ADSIUser.Description)"
  Write-Host "WhenCreated: $($ADSIUser.WhenCreated)"
}#end Function Main

Function Get-Dn {
    Param($SamAccountName,$Domain,$PdcRoleOwner)
           
    $SearchScope = "Subtree"
    $Searchrootpath= $Domain
    
    $DirectorySearcher = ([ADSISearcher]"LDAP://$PdcRoleOwner")
    $DirectorySearcher.Filter="(SamAccountName=$SamAccountName)"
    $DirectorySearcher.SearchScope = $SearchScope
    $DirectorySearcher.Searchroot="LDAP://$SearchrootPath"
      
    Try{
      $LDAPUserName = $DirectorySearcher.FindOne().Path
    }Catch{
      Write-Host "Error: $SamAccountName not found in $Domain"
      Break
    }
    
    Return $LDAPUserName
}#end Function GET-DN

Main

 


Zu Beispiel 1a haben sich lediglich die grün eingefärbten Zeilen verändert. Das Beispiel funktioniert beispielsweise in einem Forest gut, wenn man als EnterpriseAdmin mit EnterpriseAdminRechten in einer bestimmten Subdomäne eine LDAP-Abfrage durchführen möchte.

MSDN: DirectoryContext Constructor (DirectoryContextType, String)

Beispiel 3a: Suchen eines Accounts in einem fremden Forest und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] -> [ADSI] )

In diesem Beispiel führe ich eine Abfrage unter veränderten Credentials durch. Um mit der Namensauflösung möglichst wenig Probleme zu bekommen, gebe ich als "Target Type" im DirectoryContext-Object diesmal keinen Domänennamen, sondern einen Domaincontrollernamen (=DirectoryServer) oder dessen IP-Adresse an.

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Filterkriterium SamAccountName
  $SamAccountName = "MickyMaus"
    
  #Credentials
  $User = "KarlNapf"
  $Password = "PassWord"
 
  #Domaincontroller
  $DirectoryServer = "Dc01.Dom2.Extern.Net:389"
  #$DirectoryServer = "10.125.139.16"
 
  $Context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext`
             ("DirectoryServer",$DirectoryServer,$User,$Password)
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context)
  $PdcRoleOwner=$Domain.PdcRoleOwner.Name
 
  #Funktionsaufruf
  $LDAPUserName = Get-Dn $SamAccountName $Domain $PdcRoleOwner
 
  [ADSI]$ADSIUser = $LDAPUserName
  Write-Host "Description: $($ADSIUser.Description)"
  Write-Host "WhenCreated: $($ADSIUser.WhenCreated)"
}#end Function Main

Function Get-Dn {
    Param($SamAccountName,$Domain,$PdcRoleOwner)
           
    $SearchScope = "Subtree"
    $Searchrootpath= $Domain
    
    $DirectorySearcher = ([ADSISearcher]"LDAP://$PdcRoleOwner")
    $DirectorySearcher.Filter="(SamAccountName=$SamAccountName)"
    $DirectorySearcher.SearchScope = $SearchScope
    $DirectorySearcher.Searchroot="LDAP://$SearchrootPath"
      
    Try{
      $LDAPUserName = $DirectorySearcher.FindOne().Path
    }Catch{
      Write-Host "Error: $SamAccountName not found in $Domain"
      Break
    }
    
    Return $LDAPUserName
}#end Function GET-DN

Main


Beispiel 3b: Suchen eines Accounts in einem fremden Forest und Auslesen einiger Eigenschaften des Treffers ([ADSISearcher] ohne [ADSI]

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Filterkriterium SamAccountName
  $SamAccountName = "Munich_Karl_1000001"
 
  #Credentials
  $User = "administrator"
  $Password = "myPassword"

  #Domaincontroller
  $DirectoryServer = "DC02.dom2.intern:389"
  #$DirectoryServer = "10.125.139.16"
 
  $Context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext `
             ("DirectoryServer",$DirectoryServer,$User,$Password)
  Try{
    $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context)
  }Catch{
    Write-Host "Username oder Password sind falsch"
    Exit
  }
  $PdcRoleOwner=$Domain.PdcRoleOwner.Name
 
  #Funktionsaufruf
  $User = FindOne-Object $SamAccountName $Domain $PdcRoleOwner
 
  #output
  Write-Host "Description: $($User.Properties.Item('Description'))"
  Write-Host "Description: $($User.Properties.Item('WhenCreated'))"

}#end Function Main

Function FindOne-Object {
    Param($SamAccountName,$SearchRoot,$PdcRoleOwner)
           
    $SearchScope = "Subtree"
        
    $DirectorySearcher = ([ADSISearcher]"LDAP://")
    $DirectorySearcher.Filter = "(SamAccountName=$SamAccountName)"
    $DirectorySearcher.SearchScope = $SearchScope
    $DirectorySearcher.Searchroot = "LDAP://
$PdcRoleOwner/$Searchroot"
      
    $User = $DirectorySearcher.FindOne()
    If([Bool]$User){
      Return $User
      }Else{
        Write-Host "Error: $SamAccountName not found in $Domain"
        Exit
    }
}
Main
#mögliche Ausgabe

Description: created by the children.add-method
WhenCreated: 01/21/2013 19:43:33

 

MSDN: DirectoryContext Constructor (DirectoryContextType, String, String, String)

 

1.2.2 Suchen nach einem oder mehreren Objekten

Im letzten Kapitel 1.2.1 ging es darum, genau nach einem oder besser nach dem ersten Objekt mittels der FindOne-Methode zu suchen.

Mindestend genauso häufig benötigt man Queries, die nicht nur einen, sondern alle Treffer mit bestimmten Eigenschaften aus einer AD-Datenbank zurückgeben.

Beispiel 1: Aulisten aller Computerkonten (DomainController) innerhalb einer OU ([ADSISearcher)

Clear-Host
Set-Strictmode -Version "2.0"

$rootDSE = [ADSI]"LDAP://rootDSE"
$DomainDN = $rootDSE.defaultNamingContext
$OUName = "OU=Domain Controllers,$DomainDN"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Searchroot = "LDAP://$OUName"
$DirectorySearcher.Filter = "((Objectclass=Computer))"
$DirectorySearcher.Searchscope = "SubTree"
$DirectorySearcher.Findall()  | Foreach{  
   $($_.Properties).dnshostname
   }
#mögliche Ausgabe

DC1.test.net
DC2.test.net
DC3.test.net
DC4.test.net

Wie oben schon (Kapitel 1.1 Beispiel 1b) erwähnt, achtet bei den Properties wie dnshostname bitte auf die Groß- und Kleinschreibung. Ebenso darf kein Leerzeichen innerhalb der Filterbedingung vorkommen.

Beispiel 2: Auflisten aller Domaincontroller einer Domäne (dsquery)

Geht es nur um Domaincontroller ist die Abfrage mit dsquery/ dsget etwas einfacher

Clear-Host
Set-Strictmode -Version "2.0"

dsquery server -domain "dom1.intern" |  dsget server -dnsname
#mögliche Ausgabe

DC1.test.net
DC2.test.net
DC3.test.net
DC4.test.net

In großen Umgebungen liefert dsquery sehr viel schneller Ergebnisse zurück, als die Klasse [ADSISearcher]. Dafür ist der Zugriff auf weitere Eigenschaften der Ergebnisse in dsquery nicht so komfortabel.

Beispiel 3: Auflisten aller User in einer OU ([ADSISearcher])

Set-StrictMode -Version "2.0"
Clear-Host

$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$Pdce = $Domain.PdcRoleOwner.Name

$SearchRoot = "OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
#$SearchRoot = "DC=Dom1,DC=Intern"
 
$DirectorySearcher = ([ADSISearcher]"LDAP://")  
$DirectorySearcher.Filter="(&(SamAccountName=Munich*)(ObjectCategory=User))"
$DirectorySearcher.SearchRoot = "LDAP://
$PDCe/$Searchroot"
$Users = $DirectorySearcher.Findall()
 
Foreach($User in $Users) {
  "{0}  {1} " -f $($User.Properties.Item("SamAccountName")),$($User.Properties.Item("WhenCreated"))
  }
#mögliche Ausgabe

Munich_Karl_1000004  13.11.2013 18:01:21
Munich_Karl_1000005  13.11.2013 18:01:21
Munich_Karl_1000006  13.11.2013 18:01:21


Beispiel 4: Suchen nach einem Suchkriterium mit Platzhalter

Clear-Host
Set-StrictMode -Version "2.0"

$SamAccountName = "Munich_Karl_100*"
$Users = ([ADSISearcher]"((SamaccountName=$SamAccountName))").FindAll()
ForEach($User in $Users){   
    "{0}  {1}" -f $($User.Properties.Item('SamAccountname')),$($User.Properties.Item('WhenCreated'))
    }
#mögliche Ausgabe

Munich_Karl_1000004  13.11.2013 18:01:21
Munich_Karl_1000005  13.11.2013 18:01:21
Munich_Karl_1000006  13.11.2013 18:01:21

 

1.3 Forest und Domänen nach Objekten Eigenschaften filtern

Beispiel 1: Verzeichnis nach User filtern (Passwordlastset, Accountdisabled, AccountExpires, WhenCreated, CreatedAfter)

Ein ähnliches, etwas einfacheres Beispiel findet ihr weiter unten im Kapitel 4.2.3.4.4 Zeit in LDAPFiltern (ADS_UTC_TIME)

Clear-Host
Set-Strictmode -Version "2.0"
 
Function Main {
  ###################### Settings
  #Search Conditions
  $SearchRoot = "LDAP://OU=Scripting,DC=Dom1,DC=Intern"
  $PwdLastSet_Before = "2014-12-01"  #ISO  YYYY-MM-DD
  $CreatedAfter = "2010-11-20"       #ISO  YYYY-MM-DD
  $EnabledUsers = $True #$True, $False or $Null
  $PasswordneverExpires = $Null  #$True, $False or $Null
  $SamAccountName = "UserIMs01*"
    
  #Outpath
  $FileTime = $(Get-Date).ToString("yyyy-MM-dd_HHmmss")
  $OutfilePath = "C:\Temp\$FileTime" +"_Out.csv"
  ####################### End Settings
   
  #ConvertTime
  $CreatedAfter = [Management.ManagementDateTimeConverter]::ToDmtfDateTime($CreatedAfter)  
  $CreatedAfter = $CreatedAfter.Split(".")[0]+".0Z"
  $pwdlastsetBeforeFileTime = $([DateTime]$pwdlastset_Before).ToFileTime()
 
  #Filtercriteria  
  $Filter =@()
  $Filter = "(&(objectcategory=user)(objectclass=user))"
  $Filter += "(pwdlastset<=$pwdlastsetBeforeFileTime)"
  $Filter += "(whencreated>=$CreatedAfter)"
  $Filter += "(samaccountname=$SamAccountName)"
 
  If($EnabledUsers -eq $True){
    $Filter += "(!UserAccountControl:1.2.840.113556.1.4.803:=2)"  #enabled Users
  }Elseif($EnabledUsers -eq $False){
    $Filter += "(UserAccountControl:1.2.840.113556.1.4.803:=2)"  #disabled Users
  }
 
  If($PasswordneverExpires -eq $True){
    $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=65536)"  #password never expires
  }ElseIf($PasswordneverExpires -eq $False){
    $Filter += "(!userAccountControl:1.2.840.113556.1.4.803:=65536)"  #password expires
  }
  [string]$Filter = "(&$Filter)"
  
  #Configuring the DirectorySearcher
  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.Filter = $Filter
  $DirectorySearcher.Searchroot = $SearchRoot
  $DirectorySearcher.SearchScope = "subtree"
  $DirectorySearcher.PageSize = 1000
  $DirectorySearcher.Asynchronous = $True
  $DirectorySearcher.CacheResults = $False
  $DirectorySearcher.PropertiesToLoad.AddRange((`
      'distinguishedName','whenCreated','pwdlastset','accountexpires','useraccountcontrol'))
 
  $PSO_AllUsers = @() 
  $DirectorySearcher.FindAll() | ForEach {
    [psobject]$PSO_User = ""| Select `
       DistinguishedName,pwdlastset,PWDAgeInDays,AccountExpires,whenCreated,IsDisabled,PasswordNeverExpires
    $PSO_User.DistinguishedName = $($($_.Properties).distinguishedname)
    $PSO_User.WhenCreated = $($_.Properties.whencreated).ToString("d")
    Try{
     $PSO_User.accountexpires = [DateTime]::FromFileTime($($_.Properties.accountexpires))
    }Catch{$PSO_User.accountexpires = "never"}
 
    $PwdLastSet = [DateTime]::FromFileTime($($_.Properties.pwdlastset))
    $PSO_User.pwdlastset = $PwdLastSet.ToString("d")
    $PSO_User.PWDAgeInDays = $((get-date)-$PwdLastSet).Days
    
    $PSO_User.IsDisabled = $($_.Properties.Item('userAccountControl')) -Band 2
    $PSO_User.PassWordNeverExpires = $($_.Properties.Item('userAccountControl')) -Band 65536
    
    $PSO_AllUsers += $PSO_User
  }#FindAll
    Write-Results $PSO_AllUsers $OutFilePath
}#end Main
 
Function Write-Results{
    Param ($PSO_AllUsers, $OutFilePath)
    $PSO_AllUsers  | ft * -AutoSize
    $PSO_AllUsers | Export-CSV -Path $OutFilePath -Delimiter ";"
    Write-Host "See $OutFilePath" -BackgroundColor DarkRed -ForegroundColor Yellow  
} #end function main
 
Main

#mögliche Ausgabe

 

DistinguishedName                            pwdlastset PWDAgeInDays AccountExpires whenCreated IsDisabled 
-----------------                            ---------- ------------ -------------- ----------- ---------- 
CN=UserIMS010,OU=Scripting,DC=dom1,DC=intern 20.05.2014 151           never         20.05.2014           0 
CN=UserIMS011,OU=Scripting,DC=dom1,DC=intern 20.05.2014 151           never         20.05.2014           0  

Das Skript filtert also eine Domäne (LDAP-Query) nach Usern,

  • deren Passwörter vor einem bestimmten Datum gesetzt wurde ($PwdLastSet_Before)
  • nach einem bestmmten Datum erstellt wurden ($CreatedAfter)
  • aktiviert sind ($EnabledUsers)
  • passwordneverexpires gesetzt ist ($PasswordneverExpires)

Neben der LDAPQuery kann auch die Ausgabe sehr zeitintensiv sein. Ich benutze in diesem Skript zur Speicherung der Ergebnisse von Findall() eine Collection und kein Array. Arrays sind zur Speicherung großer Datenmengen ungeeignet, wenn immer wieder neue Datensätze angehängt werden. Zur Ausgabe wandle ich die Collection dann in ein Array um, um die Ausgabe nicht zeilenweise über Foreach 1000-nde Male durchführen zu müssen, sondern die Ausgabedatei nur einmalig öffnen und mit dem ganzen Array beschreiben zu müssen.

Die Erklärungen insbesondere zur LDAP-Query und der Elemente von [ADSISearcher] folgen in den folgenden Kapiteln.

Beispiel 2: Auslesen von Gruppenmitgliedern

Clear-Host
Set-Strictmode -Version "2.0"

$Searchroot = "DC=domain1,dc=net"
$GroupName = "administrators"

$Filter =@()
$Filter = "(&(objectcategory=group))"
$Filter += "(name=$GroupName)"
[string]$Filter = "(&$Filter)"
 
$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Searchroot = "LDAP://$Searchroot"
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.Searchscope = "SubTree"

$DirectorySearcher.Findall()  | Foreach{  
   $($_.Properties).distinguishedname
   ""
   $($_.Properties).member
   }

Genauer (verkettete Mitgliedschaften) gehe ich im Kapitel 4.2.3.4.2 Suche in allen VorgängerObjekten auf das Auslesen von Gruppenmitgliedern ein


2 LDAP- und GlobalCatalog Queries

Queries können zu einem Domaincontreoller, sofern dieser die zusätzliche Rolle "GlobalCatalog" besitzt, an die beidenPorts 389 (LDAP) und 3268 (GlobalCatalog) gesendet werden.

Queries gegen Port 389 werden nur mit Informationen aus der eigenen Domäne des Domaincontrollers beantwortet.
Mit Queries gegen den GlobalCatalog-Port kann man dagegen Informationen zum gesamten Forest erhalten, sofern 
 - der angefragte Domaincontroller gleichzeitig Globalcatalogserver ist
 - die gesuchte Property die Eigenschaft "IsInGlobalCatalog = True" besitzt
 - die Abfrage unter einem Useraccount des Forests ausgeführt wird. Jeder ForestUser kann lesend alle im GC gespeicherten forestweiten AD-Daten wie den Samaccountname abfragen.

Ob das Attribut einer Klasse im GlobalCatalog default enthalten ist, kann man in der MSDN nachsehen:

MSDN: User Class

beipielsweise das Account-Expires Attribute. Man muss nur auf die richtige WindowsVersion in der Tabelle achten (2000/ 2003/ ....)



Die Eigenschaft "IsInGlobalCatalog" kann bewusst durch einen Administrator oder unbewusst durch Schemaupdates verändert worden sein. Den tatsächlichen Status des Attributes im eigenen Schema kann man mit Hilfe .Net bestimmen:

Beispiel 1: Bestimmen, ob ein Attribut im GlobalCatalog veröffentlicht ist
Set-StrictMode -Version "2.0"
Clear-Host
 
$Property = "AccountExpires"
$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$CurrentSchema.FindProperty($Property)

#$($CurrentSchema.FindProperty($Property)).IsInGlobalCatalog
#mögliche Ausgabe gekürzt

IsOnTombstonedObject   : False
IsTupleIndexed         : False
IsInGlobalCatalog      : False
RangeLower             :
RangeUpper             :


Beispiel 2a: Auflisten aller Objekte einer OU (ohne Rekursion)
Clear-Host
Set-StrictMode -Version "2.0"

$Path = "Ou=User,Ou=Scripting, Dc=Dom1, Dc=Intern"
$Server = "GCDC01.Dom1.intern"

#$OU=([ADSI]"LDAP://$Path")
$OU=([ADSI]"GC://$Server/$Path")
$ChildObjects = $OU.Children | ForEach {$_.Name}

$ChildObjects | Format-Table * | Select -First 3
$ChildObjects.Count
#mögliche Ausgabe

Karl14000
Karl14001
Karl14002
202

Bei dieser Suche handelt es sich um eine einfache LDAP-Query auf Port 389. Der LDAP-Client sucht sich per DNS einen beliebigen DC der Domäne und schickt diesem sein Query. Möchte man die Anfrage von einem GlobalCatalogServer (DomainController) desselben Forests, aber einer anderen Domäne beantwortet haben, so ist statt dem "LDAP-Provider", der GC-Provider zu benutzen. Anstelle des Wortes Provider wird oft auch die Bezeichnung Moniker verwednet. Der DomainController muss dazu die Eigenschaft "IsGlobalCatalog" besitzen und die abgefragten Objekte müssen im GlobalCatalog published sein.

Für die Children-Eigenschaft gibt es keine rekursiv-Option!

 

Beispiel 2b: Suchen ab einem Eintiegspunkt nach einem Namensbestandteil (mit Rekursion)
Dieses Beispiel nicht für den praktischen Einsatz gedacht (siehe Anmerkung unten)

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  $SearchRoot="OU=test,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
  $SearchErgebnis = Find-EntriesInSubOUs($SearchRoot)
  $SearchErgebnis | ForEach {Write-Host -foregroundcolor red $_}
}

Function Find-EntriesInSubOUs($SearchRoot){
   #Rekursiv die Domäne durchsuchen. Einstiegspunkt der Suche ist $Searchroot
     $Filter = "Munich"
     $AdObject = [ADSI]"LDAP://$SearchRoot"
        if($Searchroot -like "*$Filter*"){
           Write-Host $Searchroot          }
     $AdObject.Children | ForEach { 
       Find-EntriesInSubOUs($_.DistinguishedName)  #Recursion
       }
}

Main
#mögliche Ausgabe

OU=test,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern
OU=bla,OU=test,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern
CN=Munich_Karl_1000004,OU=bla,OU=test,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern
CN=Munich_Karl_1000001,OU=test,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern

Das Beispiel liefert alle Objekte im ADBaum zurück, die den Filter "Munich" im CN enthalten.
Im Normalfall benutzt man für eine Suche im AD andere Mechanismen, wie die .Net Klasse [ADSISearcher], da dort die Suche genauer spezifiziert werden kann. So ist es dort möglich, beispielsweise nur nach Computern zu suchen. Außerdem ist die Suche mit ADSISearcher sehr viel performanter und resourcenschonender, da nicht zu jedem Ergebnis eine neue LDAP-Connection aufgebaut werden braucht.

 

Beispiel 3a: Unterverzeichnisse eines LDAP-Pfades auflisten

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
   Get-PKIFolder
}

Function Get-PKIFolder{
  $RootDomainNamingContext = ([ADSI]"LDAP://rootDSE").rootDomainNamingContext
  $PKIPath = "CN=Public Key Services,CN=Services,CN=Configuration,$RootDomainNamingContext"

  $PKiServices = ([ADSI]"GC://$PKIPath")
  $ChildPKIServices = $PKIServices.Children | ForEach {$_.distinguishedname}
  Return $ChildPKIServices
}

Main

#mögliche Ausgabe

CN=AIA,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=KRA,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern
CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,DC=dom1,DC=intern


Beispiel 3a: Unterverzeichnisse eines LDAP-Pfades auflisten (DirectorySearcher)

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
   Get-PKIFolder
}

Function Get-PKIFolder{
  $RootDomainNamingContext = ([ADSI]"LDAP://rootDSE").rootDomainNamingContext
  $PKIPath = "CN=Public Key Services,CN=Services,CN=Configuration,$RootDomainNamingContext"

  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.Searchroot = "LDAP://$PKIPath"
  $DirectorySearcher.Searchscope = "OneLevel"
  $DirectorySearcher.Filter = "(objectCategory=*)" 
  $DirectorySearcher.FindAll() | Foreach{ $_.properties.distinguishedname}
}

Main    

 

3 Schema - Queries

3.1 Einleitung

Das ActiveDirectory speichert seine Klassendefinitionen im ActiveDirectory-Schema. Davon gibt unter  Windows 2012 mittlerweile 258 Klassen. Beispiele sind die Computer-, User oder Gruppenklasse.

Eine Aufstellung aller Klassen liefert: MSDN: All Classes

In den verzweigenden Sublinks zu jeder Klasse wird auf dieser Seite jede Klasse in Abhängigkeit vom verwendeten Betriebssystem (Win2000 bis Win2012) detailliert beschrieben. Microsoft aktualisiert diese Seite bei jedem neuen Serverbetriebssystem.

In diesem Kapitel 4 werde ich vorwiegend den LDAP-Provider "[ADSI]"LDAP://" mit der dahinterliegenden .Net-Klasse "DirectoryEntry" nutzen. 

 

3.2 Mit der Powershell ins AD-Schema blicken

An dieser Stelle ein paar Beispiele, die ich einerseits in den Skripten der späteren Kapitel benötige, die andererseits beim Verständnis des ADSchemas und seiner Klassen helfen können. Um auf das ADSchema zugreifen zu können, bietet .Net die Klasse "ActiveDirectorySchema" an.

MSDN: ActiveDirectorySchema Class


Mit der statischen Methode "GetCurrentSchema()" verschafft man sich den Zugriff auf das aktuelle Schema.

MSDN: ActiveDirectorySchema.GetCurrentSchema Method

Beispiel 1a: Anzeigen aller Klassen des ADSchemas

Set-StrictMode -Version "2.0"
Clear-Host

$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()

$CurrentSchema.FindallClasses() | Select Name,CommonName -First 3
$CurrentSchema.FindallClasses().Count
#Ausgabe WindowsServer2012
 
Name                  CommonName          
----                  ----------          
organization          Organization        
nTDSDSA               NTDS-DSA            
dMD                   DMD                 
256

Gegen einen W2k8R2-Domaincontroller erhält man "nur" 234 Klassen

MSDN: ActiveDirectorySchema.FindAllClasses Method

Beispiel 1b: Vergleich des ActiveDirectorySchema auf einem W2k8R2 und einem Windows2012 Domaincontroller

Set-StrictMode -Version "2.0"
Clear-Host

$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()

$CurrentSchema.FindallProperties().Count
$CurrentSchema.FindallProperties('Indexed').Count
$CurrentSchema.FindallProperties('InGlobalCatalog').Count
#Ausgabe in einer W2k8R2-Domäne

1314
115
164
#Ausgabe in einer Windows2012-Domäne 

1426
142
186

Die Überraschung hält sich wahrscheinlich in Grenzen, dass mit Server2012 wieder einige neue Eigenschaften im ADSchema dazugekommen sind.

MSDN: ActiveDirectorySchema.FindAllProperties Method

 

Beispiel 2: Anzeigen aller Eigenschaften der User-Klasse und ob diese "SingleValued","Indexed" und "InGlobalCatalog" sind

Set-StrictMode -Version "2.0"
Clear-Host
 
$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$SchemaUser = $CurrentSchema.FindClass('User')
$SchemaUser.GetAllProperties() | Format-Table Name,IsSingleValued,IsIndexed,IsInGlobalCatalog -auto
#mögliche Ausgabe

Name                         IsSingleValued IsIndexed         IsInGlobalCatalog
----                         -------------- ---------         -----------------
...
objectClass                  False          True              True
objectSid                    True           True              True
sAMAccountName               True           True              True
accountExpires               True           False             False
accountNameHistory           False          False             False

MSDN: ActiveDirectorySchema.FindClass Method

MSDN: ActiveDirectorySchemaClass.GetAllProperties Method

Beispiel 3: indizierte und im GC veröffentlichte Eigenschaften anzeigen

Set-StrictMode -Version "2.0"
Clear-Host
 
$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$CurrentSchema.FindallProperties('Indexed') | Format-Table Name,IsIndexed -auto
#$CurrentSchema.FindAllProperties('InGlobalCatalog') | Format-Table Name,IsInGlobalCatalog -auto
#Ausgabe gekürzt

Name                              IsIndexed
----                              ---------
altSecurityIdentities                  True
birthLocation                          True
cOMClassID                             True
...

MSDN: PropertyTypes-Enumeration

MSDN: ActiveDirectorySchema.FindAllProperties Method

Welche Eigenschaften einer Schemaeingenschaft neben 'Indexed' und 'InglobalCatalog' noch abgefragt werden können, findet ihr in Beispiel 5 weiter unten

Beispiel 4: Abfragen einer Klasseneigenschaft ("IsSingleValued)

Set-StrictMode -Version "2.0"
Clear-Host

$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$CurrentSchema.FindProperty("Mail").IsSingleValued
$CurrentSchema.FindProperty("OtherMailBox").IsSingleValued
#Ausgabe

True
False

MSDN: ActiveDirectorySchema.FindProperty Method


Beispiel 5: Anzeige der ActiveDirectorySchemaProperty-Eigenschaften

Set-StrictMode -Version "2.0"
Clear-Host

$CurrentSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$CurrentSchema.FindProperty("Cn")
#Ausgabe

Name                   : cn
CommonName             : Common-Name
Oid                    : 2.5.4.3
Syntax                 : DirectoryString
Description            :
IsSingleValued         : True
IsIndexed              : True
IsIndexedOverContainer : False
IsInAnr                : False
IsOnTombstonedObject   : False
IsTupleIndexed         : False
IsInGlobalCatalog      : True
RangeLower             : 1
RangeUpper             : 64
IsDefunct              : False
Link                   :
LinkId                 :
SchemaGuid             : bf96793f-0de6-11d0-a285-00aa003049e2

Die Bedeutung der einzelnen Eigenschaften ist hier näher beschrieben:

MSDN: ActiveDirectorySchemaProperty-Eigenschaften

MSDN: ActiveDirectorySchema.FindProperty Method

 

4 komplexere Queries mit [ADSISearcher] DirectorySearcher

[ADSISearcher] oder gleichbedeutend "System.Directoryservices.Directorysearcher" bietet eine Vielzahl an Methoden und Eigenschaften an, um LDAP-Queries ganz genau auf seine Anforderungen hinzu designen.
Allerdings erfordert die durch diese Vielzahl erreichte Flexibilität sowohl ein gewisses Maß an Verständnis der Materie wie auch sorgfätige Tests.
Ich habe mir dazu in einem TestAD in einer OU 35.000 User und in einer anderen OU 50 User angelegt, um hiermit spielen zu können. siehe 4.1.1 Anlegen von Usern

 

4.1 Methoden von [ADSISearcher]

MSDN: DirectorySearcher-Methoden Tabelle aller Methoden 


Die beiden wichtigsten Methoden dieser Tabelle sind Findall() und Findone(), die bereits in den Einführungsbeispielen intensiv in Benutzugn sind

Findall()

Führt die Suche aus und gibt eine Auflistung der gefundenen Einträge zurück.

FindOne() 

Führt die Suche aus und gibt nur den ersten gefundenen Eintrag zurück.


Beispiel 1: Die Methode FindAll()
Die Methode Findall() liefert eine Collection aller gefilterten Objekte zurück. Auf jedes Element der Collection kann über einen Index zugegriffen werden. $DirectorySearcher.Findall()[0] ist das erste Element.

Clear-Host
Set-Strictmode -Version "2.0"

$Filter = "(Name=Munich_Karl_100000*)" #keine Leerzeichen im Kriterium!

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "($Filter)"

Write-Host "Example 1"
$DirectorySearcher.FindAll() | Select Path -First 2 #findet alle Namen mit dem Suchkriterium

Write-Host "`nExample 2"
$DirectorySearcher.Findall() | Foreach{$($_.Properties).samaccountname } | Select -First 2

Write-Host "`nExample 3"

$DirectorySearcher.FindAll()[0] | Select Path  #liefert das erste Objekt der Collection
$DirectorySearcher.FindAll()[0].Path #identisch

Write-Host "`nExample 4"

$DirectorySearcher.FindAll()[0].Properties.item("Samaccountname")
$DirectorySearcher.FindAll()[0].Properties.samaccountname
#mögliche Ausgabe

Example 1

Path    
----     
LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern 
LDAP://CN=Munich_Karl_1000001,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern

Example 2
Munich_Karl_1000000
Munich_Karl_1000001

Example 2
LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern 
LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern


Example 4
Munich_Karl_1000000
Munich_Karl_1000000



Beispiel 2: Die Methode FindOne()

Clear-Host
Set-Strictmode -Version "2.0"

$Filter = "(Name=Munich_Karl_100000*)" #keine Leerzeichen im Kriterium!

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "($Filter)"

Write-Host "Example 1"

$DirectorySearcher.FindOne() | Select Path #beendet die Suche nach dem ersten Treffer
$DirectorySearcher.FindOne().Path #identisch

Write-Host "`nExample 2"
$DirectorySearcher.FindOne().Properties.samaccountname
$DirectorySearcher.FindOne().Properties.Item("SamAccountName") #identisch
#mögliche Ausgabe

Example 1
Path
----                     
LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern  
LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern


Example 2
Munich_Karl_1000000
Munich_Karl_1000000

Die Methode Findone() ist dann geeignet, wenn die Filterbedingungen so gewählt sind, dass nur ein Ergebnis zurückkommen kann. Wenn nach dem ersten Treffer mit keinen weiteren Treffern zu rechnen ist, würde eine weitere Suche wie bei Findall() nur unnötig Zeit kosten.
Ausserdem ist Findone() während der Scriptentwicklung hilfreich, wenn mit Findall() sehr viele Objekte gefunden werden würden, deren Ausgabe bei jedem Testlauf Zeit verbraucht. Hat man Filter und Script fertig, so kann man einfach aus einem Findone() ein Findall() machen.

Beispiel 3: Welche Properties kann man direkt aus einem mit Findall() oder FindOne() gefundenen Objekt lesen?

Clear-Host
Set-Strictmode -Version "2.0"

$Filter = "(Name=Munich_Karl_100000*)" #keine Leerzeichen im Kriterium!

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "($Filter)"

#Eigenschaften eines Userobjects
$DirectorySearcher.FindOne().Properties
$DirectorySearcher.FindOne().Properties.samaccountname
#mögliche Ausgabe

Name                           Value    
----                           -----    
logoncount                     {0}      
codepage                       {0}      
objectcategory                 {CN=Person,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern}    
description                    {created by the children.add-method}                        
usnchanged                     {1130615}
instancetype                   {4}      
name                           {Munich_Karl_1000000}                                       
badpasswordtime                {0}      
pwdlastset                     {130288392810375719}                                        
objectclass                    {top, person, organizationalPerson, user}                   
badpwdcount                    {0}      
samaccounttype                 {805306368}                                                 
usncreated                     {38433}  
sn                             {Munich_Karl_1000000}                                       
userparameters                 {CtxCfgPresent                     }
objectguid                     {196 36 43 40 147 188 85 76 181 119 140 29 211 232 147 98}  
memberof                       {CN=GG-Bangkok001,OU=Global,OU=Groups,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern}         
whencreated                    {13.11.2013 18:01:20}                                       
adspath                        {LDAP://CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern}  
useraccountcontrol             {544}    
cn                             {Munich_Karl_1000000}                                       
countrycode                    {0}      
primarygroupid                 {513}    
whenchanged                    {27.02.2014 22:55:12}                                       
dscorepropagationdata          {19.02.2014 07:35:50, 13.11.2013 20:56:33, 01.01.1601 00:00:00}   
lastlogon                      {0}      
distinguishedname              {CN=Munich_Karl_1000000,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern}   
samaccountname                 {Munich_Karl_1000000}                                       
objectsid                      {1 5 0 0 0 0 0 5 21 0 0 0 170 68 82 201 94 16 0 0}
lastlogoff                     {0}      
accountexpires                 {129159288000000000}                                        
userprincipalname              {Munich_Karl_1000000@Dom1.Intern}                           

 

Zum wiederholten Male: Bitte beachtet, dass die Eigenschaften im Skript komplett in Kleinbuchstaben geschrieben werden müssen!

Beispiel 4: Die Eigenschaften accountexpires und pwdlastset

Clear-Host
Set-Strictmode -Version "2.0"

$Filter = "(Name=Munich_Karl_100000*)" #keine Leerzeichen im Kriterium!

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "($Filter)"

#Eigenschaften eines Userobjects
$User = $DirectorySearcher.FindOne()
$SamAccountName = $User.Properties.samaccountname
$pwdlastset = [DateTime]::FromFileTime($($User.Properties.pwdlastset))
$accountexpires = [DateTime]::FromFileTime($($User.Properties.pwdlastset))

#Ausgabe
$SamAccountName
$pwdlastset.ToString("d")
$accountexpires.ToString("s")
$accountexpires.ToString("yyyy-MM-dd")
#mögliche Ausgabe

Munich_Karl_1000000
13.11.2013
2013-11-13T19:01:21
2013-11-13

Da Datumsausgaben oft noch sortiert werden, sind die ISO-konformen Datumsformate recht nützlich

MSDN: Standard DateTime Format Strings


Beispiel 5:  Die Properties CanonicalName und IsDisabled

Clear-Host
Set-Strictmode -Version "2.0"

Function Main {
  $Filter = "(Name=Munich_Karl_100000*)" #keine Leerzeichen im Kriterium!

  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.Filter = "($Filter)"
  $DirectorySearcher.SearchScope = "subtree"

  $User = $DirectorySearcher.FindOne()  
 
  #direkt aus dem AD gelesene Properties  
  $SamAccountName = $($User.Properties).samaccountname
  $whenCreated = $($User.Properties).whencreated
  $distinguishedName = $($User.Properties).distinguishedname
    
  #Functionsaufrufe, da nicht im ActiveDirectory vorhanden
  $IsDisabled = Isdisabled $User
  $Canonicalname = Get-Canonicalname $distinguishedname
        
   "{0};{1:d};{2};{3};{4}" -f $($SamAccountName),$($whenCreated),$IsDisabled,$($distinguishedName),$Canonicalname
} #end function main

Function IsDisabled{
  Param($User)
  $UAC = $($User.Properties.item('userAccountControl'))
    if(($UAC -BAND 2) -eq 0){
      Return "False" }
    Else{ Return "True" }
 }# end function IsDisabled

Function GET-CanonicalName {
#get canonicalname http://gallery.technet.microsoft.com/scriptcenter/04e4e149-519a-4834-9626-02275de57ea6
param([string]$DN)
$CN=""

# Split the Distinguished name into separate bits
#
$Parts=$DN.Split(",")
 
# Figure out how deep the Rabbit Hold goes
#
$NumParts=$Parts.Count
 
# Although typically 2 DC entries, make sure and figure out the length of the FQDN
#
$FQDNPieces=($Parts -match 'DC').Count
 
# Keep track of where the FQDN is (calling it the middle even if it
# Could be WAY out there somewhere
#
$Middle=$NumParts-$FQDNPieces
 
# Build the CN.  First part is separated by '.'
#  
foreach ($x in ($Middle+1)..($NumParts)) {
    $CN+=$Parts[$x-1].SubString(3)+'.'
    }
 
# Get rid of that extra Dot
#
$CN=$CN.substring(0,($CN.length)-1)
 
# Now go BACKWARDS and build the rest of the CN
#
foreach ($x in ($Middle-1)..0) {  
    #$Parts[$x].substring(3)
    $CN+="/"+$Parts[$x].SubString(3)
    }
 
Return $CN
}

Main

Reichen die in Beispiel 3 gezeigten Properties nicht aus, baut man sich für jede dieser Eigenschaften eine Function. Die Function zur Umwandlung des DistinguishedNames in einen Canonicalname habe ich in der Technet gefunden:

 

4.2 Eigenschaften von [ADSISearcher]

Mit allen in diesem Kapitel behandelten Eigenschaften sollte man sich beschäftigen und überlegen, ob und wie man diese für eine Query einsetzt. Die Auswirkungen jeder Eigenschaft auf die zurückgegebenen Ergebnisse und/ oder auf den Resourcenbedarf der Suche können gravierend sein!

MSDN: DirectorySearcher-Eigenschaften

 

Kapitel

Name

Beschreibung

4.2.1

Asynchronous

Ruft einen Wert ab, der angibt, ob die Suche asynchron ausgeführt wird, oder legt diesen fest.

4.2.2

CacheResults

Ruft einen Wert ab, der angibt, ob das Ergebnis im Cache des Clientcomputers gespeichert wird, oder legt diesen fest.

4.2.3

Filter

Ruft einen Wert ab, der das Format der Filterzeichenfolge für LDAP (Lightweight Directory Access Protocol) angibt, oder legt diesen fest.

4.2.2.4

PageSize

Ruft einen Wert ab, der die Seitengröße für eine ausgelagerte Suche angibt, oder legt diesen fest.

4.2.5

SearchRoot

Ruft einen Wert ab, der den Knoten in der Active Directory-Domänendienste-Hierarchie angibt, bei dem die Suche beginnt, oder legt diesen fest.

4.2.6

SearchScope

Ruft einen Wert für den vom Server überwachten Suchbereich ab oder legt diesen fest.

4.2.7

Tombstone

Ruft einen Wert ab, der angibt, ob bei der Suche auch gelöschte Objekte, die mit den Suchfilterkriterien übereinstimmen, zurückgegeben werden sollen, oder legt diesen Wert fest.

4.2.8. PropertiesToLoad Beschränkt die Ergebnisse auf die in der Liste aufgeführten Eigenschaften.

Von den im obigen Link angegeben Eigenschaften von DirectorySearcher möchte ich auf die in der Tabelle genannten  sieben Eigenschaften etwas näher eingehen:

 

4.2.1 Eigenschaft Asynchronous

Ruft einen Wert ab, der angibt, ob die Suche asynchron ausgeführt wird, oder legt diesen fest.

Default: $False

MSDN: DirectorySearcher.Asynchronous-Eigenschaft

http://msdn.microsoft.com/de-de/library/system.directoryservices.directorysearcher.asynchronous.aspx


Beispiel 1: Asynchrone Suche nach einem Useraccount

Set-StrictMode -Version "2.0"
Clear-Host

$Filter = "Name=Munich_Karl_1*"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.Asynchronous = $False
$DirectorySearcher.Findall() | Foreach {
$($_.Properties).samaccountname}

Wird die Eigenschaft "Asynchronous" = $true gesetzt, so erfolgt die Suche asynchron, andernfalls synchron.

Bei grossen Suchen mit vielen erwarteten Treffern bringt eine asynchrone Suche einige Vorteile.

  • Das Skript wartet nicht darauf, bis die vollständige Suche durchgelaufen ist, sondern gibt immer wieder Teilmengen von Ergebnissen zurück
  • eine asynchrone Suche blockiert andere Threads nicht so lange wie eine synchrone Suche. Man kann so beispielsweise schneller wieder auf die powershell_ise zugreifen und zum Entwickeln benutzen, während im Hintergrund die Suche noch weiterläuft
  • Skripte im asynchronen Modus laufen eventuell stabiler
  • - Für den Domaincontroller gibt es dagegen keinen Unterschied, ob ein Client eine synchrone oder asynchrone Abfrage gegen ihn ausführt. Seine Performancebelastung bleibt gleich.
     

4.2.2 Eigenschaft CachedResults

Ruft einen Wert ab, der angibt, ob das Ergebnis im Cache des Clientcomputers gespeichert wird, oder legt diesen fest.

Default: $True

MSDN: DirectorySearcher.CacheResults Property

 

Beispiel 1: Eigenschaft CachedResults

Set-StrictMode -Version "2.0"
Clear-Host

$Filter = "Name=Munich_Karl_1*"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.CacheResults = $False
$DirectorySearcher.Findall()  | Select Path

CachedResults beeinflusst das Verhalten des ClientCaches bei vielen Suchergebnissen. Bei aktiviertem (=default) Caching werden die Ergebnisse in den Clientcache geladen und können dort mehrfach verwendet werden, ohne dass eine erneute Suche erforderlich ist. Auf der anderen Seite vermindert dieser Ladevorgang die Performance der Suche. (so lautet etwa die Erklärung der MSDN im Link oben)

In anderen Programmierbüchern, wie Amazon: The Net Developer's Guide to Directory Services Programming (Microsoft .Net Development) S.146 wird deutlich davon abgeraten, das Caching zu aktivieren. Es sei schwierig die Eigenschaft richtig zu benützen und Caching sei wenn überhaupt nur in foreach-Schleifen wirklich sinnvoll.
 

4.2.3 Eigenschaft Filter

MSDN: Search Filter Syntax

Filter ist die entscheidende Eigenschaft des DirectorySearchers, um aus einem ActiveDirectory die gewünschten Informationen herauszuholen. Im Idealfall kann der Suchfilter so definiert werden, dass man genau diejenigen Objekte zurückerhält, die man weiter auswerten oder bearbeiten möchte. Nicht wesentlich mehr Objekte als nötig und auf gar keinen Fall weniger!
Auf der angegebenen MSDN-Seite ist die LDAP-Syntax des RFC 2254 näher beschrieben.

Wer sich tiefer mit Themen wie LDAPSearches oder Schemaabfragen auseinandersetzen möchte, dem kann ich als Ergänzung dieses Buch empfehlen:

"Active Directory Forestry, Investigating and Managing Objects and Attributes for Windows 2000 and Windows Server 2003"
Das Buch kommt zwar in der Aufmachung wie ein dünnes Kinderbuch daher, hat aber auf seinen gut 180 Seiten etliche schwere Geschütze zum Thema ActiveDirectory Schema und LDAP Searches eingepackt!

 

4.2.3.1 Hilfsmittel zum Filterbau

Ein hilfreiches Werkzeug zur Erstellung von Filtern ist im MMC-SnapIn "Active Directory Benutzer und Computer" (dsa.msc) unter "Gespeicherte Abfragen" oder "Saved Queries" im linken Objektbaum der MMC enthalten:

Auf "Saved Queries" mit der rechten Maustaste klicken,

  • einen beliebigen Namen für die Suche eingeben
  • einen Einstiegspunkt der Suche auswählen
  • Suche Definieren klicken
  • entweder "allgemeine Suche" für einen einfacheren Filter auswählen, oder "benutzerdefinierte Suche" für komplexere Filter.

Mit "benutzerdefinierte Suche" -> "Erweitert" können selbst erstellte Filter geprüft werden.

Im folgenden Technetlink ist Schritt für Schritt beschrieben, wie man gesperrte Useraccounts mit Hilfe dieses SnapIn findet:

TechnetBeispiel: Enumerate locked out user accounts using Saved Queries

Ein anderes mächtiges Tool, auf das in dem oben erwähnten Buch von John Craddock ausführlich eingegangen wird, ist ldp.exe. (allerdings noch für W2k3 und XP)
 

4.2.3.2 FilterAttribute ObjectCategory und ObjectClass

Beide Attribute lassen sich in einem LDAP-Filter verwenden, um eine LDAP-Suche auf bestimmte Klassen (User, Computer, Kontakte) zu konzentrieren

Beispiel 1: ObjectCategory vs. ObjectClass

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$$DirectorySearcher.Filter = "(&(ObjectCategory=User)(Name=Munich_Karl_1*))"
#oder
$DirectorySearcher.Filter = "(&(ObjectClass=User)(Name=Munich_Karl_1*))"

Die MSDN gibt zwei Gründe an, warum zumindest bei grossen AD Datenbanken aus Performancegründen die Objectcategory als Filter besser geeignet ist, als ObjectClass.

a) ObjectCategory ist im Gegensatz zu ObjectClass ein indiziertes Attribut der Datenbank
b) ObjectCategory ist ein singleValue Attribute, ObjectClass ein multiValue Attribute

MSDN: What Makes a Fast Query?

MSDN: objectCategory vs. objectClass

Leider ist das noch nicht alles, was es zu objectClass und ObjectCategory zu sagen gibt.
Ein Filter mit (objectCategory=User) wie

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$$DirectorySearcher.Filter = "(&(Objectcategory=User)(Name=Munich_Karl_1*))"

gibt nicht nur Userobjekte, sondern auch Objekte der Klassen contact, organizationalunit, person und inetorgPerson zurück, da diese Klassen im Attribut objectCategory ebenfall "user" stehen haben.

und andererseits gibt ein Filter

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$$DirectorySearcher.Filter="(&(objectClass=User)(Name=Munich_Karl_1*))"

mehr oder weniger überraschenderweise neben den Userobjekten auch Computerobjekte zurück, da die Klasse Computer von der Klasse User abgeleitet ist.

Nach langer Rede nun das kurze Ergebnis für einen LDAP-Filter der performant ausschliesslich Userobjekte mit dem Namensbestandteil *Napf* zurückliefert:


Beispiel 2: gemeinsamer Einsatz von objectClass und objectCategory

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "(&(objectClass=User)(objectCategory=User)(Name=Munich_Karl_1*))"


4.2.3.3 Resourcenverbrauch und Perfomance von Filtern

Über den Einfluss von Filtern auf den Resourcenbedarf insbesondere von Domaincontrollern muss man sich unbedingt Gedanken machen, wenn die LDAP-Abfragen in Logonskripten verwendet werden sollen.
Ungünstige Filter, nahezu gleichzeitig von mehreren 100 bis 1000 Usern während der Hauptanmeldezeit gegen einen Domaincontroller abgeschossen, können die Prozessorlast auf 100% hochtreiben und damit einen DC lahmlegen. Ich habe selbst den Fall erlebt, bei dem Entwickler jeden User bei der Anmeldung suchen liessen, ob er in Groupname=*xyz* Mitglied ist. Das Programm lief durch alle Test- und Abnahmeumgebungen problemlos durch, in der Produktion standen beim Rollout des ersten Piloten in der Früh um 8 Uhr alle DCs luftschnappend bei 100% Prozessorlast.

Abhilfe 1: Über einen RegistryKey lassen sich "teure" und "uneffektive" LDAPSearches auf einem DC aufspüren. 

MSDN: Creating More Efficient Microsoft Active Directory-Enabled Applications

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics\15 Field Engineering

Diesen Key auf 5 setzen. Danach werden LDAPSearches gelogged, die folgende Bedingungen erfüllen:

Using the default values, a search is considered expensive if it visits more than 10,000 entries. A search is considered inefficient if the search visits more than 1,000 entries and the returned entries are less than 10 percent of the entries that it visited.

In beiden Fällen wird der Event 1644 im Directory Service Log geschrieben. Simulieren lässt sich der Fall, in dem man einen LDAPFilter mit "Filter=*xyz*" abschiesst und im AD über 10.000 Objekte angelegt sind

Damit wäre bei einem Probelauf eines einzelnen Users die mangelhafte Qualität des Filters im obigen Beispiel aufgefallen.

Abhilfe 2: Man kann sich während des Ausführens der Suche vom Server direkt Statistiken über ein sogenanntes LDAPControl zurückgeben lassen:

MSDN: 1.2.840.113556.1.4.970 LDAP_SERVER_GET_STATS_OID

ein ausformuliertes, dokumentiertes Beispiel findet man auf diesem Blog:

bsonposh.com: Working with LDAP Stats Control in Powershell

4.2.3.4 LDAPControls

Es gibt Fälle, bei denen die normale Filtersyntax entweder nicht oder nur recht aufwändig zum Ziel führen würde. Besonders bei Objekteigenschaften, die im AD hexadezimal hinterlegt sind (Usereigenschaften wie gesperrt, Password abgelaufen oder Gruppentypen wie global und lokal) kann die Verwendung von LDAPControls schnell zum Ziel führen.
 

4.2.3.4.1 bitweiser AND/ OR Vergleich

OID

String identifier (from NTLDAP.H)

Description

1.2.840.113556.1.4.803

LDAP_MATCHING_RULE_BIT_AND

A match is found only if all bits from the attribute match the value. This rule is equivalent to a bitwise AND operator.

1.2.840.113556.1.4.804

LDAP_MATCHING_RULE_BIT_OR

A match is found if any bits from the attribute match the value. This rule is equivalent to a bitwise OR operator.


Beispiel 1: Suche nach einem Gruppentyp mit bitweisem Vergleich / Anzahl der Gruppenmitglieder bestimmen

MSDN: ADS_GROUP_TYPE_ENUM Enumeration

Clear-Host
Set-Strictmode -Version "2.0"

$Path = "LDAP://OU=Global,OU=Groups,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.SearchRoot = $Path

$ADS_GROUP_TYPE_GLOBAL_GROUP = 0x00000002
$ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP = 0x00000004
$ADS_GROUP_TYPE_LOCAL_GROUP = 0x00000004
$ADS_GROUP_TYPE_UNIVERSAL_GROUP = 0x00000008
$ADS_GROUP_TYPE_SECURITY_ENABLED = 0x80000000

$DirectorySearcher.Filter="(&(ObjectCategory=Group)(GroupType:1.2.840.113556.1.4.803:=$ADS_GROUP_TYPE_SECURITY_ENABLED))"
$DirectorySearcher.Filter="(&(objectCategory=group)(GroupType:1.2.840.113556.1.4.803:=$(0x80000000)))"
$DirectorySearcher.Filter="(&(ObjectCategory=Group)(GroupType:1.2.840.113556.1.4.803:=2147483648))"

$DirectorySearcher.FindAll() | Foreach{
  $GroupName = $($_.Properties).Item("CN")
  $Members = $($_.Properties).Item("Member").Count
  "$GroupName $Members"
  }
#mögliche Ausgabe

GG-Bangkok001 3
GG-Bangkok002 2
GG-Bangkok003 5

0x80000000 entspricht dem dezimalen Wert von 2147483648. Das praktische an der hexadezimalen Schreibweise ist auch hier, dass sich durch Addition die Gruppentypen einfach kombinieren lassen. Also 0x80000004 gilt für alle lokalen Securitygruppen oder in weniger klarer Form 2147483652 gleichbedeutend in dezimaler Schreibweise.

Auf die Eigenschaft "Searchroot" gehe ich weiter unten in Kapitel 4.2.5 etwas näher ein

Das LDAPControl 803 kann man weiterhin einsetzen, wenn man das AD genauer untersuchen möchte, und zum Beispiel.
- alle zum GC replizierten Attribute
- alle indizierten Attribute
- alle nachträglich zum ADSchema hinzugefügten Objekte (Category 2)
auflisten will

MSDN: System-Flags Attribute


Beispiel 2: Aufzählen aller Category 1 oder Category 2 Objekte des Schemas

Set-StrictMode -Version "2.0"
Clear-Host

$DomainDN = ([ADSI]"LDAP://rootDSE").DefaultNamingContext

$DirectorySearcher = ([ADSISearcher]"LDAP://")
#$DirectorySearcher.Filter = "(!(systemFlags:1.2.840.113556.1.4.803:=16))" #category 2
$DirectorySearcher.Filter = "(systemFlags:1.2.840.113556.1.4.803:=16)" #category 1
$DirectorySearcher.SearchRoot = "LDAP://Cn=Schema,Cn=Configuration,$DomainDN"
$DirectorySearcher.PageSize  = 1000

$DirectorySearcher.FindAll().Path | Select -First 3
#mögliche Ausgabe

LDAP://CN=FRS-Extensions,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern
LDAP://CN=ms-DS-Password-Settings-Precedence,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern
LDAP://CN=ms-WMI-TargetNameSpace,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern

Anmerkung 2: Anstelle dieser beiden etwas länglichen LDAPControls kann man auch mit den Booleschen Operatoren -band und -bor arbeiten. siehe Kapitel 3.3Anmerkung 1: Im Netz finden sich viele interessante Beispiele und Sammlungen von Filtern, die dieses LDAPControls nutzen. Bingt oder Googelt einfach nach "1.2.840.113556.1.4.803:="

 

4.2.3.4.2 Suche in allen VorgängerObjekten

OID

String identifier (from NTLDAP.H)

Description

1.2.840.113556.1.4.1941

LDAP_MATCHING_RULE_IN_CHAIN

This rule is limited to filters that apply to the DN. This is a special "extended match operator that walks the chain of ancestry in objects all the way ro the root until it finds a match.

 

Beispiel 1: Suchen in einer Objektkette, Mitglieder einer Gruppe auslesen

Clear-Host
Set-Strictmode -Version "2.0"

$GroupDN = "CN=GG-Bangkok002,OU=Global,OU=Groups,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "(memberof:1.2.840.113556.1.4.1941:=$GroupDN)"
$DirectorySearcher.Pagesize = 1000
$DirectorySearcher.Findall() | Foreach{
  $($_.Properties).distinguishedname
  }

#Alternative mit der DirectoryEntry-Klasse
$Group=[ADSI]"LDAP://$GroupDN"
$Group.member
#mögliche Ausgabe

CN=Bangkok_Karl_3000001,OU=IT,OU=Users,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern
CN=Bangkok_Karl_3000000,OU=IT,OU=Users,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern

Der direkte Zugriff auf eine Gruppe über die DirectoryEntry-Klasse ([ADSI]) ist natürlich erheblich schneller, als eine Suche über das gesamte Directory


Beispiel 2: Suchen in einer Objektkette / Prüfen, in welchen Gruppen ein User Mitglied ist

Clear-Host
Set-Strictmode -Version "2.0"

$UserDN = "CN=Bangkok_Karl_3000000,OU=IT,OU=Users,OU=Bangkok,OU=Scripting,DC=Dom1,DC=Intern"

$DirectorySearcher = ([ADSISearcher]"LDAP://")
$DirectorySearcher.Filter = "(member:1.2.840.113556.1.4.1941:=$UserDN)"
$DirectorySearcher.Searchscope="subtree"
$DirectorySearcher.FindAll() | Foreach{
  $($_.Properties).cn
  }

"`n"
#Alternative mit der DirectoryEntry-Klasse
#allerdings ohne Anzeige der verschachtelten Gruppen

$User=[ADSI]"LDAP://$UserDN"
$User.MemberOf | ForEach{
 ([ADSI]"LDAP://$_").cn
 }
#mögliche Ausgabe

GG-Bangkok002
GG-Bangkok003

Der direkte Zugriff auf einen User über die DirectoryEntry-Klasse ([ADSI]) ist natürlich erheblich schneller, als eine Suche über das gesamte Directory

 

4.2.3.4.3 extended LDAPControls

MSDN: extended Controls

Extended Controls sind über die .Net Klasse System.DirectoryServices.Protocols einsetzbar. Vorerst hier nur zwei Beispiele, die ich nach längerer Suche gefunden habe

BSonPoSH: Working with LDAP Stats Control in Powershell

http://www.tec2009.com/slides/ds/leveragepowershell_mar-elia.pptx

 

4.2.3.4.4 Zeit in LDAPFiltern (ADS_UTC_TIME)

Zu dieser Eigenschaft ein wiederholter Verweis auf MSDN: DirectorySearcher.Filter-Eigenschaft und, da man es wohl kaum besser beschreiben kann, das folgende Zitat daraus:

"Wenn der Filter ein Attribut vom Typ ADS_UTC_TIME enthält, muss der Wert das Format yyyymmddhhmmssZ aufweisen, wobei y, m, d, h, m und s jeweils für Jahr, Monat, Tag, Stunde, Minute und Sekunde steht.Der Wert für die Sekunden (ss) ist optional.Der letzte Buchstabe Z bedeutet, dass keine Zeitdifferenz vorhanden ist.In diesem Format wird "10:20:00 A.M.May 13, 1999" zu "19990513102000Z".Beachten Sie, dass Active Directory-Domänendienste Datum und Uhrzeit in koordinierter Weltzeit (Coordinated Universal Time, UTC) speichert.Wenn Sie eine Zeit ohne Zeitdifferenz angeben, geben Sie die Zeit in UTC-Zeit an.

Wenn Sie sich nicht in der UTC-Zeitzone befinden, können Sie der UTC einen Differenzwert hinzufügen (anstatt Z anzugeben), um eine Zeit Ihrer Zeitzone anzugeben.Die Differenz berechnet sich folgendermaßen: Differenz = UTC-lokale Zeit.Geben Sie eine Differenz in folgendem Format an: yyyymmddhhmmss[+/-]hhmm.Ein Beispiel: "8:52:58 P.M.March 23, 1999" New Zealand Standard Time (die Differenz beträgt 12 Stunden) wird als "19990323205258.0+1200" angegeben."


Beispiel 1: Alle Benutzer ausgeben, die nach dem 22.08.2013 angelegt wurden

Ein ähnliches Beispiel findet ihr weiter oben im Kapitel 1.3 Forest und Domänen nach Objekten Eigenschaften filtern -> Beispiel 1

Clear-Host
Set-Strictmode -Version "2.0"
 
Function Main {
    #Define Filter
    $LDAPPath = "OU=Scripting,DC=Dom1,DC=Intern"
  $Name = "UserNW*" 
  $CreatedAfter = "2013-08-22"   #ISO  YYYY-MM-DD
  #ConvertTime
  $CreatedAfter = [Management.ManagementDateTimeConverter]::ToDmtfDateTime($CreatedAfter)  
  $CreatedAfter = $CreatedAfter.Split(".")[0]+".0Z"
  $Filter = "(&(objectcategory=user)(objectclass=user)(samaccountname=$Name)(whenCreated>=$CreatedAfter))"
 
    #Define DirectorySearcher
  $SearchRoot = "LDAP://$LDAPPAth"
  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.Filter = $Filter
  $DirectorySearcher.Searchroot = $SearchRoot
  $DirectorySearcher.SearchScope = "subtree"
  $DirectorySearcher.Pagesize = 1000
  $DirectorySearcher.PropertiesToLoad.AddRange((`
         'samaccountname','whenCreated','pwdlastset','accountexpires','useraccountcontrol'))
    #Creating Array for psobjects
  $Allusers = @()
  $DirectorySearcher.FindAll() | ForEach {
        define psobject
    [psobject]$FoundUser = "" | Select samaccountname,whenCreated,pwdlastset,accountexpires,UAC,isdisabled
    $FoundUser.SamAccountName = $($_.Properties.samaccountname).Tostring()
    $FoundUser.whenCreated = $(($_.Properties).whencreated).ToString("d")
   
    Try{
    $FoundUser.pwdlastset = $([DateTime]::FromFileTime($($_.Properties.pwdlastset))).ToString("d")
    }Catch{$FoundUser.pwdlastset = "-"}
 
    Try{
    $FoundUser.accountexpires = [DateTime]::FromFileTime($($_.Properties.accountexpires))
    }Catch{$FoundUser.accountexpires = "never"}
 
    $FoundUser.IsDisabled = $(IsDisabled $_)[0]
    $FoundUser.UAC = $(IsDisabled $_)[1]
    $Allusers += $FoundUser
  }#Findall
  $Allusers | Format-Table -AutoSize
} #end function main
 
Function IsDisabled{
  Param($User)
  $UAC = $($User.Properties.item('userAccountControl'))
    if(($UAC -BAND 2) -eq 0){
      Return "False",$UAC }
    Else{ Return "True",$UAC }
 }# end function IsDisabled
 
Main
#mögliche Ausgabe

Munich_Karl_1000000;13.11.2013 2013-11-13T19:01:21;2010-04-17;False
Munich_Karl_1000001;13.11.2013 2014-03-06T23:58:00;-;True
Munich_Karl_1000002;13.11.2013 2013-11-13T19:01:21;2015-10-26;False

Die Properties "whenCreated" und "pwdlastset" / "accountexpires" sind im AD in unterschiedlichen Formaten gespeichert. (siehe Beispiele 3 und 4 in Kapitel 4.1). Daher ist die Formatierung verschieden

Beispiel 2: Die Eigenschaft Canonicalname
Manchmal ganz nützlich ist die Eigenschaft "CanonicalName", die allerdings ebenso wie die IsDisabled zuerst in einer Function erstellt werden muss.
Diese Datei herunterladen (exportuser-canonicalname..ps1.txt) exportuser-canonicalname..ps1.txt

Besonders bei der Verwendung der Eigenschaften "lastlogon", lastlogontimestamp" und "pwdlastset" sollte man sich mit der Materie genauer auseinandersetzen. So kann es schonmal vorkommen, dass der ausgelesene pwdlast-Termin eines Accounts nach dem lastlogontimestamp liegt. (siehe den zweiten link unten)

Technet: The LastLogonTimeStamp Attribute” – “What it was designed for and how it works
Technet: How LastLogonTimeStamp is Updated with Kerberos S4u2Self - "LastLogonTimeStamp can be updated even if the user has not logged on."
Technet: Exploring S4U Kerberos Extensions in Windows Server 2003

 

4.2.3.4.5 SID und UAC

Auch diese beiden Eigenschaften lassen sich leicht aus den Treffern aus ADSISearcher herausholen

Beispiel 1: SID und UAC bestimmen
Clear-Host
Set-StrictMode -Version "2.0"

$SamAccountName = "administrator"

$User = ([ADSISearcher]"((SamaccountName=$SamAccountName))").FindOne()
[String]$ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier $User.Properties.ObjectSid[0],0).Value

$UserAccountControl = $($User.Properties.UserAccountcontrol)

$ObjectSid
$UserAccountControl

Mehr Informationen zur SID findet ihr unter: PKI und Sicherheit -> Sicherheit
Mehr Informationen zur UAC findet ihr unter:  Provider (Moniker) -> 3.3 Useraccountcontrol / UserFlags
 

4.2.4 Eigenschaft Pagesize

Default: 0 (deaktiviertes Paging)

MSDN: DirectorySearcher.PageSize Property

Erklärung der MSDN: Ruft einen Wert ab, der die Seitengröße für eine ausgelagerte Suche angibt, oder legt diesen fest.

Bei der Pagesize handelt es sich um eine recht "hinterhältige" Eigenschaft von [ADSISearcher]. Lässt man die Eigenschaft auf dem DefaultWert 0 stehen, so funktionieren Searches bis 1000 zurückgegebenen Treffern tadellos, ab dem 1001-ten Treffer kommt aber nichts mehr zurück und das auch noch ohne Fehlermeldung.
Es kann also passieren, daß ein Skripte in einer kleinen Testumgebung problemlos läuft und in einem grösseren ProduktionsAD ein mehr oder weniger grosser Teil der erwarteten Treffer fehlen.

Aus diesem Grund sollte man eigentlich immer das Paging aktivieren, so dass eine Suche nach einer gewissen Anzahl (=pagesize) von Treffern beendet wird, sich aber anschliessend automatisch neu startet. Nachteile sind mir zumindest keine bekannt.

Ein üblicher Wert für pagesize ist 1000. Diese 1000 hat nichts mit den 1000 Treffern bei aktiviertem Paging zu tun!

DirectorySearcher.PageSize = 1000

 

4.2.5 Eigenschaft Searchroot

default: null

MSDN: DirectorySearcher.SearchRoot Property

Beschreibung der MSDN: Ruft einen Wert ab, der den Knoten in der Active Directory-Domänendienste-Hierarchie angibt, bei dem die Suche beginnt, oder legt diesen fest.

Ist Searchroot nicht gesetzt, beginnt die Suche am Root des Domaincontextes, also hier:

([ADSI]"LDAP://rootDSE").defaultNamingContext # z.B. DC=Dom7,DC=intern

Man beschleunigt seine LDAP-Suche natürlich, wenn der Einstiegsknoten bereits möglichst nahe an den zu durchsuchenden Objekten liegt, wie in dem folgenden Beispiel gezeigt.


Beispiel 1: Eigenschaft SearchRoot

Clear-Host
Set-Strictmode -Version "2.0"

$Filter = "(ObjectCategory=User)"
$SearchScope = "Subtree"
$PDCe = "WIN-VSKGC0FLE6E.Dom1.Intern"

$SearchRoot = "LDAP://$PDCe/OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"


$DirectorySearcher = [ADSISearcher]"LDAP://"
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.SearchRoot = $SearchRoot
$DirectorySearcher.Searchscope = "Subtree"
$DirectorySearcher.Findall() | Select Path -First 3

# mögliche Ausgabe

Path
----
LDAP://CN=Munich_Karl_1000004,OU=bla,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern  LDAP://CN=Munich_Karl_1000005,OU=bla,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern
LDAP://CN=Munich_Karl_1000006,OU=bla,OU=Executive,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern

Beachtet, dass Searchroot stets mit dem Moniker LDAP:// beginnt.

Leider kann man den SearchRoot nicht so setzen, dass alle Namingcontexts des ActiveDirectory
- cn=Dom7,DC=intern
- cn=configuration,DC=Dom7,DC=intern
- cn=schema,configuration,DC=Dom7,DC=intern
- Application contexts

default durchsucht werden. Man muss also den Schema- oder ConfigurationContainer explizit angeben, will man in diesen suchen.


Beispiel 2: Suche im Schemacontainer

Clear-Host
Set-Strictmode -Version "2.0"

$DomainDN = ([ADSI]"LDAP://rootDSE").DefaultNamingContext

$Filter = "(cn=User*)"
$SearchScope = "Subtree"
$PDCe = "WIN-VSKGC0FLE6E.Dom1.Intern"

$SearchRoot = "LDAP://$PDCe/cn=schema,cn=configuration,$domainDN"

$DirectorySearcher = [ADSISearcher]"LDAP://"
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.SearchRoot = $SearchRoot
$DirectorySearcher.FindAll() | Select Path -First 3

#mögliche Ausgabe

Path
----
LDAP://CN=User,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern
LDAP://CN=User-Account-Control,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern
LDAP://CN=User-Cert,CN=Schema,CN=Configuration,DC=Dom1,DC=Intern

 

4.2.6 Eigenschaft Searchscope

default: Subtree

Erklärung aus der MSDN: Ruft einen Wert für den vom Server überwachten Suchbereich ab oder legt diesen fest.

MSDN: DirectorySearcher.SearchScope Property

MSDN: SearchScope Enumeration

Membername

Beschreibung

Base

Beschränkt die Suche auf das Basisobjekt. Das Ergebnis enthält maximal ein Objekt.Wenn die AttributeScopeQuery-Eigenschaft für eine Suche angegeben wird, muss der Suchbereich auf Base festgelegt werden.

OneLevel

Durchsucht die unmittelbar untergeordneten Objekte des Basisobjekts, aber nicht das Basisobjekt.

Subtree

Durchsucht die gesamte Teilstruktur, einschließlich des Basisobjekts und allen zugehörigen untergeordneten Objekten. Wenn der Bereich für eine Verzeichnissuche nicht angegeben wird, wird ein Subtree-Suchtyp ausgeführt.

Ich denke, recht viel mehr braucht man zu dieser Eigenschaft nicht zu schreiben. Ein Beispiel steht ein Kapitel höher bei 5.2.2.5

 

4.2.7 Eigenschaft Tombstone

MSDN: DirectorySearcher.Tombstone-Eigenschaft

Erklärung der MSDN: Ruft einen Wert ab, der angibt, ob bei der Suche auch gelöschte Objekte, die mit den Suchfilterkriterien übereinstimmen, zurückgegeben werden sollen, oder legt diesen Wert fest.


Hintergründe zur Tombstone Eigenschaft im ActiveDirectory

Technet Magazin: Tombstone-Wiederbelebung in Active Directory


Beispiel 1: Anzeigen von tombstoned Objekten einer Domäne (LDAP-Filter)
Dieses Beispiel liest die Userobjekte (incl. Computerobjekte) aus dem DomainContainter, bei denen das TombStone-Flag gesetzt ist. 

Clear-Host
Set-Strictmode -Version "2.0"
 
Function Main{
  $Filter = "User"  # *, User, Computer, Group, PrintQueue
 
  $rootDSE = [ADSI]"LDAP://rootDSE"
  $DomainDN = $rootDSE.DefaultNamingContext
  $rootDSE.psbase.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::FastBind
  $rootDSE.psbase.Path ="LDAP://cn=Deleted Objects," + $DomainDN
   
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
  $PDCe = $Domain.PdcRoleOwner.Name
 
  $DirectorySearcher = ([ADSISearcher]"LDAP://$PDCe")
  $DirectorySearcher.searchroot=$rootDSE
  $DirectorySearcher.Filter = "(&(isDeleted=TRUE)(objectclass=$Filter))"
  $DirectorySearcher.Tombstone = $true
  $DirectorySearcher.SearchScope = [System.DirectoryServices.SearchScope]::onelevel
  $DirectorySearcher.Pagesize = 1000
  $DirectorySearcher.PropertiesToLoad.AddRange(('name','whenChanged','lastknownparent','objectclass','objectguid'))
 
  $DeletedObjects = $DirectorySearcher.Findall()
  
  $AllOuts = @()
  Foreach($DeletedObject in $DeletedObjects){
    [psobject]$Out = "" | Select-Object Name, whenchanged, lastknownparent, objecttype, objectguid
     
    $Out.whenchanged = $($DeletedObject.Properties.whenchanged).ToString("d")
    $Out.LastknownParent = $($DeletedObject.Properties.lastknownparent)
    $Guid = $($DeletedObject.Properties.objectguid)
    $Out.Objectguid = New-Object guid(,$Guid)
    $Out.Name = $($($DeletedObject.Properties.name).split("`n"))[0]
    $ObjectClass = @($($DeletedObject.Properties.objectclass))
    $Out.ObjectType = $ObjectClass[-1]
    $AllOuts += $Out
  }#For
         
  $AllOuts | ft  `
  @{Label = "Name" ; Expression =   {"{0}" -f $_.Name} ; Align = "Left" ; Width = 16},
    whenchanged,
    @{Label = "lastknownparent" ; Expression =   {"{0}" -f $_.lastknownparent} ; Align = "Left" } ,
    objecttype,objectguid -auto
}#End Main
 
Main

#mögliche Ausgabe
 

Name       whenchanged lastknownparent                                        objecttype objectguid                          
----       ----------- ---------------                                        ---------- ----------                          
UserLIQ020 12.10.2014  OU=IT,OU=Users,OU=Dhaka,OU=Scripting,DC=dom1,DC=intern user       e4097b7a-bcfd-49ad-a064-7d9a676e331f
UserVUP020 03.10.2014  OU=IT,OU=Users,OU=Tokio,OU=Scripting,DC=dom1,DC=intern user       3bbef799-a763-47e4-8346-aa9a280e072c

 

Beispiel 2: gelöschte Sitelinks im Configurationcontainer auslesen

Das Beispiel ist nahezu identisch zu Beispiel 1. Die Unterschiede habe ich orange markiert

Clear-Host
Set-Strictmode -Version "2.0"
 
Function Main{
  $Filter = "SiteLink"  
 
  $rootDSE = [ADSI]"LDAP://rootDSE"
  $DomainDN = $rootDSE.DefaultNamingContext
  $rootDSE.psbase.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::FastBind
  $rootDSE.psbase.Path ="LDAP://cn=Deleted Objects,cn=configuration," + $DomainDN
   
  $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
  $PDCe = $Domain.PdcRoleOwner.Name
 
  $DirectorySearcher = ([ADSISearcher]"LDAP://$PDCe")
  $DirectorySearcher.searchroot=$rootDSE
  $DirectorySearcher.Filter = "(&(isDeleted=TRUE)(objectclass=$Filter))"
  $DirectorySearcher.Tombstone = $true
  $DirectorySearcher.SearchScope = [System.DirectoryServices.SearchScope]::onelevel
  $DirectorySearcher.Pagesize = 1000
  $DirectorySearcher.PropertiesToLoad.AddRange(('name','whenChanged','lastknownparent','objectclass','objectguid'))
 
  $DeletedObjects = $DirectorySearcher.Findall()
  
  $AllOuts = @()
  Foreach($DeletedObject in $DeletedObjects){
    [psobject]$Out = "" | Select-Object Name, whenchanged, lastknownparent, objecttype, objectguid
     
    $Out.whenchanged = $($DeletedObject.Properties.whenchanged).ToString("d")
    $Out.LastknownParent = $($DeletedObject.Properties.lastknownparent)
    $Guid = $($DeletedObject.Properties.objectguid)
    $Out.Objectguid = New-Object guid(,$Guid)
    $Out.Name = $($($DeletedObject.Properties.name).split("`n"))[0]
    $ObjectClass = @($($DeletedObject.Properties.objectclass))
    $Out.ObjectType = $ObjectClass[-1]
    $AllOuts += $Out
  }#For
         
  $AllOuts | ft  `
  @{Label = "Name" ; Expression =   {"{0}" -f $_.Name} ; Align = "Left" ; Width = 16},
    whenchanged,
    @{Label = "lastknownparent" ; Expression =   {"{0}" -f $_.lastknownparent} ; Align = "Left" } ,
    objecttype,objectguid -auto
}#End Main 
 
Main

#mögliche Ausgabe

 

Name    whenchanged lastknownparent                                                            objecttype objectguid                          
 
----    ----------- ---------------                                                            ---------- ----------                          
muc-bkk 12.10.2014  CN=IP,CN=Inter-Site Transports,CN=Sites,CN=Configuration,DC=dom1,DC=intern siteLink   2c1aa80c-9ace-4b5c-a27e-5cf1c2d8de5d

In einem LDAPBrowser wie ldp.exe ist das Object "muc-bkk" mit seinen Eigenschaften natürlich auch zu sehen

 

Im Kapitel "cmdlets -> user -> Beispiel 5" könnt ihr euch ansehen, wie ihr an die deleted objects mittels cmdlets wie Get-ADObject und Get-ADUser herankommt

 

4.2.8 PropertiesToLoad

MSDN: DirectorySearcher.PropertiesToLoad-Eigenschaft
 

4.3 Kombination aus [ADSISearcher] und [ADSI] / Objekte verändern

Bisher haben wir in diesem Kapitel Beispiele zum Auslesen von Objekten und deren Eigenschaften gesehen. Dazu konnte man auf die Properties der von den Methoden "FindOne()" und "FindAll()" zurückgelieferten Objekte zugreifen.
Oft besteht die praktische Aufgabe aber neben dem Auffinden bestimmter Objekte auch im Verändern derselben. In diesem Fall muss man die gefundenen LDAP-Pfade der von [ADSISearcher] gelieferten Objekte nehmen und damit eine LDAP-Verbindung auf das Objekt mit [ADSI] erstellen.

Beispiel 1: Suchen und Verändern eines Userobjektes
Dieses Beispiel zeigt, wie man gefundene Objekte verändern kann. Als Lösungsweg sucht man im ersten Schritt mit [ADSISearcher] alle Objekte im AD, bindet im zweiten Schritt mit [ADSI] ein Directoryentry darauf und kann anschliessend ein oder mehrere Eigenschaften der Objekte verändern.

Set-StrictMode -Version "2.0"
Clear-Host

#getting PDCe
$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe = $Domain.PdcRoleOwner.Name

$AccountExpires_Date = "2019-02-17" #ISO-Format
$AccountExpires_FileTime = [string](Get-Date $AccountExpires_Date).ToFileTime()
$TerminalServicesProfilePath = "\\192.168.47.34\`$TSHome"

$Filter = "(&(ObjectCategory=user)(SamAccountName=Munich_Karl_100000*))" #* Platzhalter

#Search
$DirectorySearcher = ([ADSISearcher]"LDAP://$PDCe")
$DirectorySearcher.Filter = $Filter
$DirectorySearcher.Pagesize = 1000
$UserPathes = $DirectorySearcher.FindAll().Path

#[ADSI] binding and changing
Foreach($UserPath in $UserPathes) {
 Try{
   $User = [ADSI]"$UserPath"
   $User.Invokeset("TerminalServicesProfilePath",$TerminalServicesProfilePath)
   $User.Invokeset("accountexpires",$AccountExpires_FileTime)
   $User.SetInfo()
   Write-Host "$($User.SamAccountName) has been successfully modified"
 }Catch{
   Write-Host "Modifying $($User.SamAccountName) has failed"
 }#try/catch

} #for

Anmerkung 1  Da die Eigenschaft TerminalServicesProfilePath nicht vom .Net verwaltet wird, muss sie über InvokeSet, das direkt ins AD greift, gesetzt werden. Unter Powershell v1.0 muß dafür $user.psbase.invokeset verwendet werden.

Anmerkung 2  Das Skript oben läuft so wie es ist unter WindowsXP, Windows2003, Windows2008 und Windows8. Unter Windows7 und Vista muss man, um auf Terminaldiensteprofil- oder Remotedesktopdiensteprofileigenschaften zugreifen zu können, erst die Datei tsuserex.dll von einem DC aus dem Verzeichnis %windir%\system32 in das gleiche Verzeichnis auf dem Client kopieren und registrieren mit "regsvr32.exe tsuserex.dll"

Ask the Directory Services Team: 

Getting the Terminal Services Tabs to Appear in AD Users and Computers

 

Anmerkung 3  Schreibaktionen würde ich immer gegen den PDC-Emulator laufen lassen


Beispiel 2: Suche nach Usern mit leeren Feldern (Homedirectory oder TerminalservicesProfilepath)

Manchmal muss man prüfen, ob alle User die notwendigen Felder gefüllt haben. Mit dem Beispielskript kann man sowohl Standardattribute wie "Homedirectory" als auch Attribute untersuchen, die nur über invokget ansprechbar sind, wie "Terminalserviceprofilepath"

#Requires -version 2.0 
   #damit läuft das Skript nur unter Powershell V2.0

#Teil 1: PDCe und distinguishedName auslesen
$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name

#Teil 2: Sucheigenschaften des [ADSISearchers9 festlegen s. Kapitel 5.2
$SearchAttribute = "TerminalServicesProfilePath" 
#$SearchAttribute = "Homedirectory"
$DisplayAttribute = "DistinguishedName"
$SearchRoot = "OU=BenutzerA,OU=Scripting,$domainDN"
$filter = "(&(objectClass=User)(objectCategory=User))"
$ds = [ADSISearcher]"LDAP://$PDCe"

$ds.filter=$filter
$ds.pagesize=1000
$ds.SearchRoot = "LDAP://$SearchRoot"

write-host -foregroundcolor red "folgende User haben ein leeres Feld $SearchAttribute `n"

#Teil 3: Suche ausführen, Ergebnisse in gewünschter Form ausgeben
$ds.findAll() | ForEach{
    $user=[ADSI]($_.path) #bindet die zurückgelieferten Ergebniss auf $user
    IF([string]::isNullOrEmpty($user.invokeget($SearchAttribute))){ 
    $user.$DisplayAttribute #wenn isNullOrEmpty = $true dann zeige den dn an
    }
}

#Ausgabe

folgende User haben ein leeres Feld TerminalServicesProfilePath

CN=A34989,OU=BenutzerB,OU=Scripting,DC=Dom7,DC=intern
CN=A34991,OU=BenutzerB,OU=Scripting,DC=Dom7,DC=intern
CN=A34992,OU=BenutzerB,OU=Scripting,DC=Dom7,DC=intern

Anmerkung 1: Da die Eigenschaft TerminalServicesProfilePath nicht vom .Net verwaltet wird, muss sie über invokeget, das direkt ins AD greift, gelesen werden. Unter Powershell v1.0 muß dafür $user.psbase.invokeset verwendet werden.

Anmerkung 2: Wenn "nur" nach einer Eigenschaft gefiltert werden soll, die direkt, also ohne invokeget, vom Objekt ausgelesen werden kann (wie homedirectory), kommt man mit einem leicht veränderten LDAP-Filter vielleicht etwas einfacher und schneller zum Ziel:

... #analog dem obigen Beispiel
$filter = "(&(objectClass=User)(objectCategory=User)(!Homedirectory=*)"
... #analog dem obigen Beispiel
ds.findall() | select path

Bei der Eigenschaft Terminalserviceprofile scheitert dieser Ansatz!

Anmerkung 3: Wie schon ein paarmal erwähnt, lasse ich grössere AD Aktionen immer gegen den PDC-Emulator laufen lassen (Kapitel 3.2.2.1)

Anmerkung 4: die Methode isnullorempty der .Net Klasse System.String liefert $true zurück, wenn des Feld leer oder null ist, also schonmal befüllt und gelöscht wurde (=leer) oder noch nie befüllt wurde (=null)

 

4.4 Existenzprüfung von AD Elementen

Beim Anlegen von Objekten im AD ist es eine kleine Herausforderungen mit bereits existierenden Objekten umzugehen. Wenn das Objekt nicht angelegt werden kann, weil es schon existiert sollte der Anwender auch eine ordentliche Benachrichtigung erhalten und nicht nur eine Fehlermeldung des Systems.

Es gibt zahlreche Möglichkeiten -bestimmt mehr als die in den drei Beispielen gleich folgen werden (etwa mit dem Cmdlet "Search-ADAccount", oder mit einem Try/ Catch - Konstrukt)-, um diese Anforderung umzusetzen:

 

Beispiel 1: Existenzprüfung von AD-Objekten (User, Computer, OUs) mit der FindOne-Methode der [ADSISearcher] (=DirectorySearcher)-Klasse

Clear-Host
Set-StrictMode -Version "2.0"

$DN = "CN=Munich_Karl_10200,OU=IT,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
$SamAccountName = "Munich_Karl_10200"
$ComputerName = "MunichDesktop015"
$OUDN = "OU=NoteBooks,OU=Computers,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"

 
[Bool]([AdsiSearcher]"DistinguishedName=$DN").FindOne()
[Bool]([AdsiSearcher]"SamAccountName=$SamAccountName").FindOne()
[Bool]([AdsiSearcher]"SamAccountName=$ComputerName$").FindOne()

[Bool]([AdsiSearcher]"DistinguishedName=$OUDN").FindOne()
[Bool]([System.DirectoryServices.DirectorySearcher]"(SamAccountName=$SamAccountName)").FindOne()

Bei Vorhandensein der Beispielobjekte geben alle Codeschnippsel "True" zurück, sonst "False"

MSDN: DirectorySearcher.FindOne-Methode

Beispiel 2: Existenzprüfung von AD-Objekten (User, Computer) mit den CmdLets Get-ADUser und Get-ADComputer

Clear-Host
Set-StrictMode -Version "2.0"

$DN = "CN=Munich_Karl_10200,OU=IT,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
$SamAccountName = "Munich_Karl_10200"
$ComputerName = "MunichDesktop015"
$Filter = {Name -like $ComputerName}

[Bool](Get-ADUser -filter {DistinguishedName -eq $DN})
[Bool](Get-ADUser -filter {SamAccountName -eq $SamAccountName})
[Bool](Get-ADComputer -filter $Filter)

Bei Vorhandensein der Beispielobjekte geben alle Codeschnippsel "True" zurück, sonst "False".

Zur Existenzprüfung von OUs steht das Cmdlet "Get-ADOrganizationalUnit" zur Verfügung

 

Beispiel 3: Existenzprüfung von AD-Objekten (User, Computer, OUs) mit der Exists-Methode der [ADSI] (=DirectoryEntry)-Klasse

Clear-Host
Set-StrictMode -Version "2.0"

$UserDN = "CN=Munich_Karl_10200,OU=IT,OU=Users,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
$ComputerDN = "CN=MunichDesktop015,OU=Desktops,OU=Computers,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"

$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe = $Domain.PdcRoleOwner.Name
 
$OUDN = "OU=Subnotebooks,OU=NoteBooks,OU=Computers,OU=Munich,OU=Scripting,DC=Dom1,DC=Intern"
 
[ADSI]::Exists("LDAP://$UserDN")
[ADSI]::Exists("LDAP://$ComputerDN")
[System.DirectoryServices.DirectoryEntry]::Exists("LDAP://$ComputerDN")

[ADSI]::Exists("WinNT://$PDCe/$SamAccountName")
[ADSI]::Exists("LDAP://$OUDN")

Bei Vorhandensein der Beispielobjekte geben alle Codeschnippsel "True" zurück, sonst "False"

MSDN: DirectoryEntry.Exists Method

Über die Verwendung des "WinNT://" -Monikers findet ihr mehr Informationen:  Überblick [ADSI] -> 3.2.1 WinNT-Provider

 

Als kleine Erinnerung eingeschoben: Bei der Exists-Methode handelt es sich im Gegensatz zur FindOne-Mehtode der DirectorySearcher-Klasse in Beispiel 1 um eine statische Methode! In Powershell muss man solche statischen Methoden mit "::" aufrufen.

In der MSDN erkennt ihr die statischen Methoden am "roten 'S'" in der linken Spalte der Übersicht der Methoden. ->  MSDN: DirectoryEntry Methods
Mit Get-Member erhält man die statischen Methoden durch den Positionsparameter  "-static"  -> Die drei wichtigsten CmdLets -> 4.2 get-member mit dem Parameter -static

 

Beispiel 4: Anlage eines Users mit vorheriger Existenzprüfung mit Get-ADuser

Ich nehme mir eine der Möglichkeiten aus dem letzten Beispiel 2 heraus, um damit die Existenzprüfung vor der Useranlage mit New-Aduser durchzuführen

Set-StrictMode -Version "2.0"
Clear-Host

Function Main{

  $DomainDN =(Get-ADDomain).DistinguishedName

  #Daten zur Anlage
  $Location = "Munich"
  $OuDN = "OU=IT,OU=Users,OU=Munich,OU=Scripting"
  $UserName = "Munich_Karl_10000"
  $Password = "Hurra123"
  $Description = "Created by New-ADUser"

  New-UserAccount $DomainDn $Location $OuDn $UserName $Description $Password
}#End Main

Function New-UserAccount{
  <#
  .Synopsis
  create useraccounts
  #>

  Param($DomainDn,$Location,$OuDn,$UserName,$Description,$Password)

  #Domänendaten ermitteln
  $DomFqdn = (Get-ADDomain).DNSRoot
  $Pdce = (Get-ADDomain).PdcEmulator

  #Usereigenschaften definieren
  $CNUser = "CN=$UserName"
  $Sn = "$UserName"
  $SamAccountname = $UserName
  $UserPrincipalName="$SamAccountName@$DomFqdn"
  $AccountPassword = ConvertTo-SecureString $Password -AsPlainText -Force

  #User anlegen mit Abfrage, ob der User schon exisitiert

  If ( [Bool](Get-ADUser -filter {SamAccountName -eq $SamAccountName}) -ne $True ){
    New-ADUser -Name $Sn -SamAccountName $SamAccountName -Path "$OuDN,$DomainDN" `
    -Surname $Sn -UserPrincipalName $UserPrincipalName -Description $Description `
    -AccountPassword $AccountPassword -Enabled 1
     "$SamAccountName wurde angelegt"
  }Else{
     "$SamAccountName existiert schon"
  }
}#End New_UserAccount

Main
#mögliche Ausgaben

Munich_Karl_10000 wurde angelegt
Munich_Karl_10000 existiert schon

Der hier interessante Teil der Existenzprüfung ist grün eingefärbt

Das Beispiel habe ich -leicht abgewandelt- aus ActiveDirectory -> Einleitung/ TestDaten -> 2.3 Userkonten anlegen übernommen. Dort gibt es auch Beispielem um Computer- und Userkonten nur mit .Net anzulegen.

 

4.5.2 LDAP-Pakete mit Netmon analysieren

Um mit dem Netmon 3.4 LDAP-Pakete analysieren zu können, muss zuerst ein zusätzliches msi-Paket mit einem LDAP-Parser installiert werden. Dies geschieht durch den Doppelklick eines Paketes, welches man hier erhält:

Codeplex - Network Monitor Open Source Parsers

Download: NetworkMonitor_Parsers_x86.msi

Download:NetworkMonitor_Parsers_x64.msi

 

Unter den LDAP-Paketen sucht man als erstes nach den LDAP-Message Paketen und sollte dort auf die dieselben Eigenschaften stossen, die wir im Kapitel 5.2 besprochen haben