Behandelte Themen

  1. Einleitung
  2. Powershell und ActiveDirectory unter W2K8R2, Win7, W2K3
  3. System.DirectoryServices.Directoryentry [ADSI]
    3.1 Ein PowershellObjekt an ein bestimmtes ActiveDirectory Objekt binden
          Beispiel 1: [ADSI] TypeAccelerator mit verschiedenen Schreibweisen
          Beispiel 2: Binden eines ADObjekts mit dem Quest-cmdlet QADUser
    3.2 Beschreibung des WinNT- und ADSI-Provider
          3.2.1 WinNT Provider
                  Beispiel 1: Neues Passwort an einen bestehenden lokalen oder Domänenusers vergeben
                  Beispiel 2: Neuanlage eines lokalen oder DomänenUsers mit lokalen Eigenschaften
                  Beispiel 3: Hinzufügen eines Users zu einer Gruppe
                  Beispiel 4: Entfernen von Mitgliedern aus einer lokalen Gruppe / Auflisten der Gruppenmitglieder
                  Beispiel 5: Auslesen einer Usereigenschaft aus einem AD mittels WINNT-Provider
                  Beispiel 6: Auslesen aller lokalen User
         3.2.2 LDAP Provider
                  3.2.2.1 LDAP Connection Strings
                              Beispiel: dynamisches Ermitteln des distinguishedNames der Domäne, des PDCEmulators und des FQDNs der Domäne
                  3.2.2.2 Connection auf die Domäne
                              Beispiel 1: Verbindung auf die Domäne
                              Beispiel 2: Anzeigen von Domäneneigenschaften
                  3.2.2.3 Connection auf RootDSE
                              Beispiel 1: Zugriff auf RootDSE
                              Beispiel 2: Ermitteln einiger LDAP-Eigenschaften des RootDSE
                  3.2.2.4 Kerberos / NTLM Authentifizierung
                              Beispiel 1: Bindung an RootDSE über das Kerberosprotokoll
                              Beispiel 2: Bindung an RootDSE über das NTLMProtokoll
    3.3 Useraccountcontrol / UserFlags
                   
  4. Klassen im AD
    4.1 User
         Beispiel 1: Anzeige aller MultivaluedAttribute der Userklasse
         Beispiel 2: Abfrage, ob eine Eigenschaft Multi- oder SingleValued ist
         4.1.1 Anlegen von Usern
                   Beispiel 1: Massenanlage von Testusern
                   Beispiel 2: Anlage eines Users (mit vielen Properties)
                   Beispiel 3: Useranlage mit Daten aus einer csv-Datei
         4.1.2 Lesen und Verändern von Userproperties
                  Beispiel 1: Auslesen von Userkonten, die sich seit xx-Tagen nicht mehr an der Domäne angemeldet haben
                  Beispiel 2a: Exportieren von Usern einer OU in eine csv-Datei (eine Ebene)
                  Beispiel 2b: Exportieren von Usern einer OU nach Excel (eine Ebene)
                  Beispiel 3: Exportieren von Usern einer OU in eine csv-Datei (eine Ebene oder tiefer)
                  Beispiel 4:  Exportieren von Usern aus einer OU mit Eigenschaften wie badpassworttime oder lastlogontimestamp
     4.2 Gruppen
           4.2.1 Anlage von Gruppen
                    Beispiel 1: Massenanlage von Testgruppen 
           4.2.2 Gruppenmitgliedschaften
                    Beispiel 1: User einer ADGruppe hinzufügen oder entfernen
                    Beispiel 2: Gruppe in Gruppe verschachteln / Gruppe aus Gruppe entfernen
                    Beispiel 3: Auslesen von Gruppenmitgliedern (unverschachtelt)
                    Beispiel 4a: Auslesen von Gruppenmitgliedern (verschachtelt) mittels ds-Tools
                    Beispiel 4b: Auslesen von Gruppenmitgliedern (verschachtelt) mittels ds-LDAP-Controls
                    Beispiel 4c: Auslesen von Gruppenmitgliedern (verschachtelt) mittels Rekursion
                    Beispiel 5: In welchen Gruppen ist der angemeldete Benutzer Mitglied
    4.3 OUs/ GPOs (noch leer)
    4.4 Forests und Domänen
          Beispiel 1: Mehrere Varianten zum Ermitteln der aktuellen Domäne und aller Domaincontroller
          Beispiel 2: Auflisten aller GlobalCatalogserver eines Forests
          Beispiel 3: Auflisten aller DCs eines Forests
    4.5 Standorte und Dienste
          Beispiel 1: Verbinden auf eine bestimmte Site
          Beispiel 2: Auflisten aller Sites
    4.6 ActiveDirectory Schema
    4.7 Wenns mal nicht so läuft - ADSI Errorcodes
  5. Queries
    5.1 einfache Queries mit [ADSI] 
          Beispiel 1: Auflisten aller Objekte einer OU (ohne Rekursion)
          Beispiel 2: Suchen ab einem Eintiegspunkt nach einem Namensbestandteil (mit Rekursion)
    5.2 Queries mit [ADSISearcher]
          Beispiel 1: vier verschiedene Syntaxvarianten für die .Net-Klasse System.Directoryservices.Directorysearcher
         5.2.1 Methoden von [ADSISearcher]
                  Beispiel 1: Die Methoden findall() und findone() 
         5.2.2 Eigenschaften von [ADSISearcher]
                 5.2.2.1 Eigenschaft Asynchronous
                             Beispiel 1: Eigenschaft asynchronous
                 5.2.2.2 Eigenschaft CachedResults
                             Beispiel 1: Eigenschaft CachedResults
                 5.2.2.3 Eigenschaft Filter
                           5.2.2.3.1 Hilfsmittel zum Filterbau
                           5.2.2.3.2 FilterAttribute objectCategory und objectClass
                                          Beispiel 1: gemeinsamer Einsatz von objectClass und objectCategory
                           5.2.2.3.3 Resourcenverbrauch von Filtern
                           5.2.2.3.4 LDAPControls
                                          Beispiel 1: Suche nach Gruppentyp mit bitweisem Vergleich
                                          Beispiel 2: Aufzählen aller Category 1 oder Category 2 Objekte des Schemas
                                          Beispiel 3: Suchen in einer Objektkette, Mitglieder einer Gruppe auslesen
                                          Beispiel 4: Suchen in einer Objektkette, Prüfen, in welchen Gruppen ein User Mitglied ist            
                           5.2.2.3.5 Zeit in LDAPFiltern (ADS_UTC_TIME)
                                          Beispiel: Alle Benutzer ausgeben, die nach dem 22.08.2010 angelegt wurden 
                  5.2.2.4 Eigenschaft Pagesize
                  5.2.2.5 Eigenschaft Searchroot
                              Beispiel 1: Eigenschaft searchroot
                              Beispiel 2: Suche im Schemacontainer
                  5.2.2.6 Eigenschaft Searchscope
                  5.2.2.7 Eigenschaft Tombstone
                              Beispiel 1: Anzeigen aller tombstoned User und der Eigenschaft "whenchanged" 
    5.3 Kombination aus [ADSISearcher] und [ADSI]
          Beispiel 1: Suchen und Verändern eines Userobjektes
          Beispiel 2: Suche nach Usern mit leeren Feldern (Homedirectory oder TerminalservicesProfilepath)
    5.4 Queries mit ADO
    5.5 LDAP Calls analysieren

******************************************************************************************************

1 Einleitung

ActiveDirectory wird in der Powershell V1 noch relativ stiefmütterlich behandelt. Wobei "stiefmütterlich" nur bedeutet, dass man nicht den gewohnten Komfort von cmdlets erwarten kann, sondern direkt mit der .Net Klasse System.DirectoryServices.Directoryentry [ADSI]System.Directory.DirectorySearcher [ADSIseacher] oder dem  System.DirectoryServices.ActiveDirectory-Namespace mit seinen zahlreichen Klassen arbeiten muss. Mit einigen prägnanten Beispielen, sowie den richtigen MSDN Quellen an der Hand, ist der Programmieraufwand für ein ADSkript auch nur mit diesen nativen Mitteln durchaus machbar und bietet sogar wie weiter unten beschrieben, einige Vorteile.

Mit der Powershell V2 und nur in Verbindung mit einem W2k8R2-Domaincontroller oder einem Windows7 Client mit installierten RSAT-Tools  Microsoft Download Center: Remote Server Administration Tools for Windows 7 stellt Microsoft cmdlets zur AD-Administration mittels Powershell bereit.


Einige Vorteile bei der Verwendung von nativen .Net Klassen

- Skripte mit .Net-Klassen laufen unter Windows2003 oder XP ohne Anpassungen in der Powershell V2.0
- Skripte mit .Net-Klassen sind auch unter Powershell V1.0 anwendbar
- Es müssen keine Module nachinstalliert werden
- Skripte laufen auf allen Systemen ab XP/2003 
- Module müssen nicht gepflegt werden hinsichtlich Versionsänderungen, Support und ähnlichem
- Es müssen keine Extensions von Dittherstellern, wie Quest, nachinstalliert werden
- Skripte können einfacher in andere .Net-Sprachen (C#, VB.Net) konvertiert werden
- MSDN-Sourcen für C# oder VB.Net können an die Powershellsyntax angepasst werden

 

2 ActiveDirectory-Module unter W2K8R2, Win7, W2K3

Vorerst hier nur einige Links mit Beschreibungen, die meiner Meinung nach die notwendigen Vorbereitungsschritte gut wiedergeben
 Windows PowerShell 2.0 Brings Scripting to Active Directory — and Not Just for Windows Server 2008 R2

Windows2008R2
 MSDN - Active Directory Powershell Blog: Active Directory Module for Windows PowerShell – Quick start guide

Hat man in seiner Domäne mindestens einen 2008R2 Domänencontroller, so kann man mit folgenden Befehlen das ActiveDirectory-Module importieren:

import-module servermanager
Add-WindowsFeature -Name "RSAT-AD-PowerShell" -IncludeAllSubFeature

#get-command –module servermanager -verb *
#http://technet.microsoft.com/de-de/library/cc732757.aspx

import-module activedirectory

#get-help *-AD*
#get-command -module ActiveDirectory -verb *
#get-help New-AD*
#get-help about_ActiveDirectory_Filter

 

AD Powershell module is installed by default on a DC.

ActiveDirectory Module unter Windows 7
 MSDN: Active Directory Powershell Blog: Active Directory Powershell: Installation using RSAT on Windows 7

 

Import des GroupPolicy Modules
 Technet: Group Policy Cmdlets in Windows PowerShell

Mit diesem GPO-Module erreicht man dieselbe Funktionalität, die die VB-Skripte der GPMC angeboten haben.


Activedirectory-Module und Windows2003 Server (ADWS)
 MSDN: Active Directory Powershell Blog: Active Directory Management Gateway Service released to web - manage YOUR Windows 2003/2008 DCs USING AD POWERSHELL !

3 System.DirectoryServices.Directoryentry [ADSI]

 MSDN: DirectoryEntry Class

Mit dieser Klasse kann eine PowershellVariable auf ein beliebiges AD-Objekt gebunden werden. Anschliessend kann man über die Eigenschaften und Methoden der Klasse das Objekt auslesen und bearbeiten.

 

3.1 Überblick über die [ADSI]-Provider

Der erste entscheidende Schritt ist es einem Powershellobjekt ein AD-Objekt zuzuordnen. Danach kann man die Eigenschaften dieses Objekts verändern (Usernamen ändern), diesem Objekt ein Unterobjekt hinzufügen (User in einer OU erstellen) und viele Dinge mehr.
Kennt man den Pfad des Objects bereits, welches verändert oder geprüft werden soll, so benutzt man zur Objekterstellung den [ADSI] Type-Accelerator, gefolgt vom passenden Protokoll (LDAP://, WinNT://, GC und ein paar mehr) und dem ADPfad (ou=TestOU, dc=TestDom1, dc=de) zu dem Objekt.

In Kapitel 5 (Queries) werden die Schritte beschrieben, wenn das Directory erst nach bestimmten Kriterien mit dem [ADSISearcher] Type-Accelerator durchsucht werden muss, um eines oder mehrere AD-Objekte zu finden, die im weiteren Skript bearbeitet werden
Die möglichen Protokolle (oder Provider) von [ADSI], sowie weitere Hintergründe findet man im ScriptingGuide:
 Technet: Windows Scripting Guide 2000 Table 5.5 ADSI Providers and the Directory Services They Access

Provider

To Access

Beispiel

LDAP provider

LDAP Version 2 - and Version 3-based directories, including Active Directory.

[ADSI]"LDAP://dc=NA,dc=fabrikam,dc=com"

GC provider

The Global Catalog on Active Directory domain controllers designated as Global Catalog servers. The GC provider is similar to the LDAP provider but uses TCP port number 3268 to access the Global Catalog.

[ADSI]"GC://dc=NA,dc=fabrikam,dc=com"

ADSI OLE DB provider

Active Directory to perform search operations.

siehe Kapitel 5.4

WinNT provider

Windows NT domains and Windows NT/Windows 2000/Windows XP local account databases.

[ADSI]"WinNT://NA"

IIS provider

The Internet Information Services (IIS) metabase.

[ADSI]"IIS://sea-dc-01.na.fabrikam.com"

NDS provider

Novell NetWare Directory Services.

[ADSI]"NDS://server01/o=org01/dc=com/dc=fabrikam/dc=na"

NWCOMPAT provider

The Novell Netware Bindery.

[ADSI]"NWCOMPAT://server01"

Wie schon häufiger erwähnt, ist der [ADSI]-Type Accelerator einfach eine sehr kompakte Schreibweise für die DotNet Klasse System.DirectoryServices.DirectoryEntry.


Beispiel 1: [ADSI] TypeAccelerator mit verschiedenen Schreibweisen

Zum besseren Verständnis möcht ich drei ergebnisgleiche Schreibweisen dieser Klasse zeigen. Selbstverständlich benutze ich auch die letzte, kompakte Schreibweise.

#Variablendefintionen
$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext #z.B. Dc=Dom7,DC=intern
$userDN="CN=Administrator,CN=Users,$domainDN"

# Folgende Schreibweisen sind indentisch und liefern dasselbe
# PowershellObjekt zurück

# Powershellsyntax
$user = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$userDN")
$user

# DotNet Schreibweise"
$User = [System.DirectoryServices.DirectoryEntry]"LDAP://$userDN"
$user

# Type-Accelerator ADSI und LDAP-Provider
$user=[ADSI]"LDAP://$userDN"
$user

# Ausgabe für alle drei Schreibweisen

distinguishedName : {CN=Administrator,CN=Users,DC=Dom1,DC=intern}
Path              : LDAP://CN=Administrator,CN=Users,DC=Dom1,DC=intern


Anmerkung: Die Syntax in VB2005/2008 lautet

Dim UserDN as String ="CN=Administrator,CN=Users,DC=dom1,DC=de"
Dim User As New DirectoryServices.DirectoryEntry("LDAP://" & UserDN)



Beispiel 2:  Binden eines ADObjekts mit dem Quest-cmdlet QADUser

Komfortabel ist das Arbeiten mit den  Questextensions. Ein Powershellskript läuft damit aber nur, wenn auf dem Host die Questextensions installiert sind. Potentielle Nachteile sollte man bedenken.

#Variablendefintionen
$domainDN="dc=dom1,dc=de"
$userDN="CN=Administrator,CN=Users,DC=dom1,DC=de"

$user=get-QADUser "Administrator"
$user=get-QADUser $UserDN

 

3.2 Beschreibung des WinNT- und LDAP-Providers

Der meines Erachtens wichtigste Provider aus der Tabelle in 3.1 oben ist LDAP://. Da ich den WinNT-Provider dennoch nicht uninterressant finde, man aber im Internet deutlich weniger dazu findet, habe ich unter Kapitel 3.2.1 einige Beispiele zusammengestellt. In Kapitel 3.2.2 gehe ich dann eher allgemein auf den LDAP-Provider ein und vergleiche LDAP- und WinNT Provider miteinander.
Konkrete Beispiele zu ADObjekten wie Useranlage, Gruppenzugehörigkeiten auslesen und ähnliches kommen in Kapitel 4

3.2.1 WinNT Provider

WinNT ist der Provider, den man in erster Linie für den Umgang mit lokalen Usern oder Gruppen, also der lokalen SAM,  benutzt. Auch der Zugriff auf NT4.0 Domaincontroller ist damit möglich.
WinNT ist nicht ganz so üblich wie LDAP für das Verwalten eines ActiveDirectories, dennoch kann man ihn auch dafür nutzen. Das Setzen von Usereigenschaften oder das Verschieben von Usern in Gruppen, also Aktionen die keine reinen AD-Funktionalitäten wie den Distinguishedname nutzen, kann man mit WinNT bequem und kompakt skripten.


Beispiel 1: Neues Passwort an einen bestehenden lokalen oder Domänenusers vergeben

$userName="KarlNapf"
$computer="Client01" #hier könnte auch ein AD-Domaincontroller oder "." stehen

$user=[ADSI] "WinNT://$computer/$userName"
$user.setpassword("Hurra123")

$user.setinfo()

 

Beispiel 2: Neuanlage eines lokalen oder DomänenUsers mit lokalen Eigenschaften

$userName="KarlNapf"
$computer="Client01" #hier könnte auch ein AD-Domaincontroller oder "." stehen
$machine=[ADSI]"WinNT://$computer"

$newUser= $machine.Create("user", $userName)

$newuser.SetPassword("Hurra333") # Das Password muss unmittelbar nach Erstellen des Accounts gesetzt werden

$newuser.SetInfo()  #ist notwendig nach der Anlage des Kontos

$newuser.invokeset("description","hurra123") #ab Powershell V2.0 ist psbase vor invokeset nicht mehr nötig
$newuser.description="Hurra456"
$newuser.homedirectory = "c:\basisordner" #oder \\Server1\Folder1
$newuser.loginscript="loginscript.vbs"
$newuser.profile="c:\profilepath"

$newuser.invokeset("PasswordExpired", 1) #User muss Password bei der nächsten Anmeldung ändern
$newuser.invokeSet('AccountDisabled', $false) #ab Powershell V2.0 ist psbase vor invokeset nicht mehr nötig
$newuser.SetInfo() 

 

Beispiel 3: Hinzufügen eines Users zu einer Gruppe

$userName="KarlNapf"
$computer="Client01" #hier könnte auch ein AD-Domaincontroller oder "." stehen
$Gruppenname="Test01"

$user=[ADSI]("WinNT://$usercomputer/$userName") #anstelle des Computers kann hier auch eine Domäne stehen!
$group =[ADSI]("WinNT://$gruppencomputer/$Gruppenname")
$Group.Invoke("Add",$User.Path)

$user.setinfo()

Soll ein Domänenuser in eine lokale Gruppe eingefügt werden, ist der $usercomputer ein Domaincontroller und der $gruppencomputer der Rechner mit den lokalen Konten


Beispiel 4: Entfernen von Mitgliedern aus einer lokalen Gruppe / Auflisten der Gruppenmitglieder

#Variablendefinition
$groupname="Benutzer"
$computer="Client01"
$DC="DC1" #Domaincontroller , bei lokalen Objekten muss der Client verwendet werden
$RemovingObjectName="Domänen-Benutzer" #Gruppe oder User

#Binden
$RemovingObject=[ADSI]("WinNT://$DC/$RemovingObjectName")
$group =[ADSI]("WinNT://$computer/$groupname")

#Entfernen der Domänen-Benutzer und der NT-Autorität/Authentifizierte Benutzer
$Group.Invoke("remove",$RemovingObject.Path)
$Group.Invoke("remove","WinNT://NT-Autorität/Authentifizierte Benutzer")

#Auflisten der Mitglieder der Gruppe "Benutzer"
$memberlist=$group.invoke("members")
$memberlist  | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}

Ich habs noch nicht geschafft, die Gruppe "NT-Autorität/Authentifizierte Benutzer" als Variable zu übergeben.

Beispiel 5: Auslesen einer Usereigenschaft aus einem AD mittels WinNT-Provider

$userName="KarlNapf" #SamAccountname
$computer= "DC1"

$user=[ADSI] "WinNT://$computer/$userName"
$user.invokeget("AccountDisabled")
$user.Description

In Kapitel 4.1.2 Auslesen und Verändern von Userproperties folgen umfangreichere Beispiele, um Usereigenschaften auszulesen und zu exportieren. Dort benutze ich den LDAP-Provider

 

Beispiel 6: Auslesen aller lokalen User

$machine=([ADSI]"WinNT://.") 
$users=$machine.children | Where-Object {$_.schemaClassName -eq "User"}
$users | select -expand name

#Details: blogs.technet.com/b/heyscriptingguy/archive/2008/05/22/how-can-i-use-windows-powershell-to-determine-whether-a-local-user-account-exists.aspx

#Ausgabe

MickeyMouse
ASPNET
Hilfeassistent
KarlNapf
SUPPORT_482945a0


Der WinNT bietet eine Reihe von Methoden und Eigenschaften. In der MSDN sind diese aufgeführt 
MSDN: WinNT Custom User Properties

setpassword-Methode:
 MSDN: IADsUser::SetPassword Method

Unterstützte und nicht unterstützte Methoden und Eigenschaften:
 MSDN: IADsUser Property Methods
 MSDN: Unsupported IADsUser Properties 

Beispiel zum Verändern einer lokalen Gruppenmitgliedschaft:

 Technet: Hey, Scripting Guy! How Can I Use Windows PowerShell to Add a Domain User to a Local Group?

Eine Anmerkung zur Sicherheit lokaler Passwörter, speziell des lokalen Administrators:

Hat ein Angreifer physikalischen Zugriff auf einen Windowsrechner (XP, Vista, Windows7, Server2008), so ist es mit den geeigneten Tools kinderleicht, in weniger als 3 Minuten das Passwort des Administrators auf ein beliebiges Passwort zurückzusetzen und sich damit Vollzugriff auf die Maschine zu verschaffen. Gleiches gilt für Domaincontroller.
Gegen diese "Aufsperr-" Tools hilft nur eine Verschlüsselung der Festplatte und physikalischer Zugriffsschutz.
Lange Passwörter oder umbenannte Administratorenkonten verzögern einen Angriffserfolg um keine einzige Zehntelsekunde!

3.2.2 LDAP-Provider

In diesem Kapitel 3 liegt der Schwerpunkt auf dem LDAP-Provider ansich.
In Kapitel 4 liegt dann der Schwerpunkt auf den Objekte im Activedirektory (User, Gruppen, OUs) und wie diese mit LDAP und Powershell erstellt, verändert oder gelöscht werden können.

3.2.2.1 LDAP Connection Strings

Unter 3.1 wurde bereits kurz gezeigt, wie man ein Powershellobjekt auf ein LDAPObjekt im AD "bindet". Ohne grosse Erklärung habe ich dazu schon einen LDAP Connectionstring in den Beispielen verwendet.

In diesem Teilkapitel möchte ich kurz darstellen, aus welchen Komponenten ein solcher String bestehen muss und kann und welche Auswirkungen die Verwendung der teilweise optionalen Komponenten hat.

Allgemein sieht in einem Powershellskript ein vollständiger Connectionstring so aus:

[TypeAccelerator][Provider]://[FQDN oder CN eines DCs]:[Portnumber]/[DN des Objects]

Wobei
[TypeAccelerator]
[Provider]
[DN des Objects]

unbedingt notwendig sind, und

[FQDN oder CN eines DCs]
[Portnumber]

optional sind.

Beispiele aus  MSDN: LDAP ADsPath

LDAP ADsPath example

Description

LDAP:

Bind to the root of the LDAP namespace.

LDAP://server01

Bind to a specific server.

LDAP://server01:390

Bind to a specific server using the specified port number.

LDAP://CN=Jeff Smith,CN=users,DC=fabrikam,DC=com

Bind to a specific object.

LDAP://server01/CN=Jeff Smith,CN=users,DC=fabrikam,DC=com

Bind to a specific object through a specific server.

Wieso im dritten Beispiel der Port auf 390 gesetzt ist, kann ich nicht sagen. Die normalen Portnummern sind natürlich 389 für normales LDAP, 636 für SecureLDAP und 3268, 3269 für GlobalCatalog Bindungen

Als Server benutze ich meistens den PDCemulator der Domäne. Damit vermeidet man Replikationsfallen auch beim Entwickeln und Troubleshooten des Skripts.
Am besten ermittelt man den DomainDN und den PDCe am Anfang des Skripts, oder in einer eigenen Funktion, einmal dynamisch, was die System.DirectoryServices-Klasse leicht hergibt. Beide Variablen benötigt man in ActiveDirectory-Skripten recht häufg. 


Beispiel 1: dynamisches Ermitteln des distinguishedNames der Domäne, des PDCEmulators und des FQDNs der Domäne

$rootDSE = [ADSI]"LDAP://rootDSE"
$domainDN = $rootDSE.defaultNamingContext
"DN:    $domainDN"

$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name
"PDCe: $PDCe"

$DomFQDN=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name #z.B. Dom7.intern
# $DomFQDN=$env:USERDNSDOMAIN #alternativ
"DomFQDN: $DomFQDN"

#Ausgabe

DN:    DC=Dom1,DC=intern
PDCe: DC1.Dom1.intern
DomFQDN: Dom1.intern


oder als Funktion

function getRootDSEandPDCe {
   $rootDSE = [ADSI]"LDAP://rootDSE"
   $fnDomainDN = $rootDSE.defaultNamingContext
   $fnDomainDN

   $fnDomain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
   $fnPDCe=$fnDomain.PdcRoleOwner.Name
   $fnPDCe

   $fnDomFQDN=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name #z.B. Dom7.intern
   $fnDomFQDN
}

$ergebnis=getRootDSEandPDCe

$DomainDN=$ergebnis[0]
$PDCe=$ergebnis[1]
$DomFQDN=$ergebnis[2]

Den FQDN der Domäne habe ich zur Vollständigkeit dazugenommen, obwohl er erstmal nichts mit LDAP-Connectionstrings zu tun hat

Einige Spezialstrings beschreibe ich im folgenden Kapitel näher:

3.2.2.2 Connection auf die Domäne

Beispiel 1:Verbindung auf die Domäne

[ADSI]""
#alternativ: [ADSI]LDAP://<FQDN der Domäne>

 

Beispiel 2: Anzeigen von Domäneneigenschaften
Einige der DomäneneEigenschaften sind ganz interessant, am besten lasst ihr euch mit dem nächsten Zweizeiler diese mal komplett ausgeben.

$DomRoot=[ADSI]""
$DomRoot | fl *

#Ausgabe gekürzt

lockoutDuration                  : {System.__ComObject}
lockOutObservationWindow         : {System.__ComObject}
lockoutThreshold                 : {0}
maxPwdAge                        : {System.__ComObject}
minPwdAge                        : {System.__ComObject}
minPwdLength                     : {7}

Etwas mühsam ist es, dass diverse Eigenschaften nicht als lesbare Strings oder Integerwerte, sondern in verschiedenen Formaten, wie 8 Byte Integerwerten (LargeIntegerValues) gespeichert sind, auf die man mit Powershell nur über einen Umweg Zugriff hat

Unter dem Kapitel Component Object Model habe ich beschrieben, wie man einen Teil dieser 8-bit LargeIntegerWerte trotzdem lesen kann.

3.2.2.3 Connection auf RootDSE

RootDSE ist ein Standard X.500 Objekt, auf das recht einfach zugegriffen werden kann, um Attribute mit Informationen des Directories zu erhalten.


Beispiel 1: Zugriff auf RootDSE

[ADSI]"LDAP://RootDSE" | format-list*  #Serverlose Bindung
#oder mit Angabe eines DCs
[ADSI ]"LDAP://DC1.Dom1.intern/RootDSE " | format-list*

Die beste Definition der RootDSE habe ich hier  MSDN: RootDSE gefunden:
"In LDAP 3.0, rootDSE is defined as the root of the directory data tree on a directory server. The rootDSE is not part of any namespace. The purpose of the rootDSE is to provide data about the directory server. For more information about rootDSE, see  Serverless Binding and RootDSE in the Active Directory SDK documentation."
Zusätzlich werden hier alle Attribute des RootDSE aufgelistet

Abfragen über das verwendete Domänen- und Forestlevel können hilfreich sein, um potentielle Scriptfehler durch zu hoch oder zu niedrig eingestellte Levels abzufangen.
Den defaultNamingContext verwendet man, um sein Script möglichst flexibel in verschiedenen Umgebungen einsetzen zu können

 MSDN: Serverless Binding and RootDSE

Aus Gründen der Ausfallsicherheit sollte bei produktive eingesetzten Skripten möglichst kein fester Server (=serverless) für die LDAP-Bindung  verwendet werden. Andererseits ist für das Troubleshooting mittels Netzwerkanalyse bequemer, wenn man genau den Domaincontroller ansteuert, auf dem Tools wie Netmon oder Wireshark laufen.


Beispiel 2: Ermitteln einiger LDAP-Eigenschaften des RootDSE

$DSE=[ADSI]"LDAP://RootDSE"
$DSE.currenttime
$DSE.defaultNamingContext
$DSE.domainfunctionality

#Ausgabe

20100720211141.0Z
DC=dom1,DC=intern
3


Möchte man mit der Zeit weiterarbeiten, so kann man den Zeitstring mit $DSE.substring(0,4) etc. nach Jahr, Datum und Uhrzeit aufsplitten und mit

get-date <Tag>,<Monat>,<Jahr>

in ein ZeitDatumsobject umwandeln. Man muss nur mit der von Windows und LDAP verwendeten Zeitzone [UTC] aufpassen, die eine Verschiebung der Zeit um eine Stunde im Winter und zwei Stunden im Sommer zur Folge hat.

3.2.2.4 Kerberos / NTLM Authentifizierung

Zur Authentifizierung an einer Domäne ist bereits seit Windows 2000 Kerberos das Standardprotokoll. NTLMv2 kann nach wievor genutzt werden, hat aber den ein- oder anderen Nachteil, wie zum Beispiel, dass es nicht ticketbasiert arbeitet und damit eine Authentifizierung bei jedem ResourcenZugriff übers Netz neu erfolgen muss.

Wenn nichts dagegen spricht, sollte man seine Skripte also so verfassen, dass diese Kerberos benutzen

Beispiel 1: Bindung an RootDSE über das Kerberosprotokoll
Will man sicher gehen, dass das Powershellskript Kerberos zur Domänenauthentifizierung benutzt, verwendet man den FQDN eines Domaincontrollers im LDAP-Pfad

$DSE=[ADSI]"LDAP://DC1.Dom1.Intern/RootDSE"
# oder wenn Windows den einfachen Namen zum FQDN ergänzt auch
$DSE=[ADSI]"LDAP://DC1/RootDSE"  


Beispiel 2: Bindung an RootDSE über das NTLMProtokoll
Gibt man im Connectionstring keinen Server an, so sucht sich der Client einen zufälligen Domaincontroller über DNS-Mechanismen und authentifiziert sich am DC über NTLM. Ebenso erfolgt eine NTLM-Authentifizeirung, wenn nicht der Rechnername sondern die IPAdresse verwendet wird.

$DSE=[ADSI]"LDAP://RootDSE"
# oder
$DSE=[ADSI]"LDAP://192.168.1.2/RootDSE"

In einem Netzwerktrace, den man beispielsweise mit Wireshark oder dem MS-Netzwerkmonitor erstellen kann, sieht man den Effekt, wenn man die LDAP-Pakete "Bindrequest" und "Bindresponse" öffnet. Um LDAP-Pakete anzusehen, muss man im Netmon zuerst einen geeigneten Parser installieren, was in Kapitel 5.5.2 beschrieben ist.

Diesen Hintergrund zu kennen, ist beim Troubleshooting hilfreich: Eine Abfrage funktioniert mit IPAdresse einwandfrei, eine Abfrage auf den Servernamen schlägt hingegen fehl (oder umgekehrt). Meist wird auf ein DNS-Namensauflösungsproblem getippt, die Ursache kann aber auch in den unterschiedlichen Authentisierungsprotokollen liegen.

 

3.3 Useraccountcontrol / UserFlags

Im oberen Beispiel aus 3.2.1 zur Userneuanlage mit dem WinNT Provider habe ich gezeigt, wie man einige Usersettings (AccountDisabled, PasswordExpires) mit Powershellmitteln setzen kann.
Meist steuert man die Usereigenschaften (ActiveDirectory User genauso wie lokale User) jedoch über die UserFlags-Eigenschaft beim WinNT-Provider oder der Useraccountcontrol-Eigenschaft beim LDAP-Provider. Zum Setzen nutzt man oft hexadezimale Werte, was am Anfang etwas kryptisch erscheint. Mit ein bischen Übung und Erfahrung erkennt man jedoch die Vorteile.

Die Ziffern einer Hexadezimalenzahl können im Gegensatz zu einer Dezimalzahl 16 (0 bis F) statt 10 (0 bis 9) Werten annehmen. In Powershell werden Hexzahlen durch das Prefix "0x" gekennzeichnet.

$myFlag=0x10
$myFlag

#Ausgabe
16 #Dezimal

$myFlag=32
"{0:x}" -f $myFlag
#[convert]::tostring(32,16)  

#Ausgabe
20 #Hexadezimal


Der UserAccountcontrol Eigenschaft eines Users ist es vollkommen egal, ob er vom Programmierer mit dezimalen oder hexadezimalen Zahlen versorgt wird. Einem Flag in Hexschreibweise sieht man nur wesentlich leichter an, welche Contstanten in ihm enthalten sind.

Die MSDN listet folgenden Werte auf, die sowohl für die Userflags Property des WinNT-Providers, wie auch der Useraccountcontrol Property des LDAP-Providers gelten.
 MSDN: ADS_USER_FLAG_ENUM Enumeration

Konstante

Dez

Hex

ADS_UF_SCRIPT

1

 0x1

ADS_UF_ACCOUNTDISABLE

2

 0x2

ADS_UF_HOMEDIR_REQUIRED

8

 0x8

ADS_UF_LOCKOUT

16

 0x10

ADS_UF_PASSWD_NOTREQD

32

 0x20

ADS_UF_PASSWD_CANT_CHANGE

64

 0x40

ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED

128

 0x80

ADS_UF_TEMP_DUPLICATE_ACCOUNT

256

 0x100

ADS_UF_NORMAL_ACCOUNT

512

 0x200

ADS_UF_INTERDOMAIN_TRUST_ACCOUNT

2048

 0x800

ADS_UF_WORKSTATION_TRUST_ACCOUNT

4096

 0x1000

ADS_UF_SERVER_TRUST_ACCOUNT

8192

 0x2000

ADS_UF_DONT_EXPIRE_PASSWD

65536

 0x10000

ADS_UF_MNS_LOGON_ACCOUNT

131072

 0x20000

ADS_UF_SMARTCARD_REQUIRED

262144

 0x40000

ADS_UF_TRUSTED_FOR_DELEGATION

524288

 0x80000

ADS_UF_NOT_DELEGATED

1048576

 0x100000

ADS_UF_USE_DES_KEY_ONLY

2097152

 0x200000

ADS_UF_DONT_REQUIRE_PREAUTH

4194304

 0x400000

ADS_UF_PASSWORD_EXPIRED

8388608

 0x800000

ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION

16777216

 0x1000000

 

Beispiel1: Setzen eines Usersettings über WinNT und LDAP

Alle User einer ActiveDirectoryGruppe sollen das Usersetting bekommen, ein Smartcard benutzen zu müssen. Bereits bestehende Usersettings wie Accountdisabled sollen nicht verändert werden.
Ohne Skript muss ein Administrator jeden User einzeln aufrufen und die Eigenschaft "User muss sich mit Smartcard anmelden" zusätzlich auswählen:

Der Lösungsweg besteht darin, den bestehenden Wert des Userflags- oder UserAccountControl Attributs auszulesen und -falls noch nicht vorhanden- den Value ADS_UF_SMARTCARD_REQUIRED 0x40000 hinzuzufügen. Was sich nach fehleranfälligen IF-Abfragen anhört, lässt sich mittels einem Booleanschen BinaryOR (-bor) sehr einfach lösen.

Die beiden nächsten Skripte arbeiten identisch. Beim WinNT Provider genügt die Angabe des SAMAccountnames zur Bindung an das Userobjekt, der LDAP-Provider verlangt den ausführlichen DistinguishedName. Dafür ist der LDAP-Provider schneller und bietet mehr Möglichkeiten, wie zum Beispiel das Verwenden von Credentials oder das Verwenden der Kerberosauthentifizierung.

#1: WinNT-Provider

$ADS_UF_SMARTCARD_REQUIRED = 0x40000 # aus der MSDN

$userName="KarlNapf"
$computer="DC1" #in einer Domäne am besten den PDC-Emulator nutzen

$user=[ADSI] "WinNT://$computer/$userName"

$oldFlags = $user.invokeGet("UserFlags") #Eigenschaft des WinNT-Providers
$newFlags = $oldflags -bor $ADS_UF_SMARTCARD_REQUIRED
$user.invokeset("UserFlags",$newFlags)

$user.setinfo()

"Alter Useraccountwert: {0:x}" -f $oldflags
"Neuer Useraccountwert: {0:x}" -f $newflags

#2: LDAP-Provider
$ADS_UF_SMARTCARD_REQUIRED = 0x40000 # aus der MSDN

$CredentialUser="dom1\administrator"
$CredentialPW='P@ssword12'
$userNameDN="CN=Karl Napf,OU=User,OU=scripting,DC=Dom1,DC=intern"
$computer="DC1.dom1.intern" #in einer Domäne am besten den PDC-Emulator nutzen
                            #der DNS-Name erlaubt eine Kerberosauthentifizierung

$user=[ADSI] "LDAP://$computer/$userNameDN"
$user.username=$CredentialUser
$user.password=$CredentialPW

$oldUAC = $user.invokeGet("UserAccountControl") #Eigenschaft des LDAP-Providers
$newUAC = $oldflags -bor $ADS_UF_SMARTCARD_REQUIRED
$user.invokeset("UserAccountControl",$newUAC)

$user.setinfo()

"Alter UAC: {0:x}" -f $oldUAC
"Neuer UAC: {0:x}" -f $newUAC

#Ausgabe bei beiden Providern

Alter Useraccountwert: 10241
Neuer Useraccountwert: 50241

 

Beispiel 2: Ermitteln aller User eines Directories, deren Account gesperrt ist und die sich gleichzeitig  mit Smartcard anmelden müssen

In diesem Beispiel wird gezeigt, wie man mit einem BinaryAnd "-band" sehr einfach ein oder mehrere Usersettings auslesen kann.

$ADS_UF_SMARTCARD_REQUIRED = 0x40000
$ADS_UF_ACCOUNTDISABLE = 0x2

$filter = "objectClass=user"
([adsiSearcher]$filter).findall() | foreach-object {
   $uac = ([adsi]$_.path).invokeget("UserAccountControl")
   if(($uac -band 0x2) -and ($uac -band $ADS_UF_SMARTCARD_REQUIRED)){
      write-host "$($_.properties.item("distinguishedName")) ist gesperrt und Smartcardanmeldung ist erforderlich"
   }
 }

#Ausgabe

CN=User1,OU=User,OU=scripting,DC=Dom1,DC=intern ist gesperrt und Smartcardanmeldung ist erfoderlich

Die entscheidende Zeile des Skripts oben ist:

if(($uac -band 0x2) -and ($uac -band $ADS_UF_SMARTCARD_REQUIRED)){

die bestehende Eigenschaft "UserAccountControl" eines jeden Benutzers wird sowohl mit dem HexWert 2 und dem HexWert 40000 binär verglichen. Natürlich kann man je nach Vorliebe ebenso Dezimalzahlen verwenden und statt der Variablen $ADS_UF_SMARTCARD_REQUIRED auch den Dezimalwert 262144 in dieser Zeile schreiben.

Anmerkung 1: Auf den [ADSISearcher] gehe ich weiter unten in Kapitel 5.2 noch tiefer ein.

Anmerkung 2: Anstelle der Booleschen Operatoren -band und -bor wird oft mit dem LDAPControl LDAP_MATCHING_RULE_BIT_AND 1.2.840.113556.1.4.803 und LDAP_MATCHING_RULE_BIT_OR 1.2.840.113556.1.4.804 gearbeitet. Siehe dazu Kapitel 5.2.3.4

 

 

4 Klassen

In Windows 2008 gibt es mittlerweile 228 Klassen, die im ActiveDirectory definiert sind. Eine Aufstellung dieser Klassen liefert:

 MSDN: All Classes

In den verzweigenden Sublinks zu jeder Klasse wird jede Klasse in Abhängigkeit vom verwendeten Betriebssystem (Win2000 bis Win2008) detailliert beschrieben.

Beispiel:  MSDN: Computer Class

Bevor man anfängt, sich durch den ActiveDirectory Schemaeditor zu kämpfen um Klassen zu erforschen, ist man meiner Meinung nach auf diesen Seiten besser aufgehoben.

In diesem Kapitel 4 werde ich ausschließlich den LDAP-Provider nutzen. Erklärungen und Beispiele für den WinNT-Provider findet ihr in Kapitel 3.2.1

Ebenso werde ich in diesem Kapitel nur solche Methoden verwenden, die keine zusätzlichen Module oder Plugins wie von  Quest oder  Codeplex benötigen und somit nativ unter allen ActiveDirectory-Umgebungen ab 2003/XP aufwärts mit Powershell V2.0 einsetzbar sind.
Einfache Portierbarkeit, Support, Stabilität und die Vermeidung von Abhängigkeiten erscheinen mir wichtiger, als eine mögliche Vereinfachung des Scriptens durch Zusatzmodule.

Das  Active Directory Module for Windows PowerShell von Microsoft werde ich in einem späteren Kapitel behandeln.

4.1 Userklasse

Eigenschaften begegnen einem Administrator wahrscheinlich das erste Mal im AD-Userinterface, der sogenannten ADUC. Und möglicherweise entsteht hierbei auch zum ersten Mal der Wunsch nach einer Automation per Skript, um vielleicht bei mehreren 100 Usern eine bestimmte Eigenschaft ohne Tipparbeit zu verändern.

Die ADUC zeigt aber keinesfalls alle Eigenschaften eines Users an und die angezeigten Eigenschaftsnamen am Bildschirm (=Displaynames) stimmen oft nicht mit den Eigenschaftsnamen überein, die  tatsächlich im ActiveDirectory hinterlegt sind und die man beim Skripten benutzen kann.

Um an die tatsächlichen Attributnamen zu kommen, die man beim Skripten braucht, hilft ein Blick in einen LDAP-Editor wie ADSIEdit.msc, oder den Hardcoreeditor LDP.exe

Eine Aufstellung aller Methoden und Attribute der Userklasse findet man unter
 MSDN: User Class,  MSDN: IADSUser Interface sowie für Terminalservereigenschaften unter  MSDN: IADsTSUserEx Interface. Es gibt also für viele Attribute aus beiden Interfaces zwei Namen zum Skripten von ein- und derselben Eigenschaft (Beispiel: givenname oder firstname). In den Anmerkungen des 2. Besispiels zur Useranlage im nächsten Kapitel gehe ich ein bischen näher darauf ein.

Verwirrenderweise wird ausserdem in der Powershellwelt von Properties also Eigenschaften gesprochen, in der ActiveDirectorywelt dagegen von Attributen.

Beispiel 1: Anzeige aller MultivaluedAttribute der Userklasse
Natürlich kann man sich auch selbst ein Bild per Skript verschaffen

$Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
$SchemaUser = $schema.FindClass('user')
$SchemaUserAttributes=$SchemaUser.get_MandatoryProperties() + $SchemaUser.get_OptionalProperties()

$SchemaUserAttributes | ? {$_.IsSingleValued -eq $False} |  ft name,IsSingleValued,IsIndexed,IsInGlobalCatalog
"die Userklasse hat $($SchemaUserAttributes.count) mandatory und optional Attributes. Davon sind diese Multivalued"

#Ausgabe gekürzt
"die Userklasse hat $($SchemaUserAttributes.count) mandatory und optional Attributes. Davon sind diese Multivalued"

Name                IsSingleValued   IsIndexed    IsInGlobalCatalog
----------          --------------   ----------   -----------------
objectClass                  False         True                True
accountNameHistory           False        False               False
allowedAttributes            False        False               False


Beispiel 2: Abfrage, ob eine Eigenschaft Multi- oder SingleValued ist
Da ich in einem späteren Beispiel dynamisch abfragen will, ob ein Eigenschaft Single- oder Multivalued ist, erstelle ich hier schonmal eine entsprechende Funktion

function isUserpropertySinglevalued{
   param($propertyname)
   $Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
   $SchemaUser = $schema.FindClass('user')
   $SchemaUserAttributes=$SchemaUser.get_MandatoryProperties() + $SchemaUser.get_OptionalProperties()
   $HTschemauserattributes=@{} #leere Hashtable

  $schemauserattributes | foreach {
    $HTschemauserattributes[$_.name]=$_.issinglevalued
    } #foreach
    $return=$HTschemauserattributes[$propertyname]
    $return
    }

   isUserpropertySinglevalued "cn"
   isUserpropertySinglevalued "memberof"

#Ausgabe
True
False

 

4.1.1 Anlage von Usern

Die Useranlage mittels Powershell über [ADSI] ist linde ausgedrückt "etwas vewirrend".

Es gibt zum ersten mehrere Syntaxvarianten um die Eigenschaft eines Users zu setzen:

$ADSI=[ADSI]"LDAP://$oupath"
$user = $ADSI.create($class,$cnuser)

$user.<Eigenschaft> = "Wert"
$user.put("<Eigenschaft>,"Wert")
$user.invokeset("<Eigenschaft>,"Wert")
$user.invoke("
<Eigenschaft>,"Wert")
$user.psbase.invoke("<Eigenschaft>,"Wert")
$user.psbase.invokeset("<Eigenschaft>,"Wert")
$user.<ArrayEigenschaft>="Wert1","Wert2"
$user.properties.item("<Eigenschaft>,"Wert")
$user.properties["<ArrayEigenschaft>].add("Wert1","Wert2"]

..es gibt sicher noch weitere

Manchmal funktionieren mehrere Syntaxvarianten, manchmal nur eine. Manchmal funktioniert die eine Schreibweise für ein und dieselbe Eigenschaft nur, wenn der Account bereits mit $user.commitchanges() angelegt wurde, wie beispielsweise $user.description. Mit $user.invokeset("Description","Beschreibung") kann die Beschreibung dagegen schon vor der Useranlage definiert werden.

Außerdem besitzen User Eigenschaften, die als kombinierter Hexadezimalwert zusammen in der sogenannten Useraccountcontrol (Kapitel 3.3) und nicht als einzelne Eigenschaft abgespeichert werden. Die wichtigste davon ist wahrscheinlich "accountdisabled", oder auch "smartcardrequired". Die meisten UAC-Eigenschaften muss man als Wert setzen, Accountdisabled kann man auch direkt über invokeset setzen, da diese Methode im "IADsUser Interface" enthalten ist. Siehe "Beispiel 2" wieter unten

In meinem folgenden Beispiel benutze ich wenn möglich die Definition über $user.invokset("..."), da diese Schreibweise bei der Mehrzahl der Eigenschaften funktioniert.
Ich würde die Syntax innerhalb eines Skripts nicht unnötig vermischen.


Beispiel 1: Massenanlage von Testusern
dieses Beispiel legt in einem (hoffentlich) TestAD 2.000 UserAccounts von C15000 bis C16999 in der OU "OU=Benutzer,OU=Scripting,$domainDN" an. Zum Testen von LDAP-Filtern, wie in Kapitel 5 beschrieben, sind solche Versuchsdaten oft nützlich.

$domainDN = ([ADSI]"LDAP://rootDSE").defaultnamingcontext
$DomFQDN=$env:USERDNSDOMAIN

for ($i=15000;$i -le 16999;$i++){

  #Teil 0 Usereigenschaften definieren
  $name="C$i"
  $CNUser="CN=$name" #$CNUser="CN=Napf Karl" #Achtung: das "CN=" vegisst man leicht
  $oupath="OU=Benutzer,OU=Scripting,$domainDN"
  $givenname="gn$name"
  $sn="sn$name"
  $description="de$name"
  $class = "user"
  $samaccountname=$name
  $userprincipalname="$samaccountname@$DomFqdn"
   
  #Teil 1 User anlegen
  $userprincipalname #Bildschirmausgabe
  $ADSI=[ADSI]"LDAP://$oupath"
  $user = $ADSI.create($class,$cnuser)
  $user.invokeset("SamaccountName", $samaccountname)
  $user.commitchanges()
 
  #Teil 2 Eigenschaften setzen
  $user.invokeset("userprincipalname",$userprincipalname)
  $user.invokeset("givenname",$givenname)
  $user.invokeset("sn",$sn)
  $user.invokeset("description",$description)
  $user.invoke("setPassword","Pa$$word")
 
  #Teil 3 Eigenschaften der UAC setzen
  $user.invokeset("accountdisabled",$false) #alternative zu Useraccountcontol
  $user.commitchanges()
}

Lässt man die For-Schleife weg und setzt für den $name einen festen Wert ($name="Napf Karl"), so hat man ein einfaches Beispiel für eine Useranlage, an dem man das prinzipielle Vorgehen gut ablesen kann und das man wie in den weiteren Beispielen gezeigt, gut erweitern kann.


Beispiel 2: Anlage eines Users (mit vielen Properties)

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext #z.B. Dc=Dom7,DC=intern
$DomFQDN=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name #z.B. Dom7.intern
# $DomFQDN=$env:USERDNSDOMAIN #alternativ

#Teil 0 , Usereigenschaften definieren
#Basisinformationen für die initiale Anlage
$sn="Napf"
$givenName="Karl"
$CNUser="CN= $SN $givenName"
$OuPath="OU=Benutzer,OU=Scripting,$domainDN"
$Class ="User"

#Eigenschaften des Users
$samaccountname="A90005"
$userprincipalname="$samaccountname@$domfqdn"
$userworkstations= "PC1,PC2,PC3"

$description="Bananenbieger und Tulpenknicker"
$telephonenumber="001-288-12444"
$othertelephone="005-133-12444","005-133-12445"
$mail="Karl.Napf@company.intern"

$HomeDirectory="\\Fileserver2\Homes\%username%"
$ProfilePath="\\Fileserver2\Profiles\%username%"
$ScriptPath="\\Fileserver2\Logonscripts\%username%"

$Terminalservicesprofilepath="\\FileServer1\TSProfile\%username%"
$TerminalServicesHomeDirectory="\\FileServer1\TSHomes\%username%"
$TerminalServicesWorkDirectory = "c:\temp\workdrive"

$homephone="001-289-34567"
$otherhomephone="001-289-7142585","001-289-5602310"

$title="Vorstandsvorsitzender"
$department="Vorstand"
$streetAddress="Am Acker 9"
$postalcode="04711"

$company="Company"
$mobile="002-2347212"

$InitialPassword="Pa$$word123"
$pwdlastset=-1 #User muss PW nicht bei der ersten Anmeldung ändern

#UseraccountControls (siehe Kapitel 3.3)
$ADS_UF_ACCOUNTDISABLE = 0x00002
$ADS_UF_SMARTCARD_REQUIRED = 0x40000

#Teil 1 Useranlage
$ADSI=[ADSI]"LDAP://$oupath"
$user = $ADSI.create($CLass,$cnuser)
$user.invokeset("SamaccountName", $samaccountname)
$user.commitchanges()

#Teil 2 Usereigenschaften setzen
$user.invokeset("userprincipalname", $userprincipalname)
$user.properties["userworkstations"].Add("$userworkstations") #$user.invokeset("userworkstations",$userworkstations) #geht nicht

$user.invokeset("sn",$sn)
$user.invokeset("givenname",$givenname)
$user.invokeset("description",$description)
$user.invokeset("telephonenumber",$telephonenumber)
$user.othertelephone=$othertelephone #mit $user.invokeset("othertelephone".$othertelephone) scheitert man
$user.invokeset("mail",$mail)

$user.invokeset("Homedirectory",$homedirectory)
$user.invokeset("profilepath",$profilepath)
$user.invokeset("scriptpath",$scriptpath)

$user.invokeset("Terminalservicesprofilepath",$terminalservicesprofilepath)
$user.invokeset("TerminalServicesHomeDirectory",$TerminalServicesHomeDirectory)
$user.invokeset("TerminalServicesWorkDirectory",$TerminalServicesWorkDirectory)

$user.invokeset("title",$title)
$user.invokeset("department",$department)

$user.invokeset("streetAddress",$streetAddress)
$user.invokeset("postalcode",$postalcode)
$user.invokeset("company",$company)
$user.invokeset("mobile",$mobile)
$user.homephone=$homephone #mit $user.invokeset("homephone".$homephone) scheitert man
$user.otherhomephone=$otherhomephone

$user.setpassword($initialpassword)
#$user.invoke("SetPassword",$initialpassword) #alternativ
#$user.invokeset("userpassword",$initialpassword) setzt nicht!!! das AD-Password
$user.invokeset("pwdlastset",$pwdlastset)

#Teil 3
# Eigenschaften der UAC setzen
$ADS_UF_SMARTCARD_REQUIRED = 0x40000 # aus der MSDN
$ADS_UF_ACCOUNTDISABLE = 0x00002
$user.invokeset("UserAccountControl",0x40220) #ein initialer User hat die UAC 0x222
$User.commitchanges()


Anmerkungen zur Portierbarkeit zwischen den WindowsVersionen

a) Das Skript oben läuft so wie es ist unter WindowsXP, Windows2003 oder Windows2008. 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


b) Auf das Setzen des SamAccountnames muss unmittelbar ein commitchanges() oder setinfo() folgen, sonst vergibt AD einen zufälligen Wert. Dieses Verhalten war in meiner XP/ 2003-er Testumgebung zu sehen, unter Win7/ 2008 verhielt sich Powershell flexibler.


Anmerkungen zur Auswahl der richtigen Namen für die Eigenschaften und Methoden

a) man benutzt den sogenannten LDAP-Display-Name des Attributs oder der Methode, den man (meistens!) über ADSIEdit.msc herausfinden kann.
Unter  MSDN: User Class sind (die meisten!) AD-Attribute der Userklasse aufgelistet: Unter dem Attribut Surename für Nachname findet sich zum Beispiel der LDAP-Display-Name "SN".

b) Die Ausnahmen von a) sind Attribute und Methoden des Remotedesktopservices wie Terminalservice, RDP und Printer. Diese werden nicht von ADSIEdit.msc angezeigt und sind auch nicht auf der eben genannten MSDN-Seite zu finden. Man muss sich die Attributnamen von einer speziellen MSDN für "Remote Desktop Services" besorgen: 
 MSDN: IADsTSUserEx Interface

c) viele Usereigenschaften und Methoden, die zwar dasselbe Attribut oder diesselbe Methode wie unter a) bedeuten, aber einen anderen Namen besitzen, findet man unter
 MSDN: IADsUser Interface

Zur Verdeutlichung habe ich ein paar Entsprechungen zwischen a) und c) zusammengestellt, die ergebnisgleich verwendet werden können. Ob das für für alle Entsprechungen gilt, weiss ich nicht!

LDAP-DisplayName

Eigenschaften und Methoden des IADsUser Interface

sn

lastname

givenname

firstname

commitchanges()

setinfo()

useraccountcontrol

0x00002

accountdisabled()


Ich denke, aus dem Beispiel oben, ADSIEdit.msc und den genannten MSDN-Seiten kann man sich das Useranlageskript so zusammenbauen, so dass alle gewünschten Properties befüllt werden.


Beispiel 3: Useranlage mit Daten aus einer csv-Datei

Liegen die Userdaten in einer Exceltabelle oder im CSV-Format vor, so lassen sich diese Daten über das cmdlet "import-csv" importieren. Mit einer foreach-Schleife wird jede Zeile der Datei in die Variable $_ eingelesen, auf die Werte jeder Spalte kann mit $_.<spaltenüberschrift> zugegriffen werden.
Da Daten fehlerhaft sein können, habe ich noch eine einfache Fehlerbehandlung mit Try-Catch eingebaut.

csv-Datei:

Nummer;sn;givenname;cn;oupath;samaccountname;description;userpassword;accountdisabled
1;Napf;Karl;A90001;OU=BenutzerC,OU=Scripting,DC=Dom7,DC=intern;A90001;Bananenbieger und Tulpenknicker;Pa$$word123;true
2;Napf;Franz-Josef;A90002;OU=BenutzerC,OU=Scripting,DC=Dom7,DC=intern;A90002;Sohn von Karl Napf;Pa$$word124;true
3;Napf;Fred;A90003;OU=BenutzerC,OU=Scripting,DC=Dom7,DC=intern;A90003;;Pa$$word125;false
4;Napf;Heinz;A90004;OU=BenutzerC,OU=Scripting,DC=Dom7,DC=intern;A90004;Bruder von Karl Napf;Pa$$word126;false

Powershellcode

$userproperties=Import-Csv "c:\powershell\user.csv” -delimiter ";"
$userproperties | foreach {

  try {
     $samaccountname=$_.samaccountname #wird im Catchblock zur Ausgabe der Fehlermeldung verwendet
     $nummer=$_.nummer #$_.nummer: der Eigenschaftsname "nummer" von $_ kommt aus der Überschrift der CSV-Datei 
 
     #Teil 1: Useranlage
     $class = "user"
     $ADSI=[ADSI]"LDAP://$($_.oupath)" #$($_.oupath) , so wird der Ausdruck in der Klammer zuerst aufgelöst
     $user = $ADSI.create($class,"CN=$($_.cn)") #CN=$($_.cn) , so wird der Ausdruck in der Klammer zuerst aufgelöst
     $user.invokeset("SamaccountName", $_.Samaccountname)
     $user.commitchanges()

     #Teil 2 Usereigenschaften setzen
     if ($_.description -ne ""){    #der Versuch ein leeres Feld setzen erzeugt einen Fehler
         $user.invokeset("description", $_.description)
     }
     $user.invokeset("sn",$_.sn)
     $user.invokeset("givenname",$_.givenname)
     $user.invokeset("userpassword",$_.userpassword)
 
     #Teil 3 Eigenschaften der UAC setzen
     $user.invokeset("accountdisabled",$_.accountdisabled) #
     $user.commitchanges()
 
  } #try-Block

  catch {
     write-host -foregroundcolor red "+++++++++++++++++++++++++++++++++++++++"
     "bei der laufenden Nummer $Nummer $Samaccountname trat folgendes Problem auf: "
     $error[0]  #$error enthält die letzten 256 Errormeldungen, $error[0] die aktuellste
  } #catchblock
} #foreach-Schleife

Zur Übersichtlichkeit habe ich hier nur wenige Userattribute verwendet. Im zweiten Beispiel dieses Kapitels sind die Attribute ausführlich behandelt.

4.1.2 Auslesen und Verändern von Userproperties

Im Beispiel 5 von Kapitel 3.2.1 weiter oben, habe ich das Auslesen von Usereigenschaften über den WinNT-Provider beschrieben, der auch mit lokalen Userkonten funktioniert.

Die folgenden Beispiele sind etwas länger geraten, als in den anderen Kapiteln üblich. Hier gehts eher um Skripte, die man in der Praxis anwenden kann, denn um die Demonstration bestimmter Powershellelemente.

Zum Testen empfiehlt es sich, ein paar Testuser anzulegen wie im Kapitel 4.1.1 weiter oben beschrieben ist.


Beispiel 1: Auslesen von Userkonten, die sich seit xx-Tagen nicht mehr an der Domäne angemeldet haben

function ConvertADSLargeInteger([object] $adsLargeInteger)
{
$highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
$lowPart = $adsLargeInteger.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)

$bytes = [System.BitConverter]::GetBytes($highPart)
$tmp = [System.Byte[]]@(0,0,0,0,0,0,0,0)
[System.Array]::Copy($bytes, 0, $tmp, 4, 4)
$highPart = [System.BitConverter]::ToInt64($tmp, 0)

$bytes = [System.BitConverter]::GetBytes($lowPart)
$lowPart = [System.BitConverter]::ToUInt32($bytes, 0)

return $lowPart + $highPart
# Funktion von bsonposh.com
}

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

#hier die Parameter anpassen
$OUdn="OU=BenutzerA,OU=scripting" #User werden aus dieser OU gelesen
$maxAgeInDays=90

# Ab hier beginnt der Code
$userContainer = [ADSI]"LDAP://$OUdn,$domainDN"
$user=$userContainer.children | ?{$_.objectclass[1] -eq "person"} #nur user werden exportiert
$user | foreach{ #jeder User im $userContainer

try{
$cn=$_.invokeget("cn")
$llts=[datetime]::FromFileTimeUtc((ConvertADSLargeInteger $_.invokeget("lastlogontimestamp")))

}
catch
{
}
if($((Get-date)-$llts).days -gt $MaxAgeInDays){

#$_.invokeset("accountdisabled",$True)
#$_.commitchanges()
$IsDisabled=$_.invokeget("AccountDisabled")
"{0} {1} {2}" -f $cn,$llts,$isDisabled
}

} #user

solange die Zeile am Ende

#$_.invokeset("accountdisabled",$True)
#$_.commitchanges()

auskommentiert bleiben, zeigt das Skript alle Userkonten an, die über 90 Tage (=$MaxAgeInDays) nicht mehr benutzt wurden. Nimmt man die beiden Befehle mit ins Skript auf, werden diese Konten auch disabled.

Beispiel 2a: Exportieren von Usern aus einer OU in eine csv-Datei, ohne SubOUs mit Eigenschaften
                    aus dem AD-Userinterface

function isUserpropertySinglevalued{
   #Abfrage ob eine Eigenschaft Multi- oder Singlevalued ist
   param($propertyname)
   $Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
   $SchemaUser = $schema.FindClass('user')
   $SchemaUserAttributes=$SchemaUser.get_MandatoryProperties() + $SchemaUser.get_OptionalProperties()
   $HTschemauserattributes=@{} #leere Hashtable

  $schemauserattributes | foreach {
    $HTschemauserattributes[$_.name]=$_.issinglevalued
    } #foreach
    $return=$HTschemauserattributes[$propertyname]
    $return
    } #function
   

#ADTeil
$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Props=""

#ab hier die Parameter anpassen
$Containerdn="OU=BenutzerA,OU=scripting,$domainDN" #User werden aus dieser OU gelesen

#als erste Eigenschaft am besten immmer den distinguishedname oder zumindest eine Singlevalued Eigensschaft
$Props="distinguishedname","cn","sn","givenname","mail","Terminalservicesprofilepath"#,
               #"description","telephonenumber","mail","homedirectory","profilepath",
               #"scriptpath","Terminalservicesprofilepath","TerminalServicesHomeDirectory",
               #"TerminalServicesWorkDirectory","homephone","title","department",
               #"streetAddress","postalcode","mobile","useraccountcontrol",
               #"memberof","othertelephone","otherhomephone"
$MVDelimiter="#"  #Multivalued Properties werden mit diesem Delimiter untereinander getrennt
$CSVdelimiter=";" #Delimiter zwischen den Properties
$filepath="c:\temp\userexport.csv"
$Bildschirmausgabe=$true

#ab hier beginnt der eigentliche Code

#CSV-Datei neu anlegen
new-item -path $filepath -itemtype File -force | out-null
$output=""

#Header in der csv-Datei
for($propsCounter=0;$propsCounter -lt $props.count;$propsCounter++){
   $output+=$CSVdelimiter+$props[$propsCounter]
   }#for
  
$output=$output.substring(1,$($output.length)-1) 
$output | out-file -filepath $filepath -encoding Default -append
if ($Bildschirmausgabe -eq $true){$output}

#Datenssätze schreiben
$output=""
$userContainer = [ADSI]"LDAP://$Containerdn"
$user=$userContainer.children | ?{$_.objectclass[1] -eq "person"} #nur user werden exportiert
$usercounter=1
$user | foreach{  #jeder User im $userContainer
   $usercounter++ #jeder User kommt in neue Zeile
   $output=""  
   $output=$_.invokeget($props[0]) #erste Spalte bitte immer den DN
   for($propsCounter=1;$propsCounter -lt $props.count;$propsCounter++){
     #$propsCounter: jede neue Eigenschaft in eine neue Spalte
       if (isUserpropertySinglevalued $props[$propsCounter]  -eq $true) {
         #Schreiben der Singlevalue-Props in die csv-datei
         try{
           $output+=";"+$_.invokeget($props[$propsCounter])+";"#$csvdelimiter
           } #catch
         catch{
          $output+=$csvdelimiter
          #leere Eigenschaften wie Terminalservicesprofilepath verursachen Fehler
             }#catch
         finally{   
             $output=$output.substring(0,$($output.length)-1)
             #ein csvdelimiter am Ende ist zuviel
         } #finally
       } else {
         #Schreiben der Multivalued-Props in die csv-datei
         try{
          $MV=""
          $MV=$_.invokeget($Props[$PropsCounter])
          $MVValue=""
          foreach ($value in $MV) {
            $MVvalue+=$value+$MVDelimiter #jeder Wert des MV-Feldes wird in den String MVvalue geschrieben.
                   } #foreach 
            $output+=$CSVDelimiter+$MVvalue
            } #try
       catch{
          $output+=+$csvdelimiter
          #leere MVEigenschaften wie Terminalservicesprofilepath verursachen Fehler
         
            }#catch
       finally{   
         $output=$output.substring(0,$($output.length)-1)
         #ein csvdelimiter am Ende ist zuviel
         } #finally
      } #else if
     
       # if
     } #for   
            
      $output | out-file -filepath $filepath -encoding Default -append
      if ($Bildschirmausgabe -eq $true){$output}
   }#user foreach
  
#Update 18.10.2010
#Das Skript erkennt jetzt selbstständig, ob Properties ein
#oder mehrere Werte enthalten können. (Multi/ Singlevalued)

#Upadate 24.10.2010
#Fehler bei Multivalues korrigiert

Anmerkungen:

Das Attribut memberof enthält die Gruppenmitgliedschaften der ersten Ebene.

Um auf Terminalserver-oder Remote Desktop Attribute zugreifen zu können, muss ab Windows7/ Server 2008 die tsuserex.dll mittels regsvr.exe tsuserex.dll registriert sein. Siehe auch 4.1.1 Anlage von Usern Beispiel 2.

Bei $user=$userContainer.children gibt es leider keine Möglichkeit, auch die Unterverzeichnisse mit einzubeziehen. Falls dies gewünscht ist, so muss der [ADSIsearcher] benutzt werden, wie dies im folgenden Beispiel 1b gezeigt wird.

Ein Beispiel zum Import einer CSV-Datei findet ihr unter 4.1.1 Anlage von Usern im Beispiel 3 "Useranlage mit Daten aus einer CSV-Datei"


Beispiel 2b: Exportieren von Usern einer OU nach Excel, ohne SubOUs mit Eigenschaften
                    aus dem AD-Userinterface

function isUserpropertySinglevalued{
   #Abfrage ob eine Eigenschaft Multi- oder Singlevalued ist
   param($propertyname)
   $Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()
   $SchemaUser = $schema.FindClass('user')
   $SchemaUserAttributes=$SchemaUser.get_MandatoryProperties() + $SchemaUser.get_OptionalProperties()
   $HTschemauserattributes=@{} #leere Hashtable

  $schemauserattributes | foreach {
    $HTschemauserattributes[$_.name]=$_.issinglevalued
    } #foreach
    $return=$HTschemauserattributes[$propertyname]
    $return
    } #function
    
    
$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Props=""

#ab hier die Parameter anpassen
$Containerdn="OU=BenutzerA,OU=scripting,$domainDN" #User werden aus dieser OU gelesen
$Props="distinguishedname","cn","sn","givenname","mail","Terminalservicesprofilepath",
        "memberof","othertelephone","otherhomephone"             
        #"description","telephonenumber","mail","homedirectory","profilepath",
        #"scriptpath","Terminalservicesprofilepath","TerminalServicesHomeDirectory",
         #"TerminalServicesWorkDirectory","homephone","title","department",
         #"streetAddress","postalcode","mobile","useraccountcontrol"
$MVDelimiter=";"
# MV für MultiValue... , Props für ..Properties..

#Ab hier keine Parameter mehr verändern

#Evtl. vorhandene Excelprozesse schliessen
if($excel){
   $excel.Application.DisplayAlerts = $false
   $excel.quit()

   $ExcelProcess=get-process excel
   $ExcelProcess | foreach {stop-process ($_.id)}

   [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | out-null
}
#neue ExcelArbeitsmappe erstellen
$excel = new-object -comobject excel.application # ComObjekt erstellen
$excel.visible = $true
$workbook=$excel.Workbooks.add()
$sheet = $workbook.Worksheets.Item(1)

#Header in Excel-Tabelle schreiben aus $myProps
for($PropsCounter=0;$PropsCounter -lt $Props.count;$PropsCounter++){
   $sheet.Cells.Item(1,$PropsCounter+1).value2=$Props[$PropsCounter]
   }#for
 
$sheet.range($sheet.cells.item(1,1),$sheet.cells.item(1,$PropsCounter)).Font.Bold = $true #Header Fett


#Datenssätze schreiben
$userContainer = [ADSI]"LDAP://$Containerdn"
$user=$userContainer.children | ?{$_.objectclass[1] -eq "person"} #nur user werden exportiert
$usercounter=1
$user | foreach{  #jeder User im $userContainer
   $usercounter++ #jeder User kommt in neue Zeile
      
   #Schreiben der Singlevalue-Props in excel
   #$SVPropsCounter: jede neue Eigenschaft in eine neue Spalte
   for($PropsCounter=0;$PropsCounter -lt $Props.count;$PropsCounter++){
       if (isUserpropertySinglevalued $props[$propsCounter]  -eq $true) {
         #Schreiben der Singlevalue-Props nach Excel
       try{
          $sheet.Cells.Item($usercounter,$PropsCounter+1).value2=$_.invokeget($Props[$PropsCounter])
          } #catch
       catch{
          $sheet.Cells.Item($usercounter,$SVPropsCounter+1).value2=""
          #leere Eigenschaften wie Terminalservicesprofilepath verursachen Fehler
             }#catch
       }else{
       #Schreiben der Multivalued-Props nach Excel
       $cellvalue=""
         try{
          $MV=$_.invokeget($Props[$PropsCounter])
          foreach ($value in $MV) {
            $cellvalue+=$value+$MVDelimiter #jeder Wert des MV-Feldes wird in den String cellvalue geschrieben.
            } #for
            $sheet.Cells.Item($usercounter,$PropsCounter+1).value2=$cellvalue.substring(0,$($cellvalue.length)-1)
            # $cellvalue hat am Ende ein Delimiterzeichen zuviel
            } #try
      catch{
         $sheet.Cells.Item($usercounter,$PropsCounter+1).value2=""
          #leere Eigenschaften wie Terminalservicesprofilepath verursachen Fehler
          
            }#catch
       }#endif
    } #for
            
   }#user foreach

#Update 24.10.2010
# #Das Skript erkennt jetzt selbstständig, ob Properties ein
#oder mehrere Werte enthalten können. (Multi/ Singlevalued)

Anmerkung:

Dieselben, wie im Beispiel 1a


Beispiel 3: Exportieren von Usern einer OU in eine csv-Datei, inklusive SubOUs mit Eigenschaften
                   aus dem AD-Userinterface

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

#ab hier die SkriptParameter anpassen
$Containerdn="OU=BenutzerA,OU=scripting,$domainDN"
$delimiter=";"
$myProperties="distinguishedname","cn","sn","givenname","samaccountname",
              "description","Terminalservicesprofilepath",
              "Useraccountcontrol","userworkstations","memberof"
$filepath="c:\temp\userexport.csv"

#ab hier eventuell die Eigenschaften des Directoysearchers anpassen
$ds=([ADSISearcher]"LDAP://$PDCe")  #Directorysearcherobject erstellen, nicht verändern!
$ds.filter="(&(objectcategory=user)(objectclass=user))" #je nach Objektty anpassen
$ds.searchscope="subtree"  #subtree oder onelevel
$ds.searchroot="LDAP://$containerdn"  #schon vairablisiert
$ds.pagesize=1000  #normalerweise nicht verändern!

#ab hier beginnt der Code

#Header in csv-Datei schreiben
$output=$myProperties[0] 
for($counter=1;$counter -lt $myProperties.count; $counter++){
   $output=$output + $delimiter + $myproperties[$counter]
   }
$output
$output | out-file -filepath $filepath -encoding Default # -append

#User suchen

$ds.findall() | foreach{
   $user=[ADSI]($_.path)
   $output=$user.invokeget($myProperties[0])
    for($counter=1;$counter -lt $myProperties.count; $counter++){
       try{
          $output=$output +$delimiter+ $user.invokeget($myproperties[$counter])
          }
       catch{
          $output=$output +$delimiter
          }
     }
  $output #ggf. Bildschirmoutput
  $output | out-file -filepath $filepath -encoding Default -append
}

Im Gegensatz zu Beispiel 1 benutze ich hier den [ADSISearcher] anstelle der childrenEigenschaft von [ADSI], um die Userobjekte zu finden. Durch die zahlreichen Methoden und Eigenschaften von [ADSISearcher] ist dieses Skript natürlich erheblich flexibler. Unter anderem ist mit der Angabe des searchscopes (subtree oder onelevel) möglich, auch in Unterverzeichnissen zu suchen.
Der ADSISearcher mit seinen wichtigsten Methoden und Eigenschaften ist in Kapitel 5.2 ausführlich beschrieben

Ausserdem unterscheide ich hier nicht zwischen Single- und Multivalueeigenschaften. Dadurch wir das Skript etwas einfacher und kürzer, andererseits kann man das Multivalue-Trennzeichen "+" im Skript nicht beeinflussen.


Beispiel 4:  Exportieren von Usern aus einer OU nach Excel, ohne SubOUs mit securityrelevanten                                              UserEigenschaftenwie badpassworttime oder lastlogontimestamp

function ConvertADSLargeInteger([object] $adsLargeInteger)
{
    $highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
    $lowPart  = $adsLargeInteger.GetType().InvokeMember("LowPart",  [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)

    $bytes = [System.BitConverter]::GetBytes($highPart)
    $tmp   = [System.Byte[]]@(0,0,0,0,0,0,0,0)
    [System.Array]::Copy($bytes, 0, $tmp, 4, 4)
    $highPart = [System.BitConverter]::ToInt64($tmp, 0)

    $bytes = [System.BitConverter]::GetBytes($lowPart)
    $lowPart = [System.BitConverter]::ToUInt32($bytes, 0)
 
    return $lowPart + $highPart
    # Funktion von bsonposh.com
}


$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Props=""

#hier die Parameter anpassen
$Containerdn="OU=BenutzerA,OU=scripting,$domainDN" #User werden aus dieser OU gelesen
$Props="cn","badpasswordtime","lastlogontimestamp" #cn an erster Stelle

# Ab hier beginnt der Code

#Evtl. vorhandene Excelprozesse schliessen
if($excel){
   $excel.Application.DisplayAlerts = $false
   $excel.quit()

   $ExcelProcess=get-process excel
   $ExcelProcess | foreach {stop-process ($_.id)}

   [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | out-null
}
#neue ExcelArbeitsmappe erstellen
$excel = new-object -comobject excel.application # ComObjekt erstellen
$excel.visible = $true
$workbook=$excel.Workbooks.add()
$sheet = $workbook.Worksheets.Item(1)

#Header in Excel-Tabelle schreiben aus $Props
for($PropsCounter=0;$PropsCounter -lt $Props.count;$PropsCounter++){
   $sheet.Cells.Item(1,$PropsCounter+1).value2=$Props[$PropsCounter]
   }#for
 
$sheet.range($sheet.cells.item(1,1),$sheet.cells.item(1,$PropsCounter)).Font.Bold = $true #Header Fett


#Datenssätze schreiben
$userContainer = [ADSI]"LDAP://$Containerdn"
$user=$userContainer.children | ?{$_.objectclass[1] -eq "person"} #nur user werden exportiert
$usercounter=1
$user | foreach{  #jeder User im $userContainer
   $usercounter++ #jeder User kommt in neue Zeile
    
   $sheet.Cells.Item($usercounter,1).value2=$_.invokeget("cn")
                
   for($PropsCounter=1;$PropsCounter -lt $Props.count;$PropsCounter++){
      try{
         $sheet.Cells.Item($usercounter,$PropsCounter+1).value2=
               ([datetime]::FromFileTimeUtc((ConvertADSLargeInteger $_.invokeget($Props[$PropsCounter])))).datetime

          }
       catch{
           $sheet.Cells.Item($usercounter,$PropsCounter+1).value2=""
          #leere Eigenschaften wie Terminalservicesprofilepath verursachen Fehler
             }#catch
      } #for
} #user

Anmerkungen:

Einige Eigenschaften, insbesondere solche die Datumswerte enthalten, können nicht ganz so einfach ausgelesen werden, sondern müssen erst umgerechnet werden. System.___ComObject

Besonders für die Kollegen von Revision und Security sind solche Auswertungen interessant.

 

4.1.3 Löschen eines User (in Bearbeitung)

$User=[ADSI]"LDAP://CN=Napf Karl,OU=Benutzer,OU=scripting,DC=Dom1,DC=intern"
$user.DeleteTree()
#oder
$user.DeleteObject(0)

 

4.2 Gruppen

Gruppen sind aus ActiveDirectory-Sicht komplexer als die eben behandelte Userklasse.

So gibt diese fünf verschiedene, teilweise kombinierbare Gruppentypen

$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

Beispiel: Hexwert einer Globalen Sicherheitsgruppe definieren

$grouptype=$ADS_GROUP_TYPE_GLOBAL_GROUP + $ADS_GROUP_TYPE_SECURITY_ENABLED
"Gruppentyp in Hex: {0:X} -f $grouptype

#Ausgabe
Gruppentyp in Hex: 80000002

Im Kapitel 5.2.2.3.4 LDAPControls liegt ein Beispiel, wie Gruppen anhand ihres Gruppentyps gefiltert werden können.

Zum Verständnis von Gruppenmitgliedschaften von Benutzern sollte man sich kurz mal mit dem Thema "Forward -und Backlinks" beschäftigen. Recht anschaulich finde ich beipielsweise diesen Technetartikel:
 Technet - Actives Verzeichnis Blog: Windows Server 2008 R2 Recycle Bin und “linked attributes”


Eine Aufstellung aller Methoden und Attribute der Groupklasse findet man unter
 MSDN: Group Class und  IADsGroup Interface


Wie schon in der Einleitung zu Kapitel 4 dargelegt, werde ich auch für die Gruppenverwaltung erstmal nur native .Net Methoden meist mit [ADSI] verwenden

 

4.2.1 Anlage von Gruppen

Grundsätzlich gilt für die Gruppenanlage dasselbe, was ich über die Useranlage unter dem Stichwort "etwas verwirrend" weiter oben geschrieben habe.
Bei den folgenden Beispielen habe ich versucht, möglichst die gleiche Struktur wie oben bei den Usern zu verwenden. Da ADGruppen ohne Inhalt nicht das Ziel eines Skriptes sein dürften, ist die Mitgliederverwaltung eine zusätzliche Aufgabe in diesem Kapitel.

Beispiel 1: Massenanlage von Testgruppen

dieses Beispiel legt in einem (hoffentlich) TestAD 500 Domainlokale Sicherheitsgruppen von DLG-100 bis DLG-600 in der OU "OU=Gruppen,OU=Scripting,$domainDN" an.

$domainDN = ([ADSI]"LDAP://rootDSE").defaultnamingcontext

for ($i=100;$i -le 600;$i++){
  #Teil 1 Gruppenanlage
  $name="DLG-$i" #DLG für DomainLocalGroup
  $CNGroup="CN=$name"  #Achtung: das "CN=" vegisst man leicht
  $oupath="OU=Gruppen,OU=Scripting,$domainDN"
  $class = "Group"
  $ADSI=[ADSI]"LDAP://$oupath"
  $group = $ADSI.create($class,$cnGroup)
  $samaccountname=$name
  $group.invokeset("SamaccountName", $samaccountname)
  $group.commitchanges()
 }

Lässt man die For-Schleife weg und setzt für den $name einen festen Wert ($name="DLG-Managers"), so hat man ein einfaches Beispiel für eine Gruppenanlage, an dem man das prinzipielle Vorgehen gut ablesen kann und das man wie in den weiteren Beispielen gezeigt, gut erweitern kann.


4.2.2 Gruppenmitgliedschaften

Beispiel 1: User einer ADGruppe hinzufügen oder entfernen

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

#Teil 0: Variablendefinition
$GroupSamAccountName="DLG-100"
$GroupOuPath="OU=Gruppen,OU=Scripting,$domainDN"
$GroupCN="CN=DLG-100"

$UserCN="CN=Napf Karl"
$UserOuPath="OU=Benutzer,OU=Scripting,$domainDN"
$UserSamaccountname="A90005"

#Teil 1: User einer Gruppe hinzufügen

# User einer Gruppe hinzufügen über CN und LDAP-Provider
$Group=[ADSI]"LDAP://$PDCe/$GroupCN,$GroupOuPath"
$user=[ADSI]"LDAP://$PDCe/$UserCN,$UserOuPath"
$Group.Invoke("Add",$User.Path) #zum Entfernen "ADD" durch "Remove" ersetzen
#$Group.Add($User.Path)

#Alternativ: User einer Gruppe hinzufügen über SamAccountname und WinNT-Provider
##$user=[ADSI]("WinNT://$PDCe/$UserSamaccountname") #anstelle des Computers kann hier auch eine Domäne stehen!
##$group =[ADSI]("WinNT://$PDCe/$GroupSamAccountName")
##$Group.Invoke("Add",$User.Path)

Anmerkung:

1) weitere Beispiele zu lokalen Gruppen und Usern unter 3.2.1 WinNT-Provider

2) Da beim WinNT-Provider weder der ADPfad des Users, noch der ADPfad der Gruppe bekannt sein müssen, kann die Verwendung des WinNT-Providers gegenüber dem LDAP-Provider, der genaue Pfade verlangt, einfacher sein.
Man sollte aber, je nach Skripteinsatz, das Thema Performance bedenken. Schliesslich muss der WinNT-Provider das gesamte AD durchsuchen und das ohne resourcenschonende Eigenschaften wie Searchroot oder LDAPFiltern. Ich habe hierzu keine Erfahrungen, kann mir aber potentiell Probleme vorstellen. Also bitte vorher gut Testen!

3) Verwendet man den WinNT-Provider sollte man einen Domaincontroller, am besten den PDCe angeben, auf dem die Bindung erfolgt. Sonst könnte die gewünschte Aktion nicht in der Domäne, sondern auf dem lokalen Rechner ausgeführt werden.

4) Die  Add-Methode aus IADsGroup gibt Rückgabewerte (Return Value) zurück, die zum Logging oder das Abfangen von Fehlern nützlich sein können


Beispiel 2: Gruppe in Gruppe verschachteln / Gruppe aus Gruppe entfernen

#PDCe und distinguishedName auslesen
$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name

#Teil 0: Variablendefinition
$ParentGroupCN="CN=DLG-100"
$GroupOuPath="OU=Gruppen,OU=Scripting,$domainDN"

$ChildGroupCN="CN=DLG-101"
$ChildGroupOuPath="OU=Gruppen,OU=Scripting,$domainDN"

#Teil 1: ChildGroup einer ParentGruppe hinzufügen über CN und LDAP-Provider
$ParentGroup=[ADSI]"LDAP://$PDCe/$GroupCN,$GroupOuPath"
$ChildGroup=[ADSI]"LDAP://$PDCe/$ChildGroupCN,$ChildGroupOuPath"

$ParentGroup.Invoke("Add",$ChildGroup.Path) #zum Entfernen statt "Add" -> "Remove"
#$ParentGroup.Add($ChildGroup.Path)

 

Beispiel 3: Auslesen von Gruppenmitgliedern (unverschachtelt)

#PDCe und distinguishedName auslesen
$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name

#Teil 0: Variablendefinition
$GroupCN="CN=DLG-100"
$GroupOuPath="OU=Gruppen,OU=Scripting,$domainDN"

$Group=[ADSI]"LDAP://$PDCe/$GroupCN,$GroupOuPath"

"`nAusgabe der DNs"
$group.member
#Alternative: dsquery group -samid $group | dsget group -members

"`nAusgabe der SamAccountnamen der Gruppenmitglieder und der Objektklasse"
$group.member | foreach{   
   $gmember=[ADSI]"LDAP://$PDCe/$_"
    "$($gmember.name) $($gmember.objectclass[1])"
}

#beispielhafte Ausgabe

Ausgabe der CNs
CN=C10001,OU=Benutzer,OU=scripting,DC=Dom1,DC=intern
CN=C10000,OU=Benutzer,OU=scripting,DC=Dom1,DC=intern
CN=DGG-Test,OU=Gruppen,OU=scripting,DC=Dom1,DC=intern
CN=DLG-102,OU=Gruppen,OU=scripting,DC=Dom1,DC=intern

Ausgabe der SamAccountnamen der Gruppenmitglieder und der Objektklasse
C10001 person
C10000 person
DGG-Test group
DLG-102 group

 

Beispiel 4a: Auslesen von Gruppenmitgliedern (verschachtelt) mittels ds-Tools

$group="dlg-100"
$groupmembers=dsquery group -samid $group | dsget group -members -expand
$groupmembers

Näheres unter "dsquery group /?" und "dsget group /?"

Beispiel 4b: Auslesen von Gruppenmitgliedern (verschachtelt) mittels ds-LDAP-Controls

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://")
$ds.filter="(memberof:1.2.840.113556.1.4.1941:=CN=DLG-100,OU=Gruppen,OU=scripting,$domainDN)"
$ds.pagesize=1000
$ds.findall()

Auf LDAP-Controls gehe ich weiter hinten im Kapitel 5.2.2.3.4 genauer ein

Beispiel 4c: Auslesen von Gruppenmitgliedern (verschachtelt) mittels Rekursion

function ListGroupmembers($fnGroup){
 $fnMember=[ADSI]"LDAP://$_"
 $fnMember.distinguishedname
 $fnMember.member | foreach{
     if ($fnmember.objectclass[1] -eq "group")
     {
        ListGroupmembers $_
     }
  }    
}

#PDCe und distinguishedName auslesen
$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name

#Variablendefinition
$GroupCN="CN=DLG-100"
$GroupOuPath="OU=Gruppen,OU=Scripting,$domainDN"
$Group=[ADSI]"LDAP://$PDCe/$GroupCN,$GroupOuPath"

#Rekursion
$group.member | foreach{
  $nestedmembers=ListGroupmembers $_  
  $nestedmembers
  }

 

Beispiel 5: In welchen Gruppen ist der angemeldete Benutzer Mitglied (AnmeldeToken)

$loggedonuser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$nt = "System.Security.Principal.NTAccount" -as [type]
$loggedonuser.Groups | ForEach-Object { $_.translate($NT) }
#im Buch "Powershell 2.0  Best Practices" von Ed Wilson  p.142 steht eine ausführliche Erklärung

Mehr Informationen, wie ein Anmeldetoken genau aussieht, findet man hier

 TechNet Blogs > Ask the Directory Services Team > What's in a Token

 

4.3 OUs/ GPOs

(in Bearbeitung)

 

4.4 Forests und Domänen

Beispiel 1: Mehrere Varianten zum Ermitteln der aktuellen Domäne und aller Domaincontroller

#Skript

"$('=' * 20) getcurrentdomain $('=' * 20)"
$dom=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$dom

"$('=' * 20) Alle DCs der Domäne, Methode 1 $('=' * 20)"
$dom.findalldomaincontrollers() | select name,ipaddress,sitename

"$('=' * 20) Alle DCs der Domäne, Methode 2 $('=' * 20)"
$dom.domaincontrollers | select name,ipaddress,sitename

"$('=' * 20) Alle DCs in ein Array $('=' * 20)"
$DCs=$dom.domaincontrollers
foreach ($DC in $DCs){$DC.Name}

 DomainController Properties zeigt die möglichen Eigenschaften

 

Beispiel 2: Auflisten aller GlobalCatalogserver eines Forests

$myForest = [System.DirectoryServices.ActiveDirectory.Forest]::getcurrentforest()
$DCGCs=$myforest.findallglobalcatalogs()
$DCGCs | ft Name, IPAddress, domain, osversion, roles, sitename

 DomainController Properties zeigt die möglichen Eigenschaften

 

Beispiel 3: Auflisten aller DCs eines Forests

$objForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$objForest.Domains | foreach{
   $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$_.name)
      $([system.directoryservices.activedirectory.domain]::GetDomain($context).domainControllers) | foreach{
      "$($_.name) $($_.ipaddress) $($_.isglobalcatalog())"
      }  
   }

 DomainController Properties zeigt die möglichen Eigenschaften

isglobalcatalog() ist eine Methode der DomainControllerClass  DomainController Methods

 

4.5 Standorte und Dienste

Dieses Kapitel ist momentan im Aufbau

 

Beispiel 1: Verbinden auf eine bestimmte Site

$rootDSE = [ADSI]"LDAP://rootDSE"
$siteName =  "Default-First-Site-Name"

$configNCDN = $rootDSE.ConfigurationNamingContext
$siteContainerDN = ("CN=Sites," + $configNCDN)
$siteDN = "CN=" + $siteName + "," + $siteContainerDN
[ADSI]"LDAP://$siteDN" | select *  

Zur näheren Erklärung der [ADSI]-Query schaut bitte etwas weiter unten im Kapitel 5.1

 

Beispiel 2: Auflisten aller Sites

$rootDSE = [ADSI]"LDAP://rootDSE"
$configNCDN = $rootDSE.ConfigurationNamingContext
$siteContainerDN = ("CN=Sites," + $configNCDN)
$ds=([ADSI]"LDAP://$siteContainerDN")
$ds.children | select -expand name

Zur näheren Erklärung der [ADSI]-Query und der Children-Eigenschaften schaut bitte etwas weiter unten im Kapitel 5.1

 

Beispiel 3: Auflisten der IP-Adressen einer Site

$rootDSE = [ADSI]"LDAP://rootDSE"
$siteName =  "Standort1"

$configNCDN = $rootDSE.ConfigurationNamingContext
$siteContainerDN = ("CN=Sites," + $configNCDN)
$siteDN = "CN=" + $siteName + "," + $siteContainerDN
[ADSI]"LDAP://$siteDN" | select -expand siteobjectBL

 

Beispiel 4: Erstellen eines neuen Subnetzes und Zuweisen zu einer Site

$subnet="192.168.9.0/24"
$sitename="Standort2" #muss vorhanden sein
$location="Lokation"
$description="Description"

$RootDSE = [ADSI]"LDAP://RootDSE"
$ConfigurationNC = $RootDSE.configurationNamingContext
$SubnetRDN = "CN=$($Subnet)"
$SiteNameRDN = "CN=$($SiteName)"

$SiteDN = "$($SiteNameRDN),CN=Sites,$($ConfigurationNC)"

$SubnetsContainer = [ADSI]"LDAP://CN=Subnets,CN=Sites,$($ConfigurationNC)"

$NewSubnet = $SubnetsContainer.Create("subnet","CN=$subnet")
$NewSubnet.Put("siteObject", $SiteDN)
$NewSubnet.Put("description", $Description)
$NewSubnet.Put("location", $Location)
$NewSubnet.SetInfo()

 

 

4.7 Wenns mal nicht so läuft - ADSI Errorcodes

 MSDN: ADSI Error Codes

5 Queries

Zum Einstieg in das Thema "Suchen im ActiveDirectory" zwei gute, allerdings nicht unbedingt leicht verdauliche Artikel.

 What Are Active Directory Searches?

 How Active Directory Searches Work

5.1 einfache Queries mit [ADSI] oder System.DirectoryServices.DirectoryEntry

Beispiel 1: Auflisten aller Objekte einer OU (ohne Rekursion)

$OU=([adsi]"LDAP://ou=User,ou=scripting,dc=dom1,dc=intern")
$ou.children | foreach {$_.name}

Die children-Eigenschaft kann übrigens nicht rekursiv genutzt werden!

Beispiel 2: Suchen ab einem Eintiegspunkt nach einem Namensbestandteil (mit Rekursion)

#DN der Domaäne und PDCe bestimmen
function getRootDSEandPDCe {  
    $rootDSE = [ADSI]"LDAP://rootDSE"
    $fndomainDN = $rootDSE.defaultNamingContext
    $fndomainDN  #erstes Suchergebnis

    $domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
    $fnPDCe=$domain.PdcRoleOwner.Name
    $fnPDCe
}

#Rekursiv die Domäne durchsuchen. Einstiegspunkt der Suche ist fnEntry z.B. DC=Dom1,DC=Intern
function findEntriesInSubOUs($fnEntry){  
    $AD=[ADSI]"LDAP://$fnEntry" #andere Schreibweisen im Beispiel von Kap. 2.1
        if($fnentry -like "*$SearchObject*"){
           $fnEntry   #die Suchergebnisse werden als Array gespeichert
         }
    $AD.children | foreach {  #Rekursion
    findEntriesInSubOUs($_.distinguishedname)}
}

$ergebnis=getRootDSEandPDCe
$DomainDN=$ergebnis[0]
$PDCe=$ergebnis[1]

write-host -foregroundcolor blue  "DN der Domäne: $DomainDN"
write-host -foregroundcolor darkyellow "PDCEmulator:   $PDCe"
#weitere Farben unter "get-help write-host" -> Syntax
""
$Searchobject="test"
$SearchErgebnis=findEntriesInSubOUs($domaindn) #domaindn ist der Startpunkt der Suche
$SearchErgebnis | foreach {write-host -foregroundcolor red $_}

#Ausgabe

DN der Domäne: DC=Dom1,DC=intern
PDCEmulator:   DC1.Dom1.intern

CN=Share-Test1,OU=scripting,DC=Dom1,DC=intern
CN=test-2,OU=scripting,DC=Dom1,DC=intern
CN=test-leer,OU=scripting,DC=Dom1,DC=intern

das Beispiel liefert alle Objekte im ADBaum zurück, die das Wort "Test" im CN enthalten.
Im Normalfall benutzt man für eine Suche im AD aber andere Mechanismen, wie ADO oder die .Net Klasse [ADSISearcher], da dort die Suche genauer spezifiziert werden kann. So ist es dort möglich, beispielsweise nur nach Computern zu suchen. Ausserdem ist die Suche sehr viel performanter und resourcenschonender.

Sinnvoll ist diese Art der Suche, wenn man in einer bestimmten OU mit höchstens ein oder zwei untergeordneten SubOUs Objekte mit bestimmten Eigenschaften verändern möchte. Sollen zum Beispiel in einer OU "Anwender" alle Userobjekte mit dem Ort "München" eine neue Telefonnummer bekommen, dann würde ich, abhängig von der übrigen Programmstruktur, vielleicht diese Suche mit der OU "Anwender" als Einstiegspunkt verwenden.

5.2 Queries mit [ADSISearcher] oder System.Directory.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


Beispiel 1: vier verschiedene Syntaxvarianten für die .Net-Klasse System.Directoryservices.Directorysearcher

([ADSISearcher]"(|(Name=*Test*)(Name=*Napf*))").findall() | select path 

([system.directoryservices.directorysearcher]"(|(Name=*Test*)(Name=*napf*))").findall() | select path

$ds=new-object -typename system.directoryservices.directorysearcher
$ds.filter="(|(Name=*Test*)(Name=*Napf*))"
$ds.findall()  | select path

$ds=([ADSISearcher]"LDAP://")  #meine favorisierte Schreibweise
$ds.filter="(|(Name=*Test*)(Name=*napf*))"
$ds.findall()  | select path

 

# Ausgabe, wenn Objekte vorhanden sind, deren Name "Napf" oder "Test" enthält
#die Ausgabe erscheint viermal identisch

Path
----
LDAP://CN=Napf,CN=Users,DC=Dom1,DC=intern
LDAP://CN=test-leer,OU=scripting,DC=Dom1,DC=intern
LDAP://CN=test-2,OU=scripting,DC=Dom1,DC=intern
LDAP://CN=Share-Test1,OU=scripting,DC=Dom1,DC=intern

Mir persönlich gefällt die letzte von den vier obigen Varianten durch ihre Mischung aus Kompaktheit und Übersichtlichkeit am besten, besonders wenn man noch weitere Eigenschaften des DirectorySearchers, wie unter 5.2.2 beschrieben, einsetzen möchte.

In dem Beispiel sind die beiden Suchkriterien "(Name=*Test*)(Name=*Napf*)" mit Oder "|" verknüpft. Und-Verknüpfungen werden mit "&" realisiert. Auf die LDAP-Syntax gehe ich aber unter 5.2.2.3 Eigenschaft Filter mit weiteren Beispielen genauer ein.

 

5.2.1 Methoden von [ADSISearcher]

 MSDN: DirectorySearcher-Methoden Tabelle aller Methoden 

Die beiden wichtigsten Methoden sind Findall() und Findone()

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.

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


Beispiel 1: Die Methoden findall() und findone()

$ds=([ADSISearcher]"LDAP://")
$ds.filter="(Name=*Napf*)"

$ds.findall() | select path  #findet alle Napf

$ds.findall()[0] | select path  #liefert das erste Objekt der Collection
$ds.findall()[0].path #identisch

$ds.findone() | select path #beendet die Suche nach dem ersten Treffer
$ds.findone().path #identisch

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.
Letztlich kann ein Ergebnis, das mit Findone() erhalten wurde, direkt mit [ADSI] weiterverarbeitet werden. siehe 4.3 Kombination aus [ADSISearcher] und [ADSI]

 

5.2.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 Tabelle aller Eigenschaften

Kapitel

Name

Beschreibung

5.2.2.1

Asynchronous

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

5.2.2.2

CacheResults

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

5.2.2.3

Filter

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

5.2.2.4

PageSize

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

5.2.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.

5.2.2.6

SearchScope

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

5.2.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.

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

5.2.2.1 Eigenschaft Asynchronous

Default: $False

 MSDN: DirectorySearcher.Asynchronous-Eigenschaft

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

Beispiel:

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

$ds=([ADSISearcher]"LDAP://") 
$ds.filter="objectCategory=User"
$ds.asynchronous=$false #false oder $true.
$ds.findall()| select path

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 einen Domaincontroller gibt es dagegen keinen Unterschied, ob ein Client eine synchrone oder asynchrone Abfrage gegen ihn ausführt. Seine Performancebelastung bleibt gleich.

5.2.2.2 Eigenschaft CachedResults

Default: $True

 MSDN: DirectorySearcher.CacheResults Property

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


Beispiel 1: Eigenschaft CachedResults

$ds=([ADSISearcher]"LDAP://")
$ds.filter="objectCategory=User"
$ds.CacheResults=$false
$ds.findall()  | select path

CacheResults 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 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 sein wenn überhaupt nur in foreach-Schleifen wirklich sinnvoll.

5.2.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:

 Amazon: 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!

5.2.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)

5.2.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

$ds.filter="(&(Objectcategory=User)(Name=Napf*))"
#oder
$ds.filter="(&(Objectclass=User)(Name=Napf*))"

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

$ds.filter="(&(Objectcategory=User)(Name=Napf*))"

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

$ds.filter="(&(objectClass=User)(Name=Napf*))"

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 1: gemeinsamer Einsatz von objectClass und objectCategory 

$ds.filter="(&(objectClass=User)(objectCategory=User)(Name=Napf*))"

 

5.2.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

<code class="ce">HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\
Diagnostics\15 Field Engineering </code>

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

<code class="ce"></code>

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

5.2.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.

a) 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 Gruppentyp mit bitweisem Vergleich
 MSDN: ADS_GROUP_TYPE_ENUM Enumeration

$ds=([ADSISearcher]"LDAP://")

$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

$ds.filter="(&(objectCategory=group)(groupType:1.2.840.113556.1.4.803:=$ADS_GROUP_TYPE_SECURITY_ENABLED))" 
$ds.filter="(&(objectCategory=group)(groupType:1.2.840.113556.1.4.803:=$(0x80000000)))"
$ds.filter="(&(objectCategory=group)(groupType:1.2.840.113556.1.4.803:=2147483648))"

$ds.findall()

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 für alle lokalen Securitygruppen oder in weniger klarer Form 2147483652 gleichbedeutend in dezimaler Schreibweise.

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

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://") 
$ds.filter="(!(systemFlags:1.2.840.113556.1.4.803:=16))" #category 2
# $ds.filter="(systemFlags:1.2.840.113556.1.4.803:=16)" #category 1
$ds.searchroot="LDAP://cn=schema,cn=configuration,$domainDN"
$ds.pagesize=1000

$ds.findall()

Anmerkung 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:="

Anmerkung 2: Anstelle dieser beiden etwas länglichen LDAPControls kann man auch mit den Booleschen Operatoren -band und -bor arbeiten. siehe Kapitel 3.3

b) 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 3: Suchen in einer Objektkette, Mitglieder einer Gruppe auslesen

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://") 
$ds.filter="(memberof:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,$domainDN)"
$ds.pagesize=1000
#$ds.findall()

#Alternative
$group=[ADSI]"LDAP://CN=Domain Admins,CN=Users,$domainDN"
$group.member

 

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

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://") 
$ds=([ADSISearcher]"LDAP://OU=Gruppen,OU=Scripting,$domainDN") #schneller gehts, wenn die Gruppen unterhalb einer Stelle im AD liegen
$ds.filter="(member:1.2.840.113556.1.4.1941:=CN=Karl Napf,OU=Benutzer,OU=Scripting,$domainDN)"
$ds.searchscope="subtree"
$ds.findall()

#Alternative
$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$user=[ADSI]"LDAP://CN=Karl Napf,OU=Benutzer,OU=Scripting,$domainDN"
$user.memberof


c) 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

 

5.2.2.3.5 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.2010 angelegt wurden

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://")
$ds.filter="(&(objectcategory=user)(objectclass=user)(whenCreated>=20100822000000.0Z))"
$ds.searchroot="LDAP://OU=BenutzerB,OU=Scripting,$domainDN"
$ds.searchscope="subtree"
$ds.findall() | select path


5.2.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!

ds.pagesize = 1000

 

5.2.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

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://")
$ds.filter="(objectcategory=user)"
$ds.searchroot="LDAP://OU=BenutzerB,OU=Scripting,$domainDN"
$ds.searchscope="subtree"
$ds.findall() | select path


Leider kann man den Root nicht so setzen, daß 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

$domainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext

$ds=([ADSISearcher]"LDAP://")
$ds.filter="(cn=user*)"
$ds.searchroot="LDAP://cn=schema,cn=configuration,$domainDN"
$ds.findall() | select path

#Ausgabe gekürzt

Path                                                                                                                                                                     
----                                                                                                                                                                     
LDAP://CN=User,CN=Schema,CN=Configuration,DC=Dom7,DC=intern                                                                                                              
LDAP://CN=User-Account-Control,CN=Schema,CN=Configuration,DC=Dom7,DC=intern                                                                                              
LDAP://CN=User-Cert,CN=Schema,CN=Configuration,DC=Dom7,DC=intern                

 

5.2.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

 

5.2.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 aller tombstoned User und der Eigenschaft "whenchanged" (im Normalfall das Löschdatum)

$rootDSE = [ADSI]"LDAP://rootDSE"
$domainDN = $rootDSE.defaultNamingContext
$rootDSE.psbase.AuthenticationType=[System.DirectoryServices.AuthenticationTypes]::FastBind
$rootDSE.psbase.path ="LDAP://cn=Deleted Objects," + $domainDN

$ds = ([ADSISearcher]"LDAP://")
$ds.searchroot=$rootDSE
$ds.Filter = "(&(isDeleted=TRUE)(objectclass=user))"
$ds.tombstone = $true
$ds.SearchScope = [System.DirectoryServices.SearchScope]::onelevel
$delObj=$ds.Findall()
#$delObj[0].properties

for ($i = 0; $i -le $delObj.count; $i++){
    $sam=$($delObj[$i].properties.samaccountname)
    $whenchanged=$($delObj[$i].properties.whenchanged)
    $Isdeleted=$($delObj[$i].properties.isdeleted)
    write-host "$sam  $whenchanged  $Isdeleted"
}

#Ausgabe

KarlNapf  08/22/2010 03:08:46  True
A10000  08/22/2010 03:08:46  True
A10001  08/22/2010 03:09:13  True
A10002  08/22/2010 03:09:39  True

 

5.3 Kombination aus [ADSISearcher] und [ADSI]

so richtig elegant wird es, wenn man die über [ADSISearcher] gefundenen Objekte gleich an [ADSI] übergibt und bei jedem [ADSI]-Objekt die gewünschte Eigenschaft ausliest oder verändert.


Beispiel 1: Suchen und Verändern eines Userobjektes


Dieses Beispiel zeigt, wie man ein Objekt verändern kann, dessen genauer Pfad (distinguishedname) unbekannt oder variabel ist. Als Lösungsweg sucht man im ersten Schritt mit [ADSISearcher] das Objekt im AD, bindet im zweiten Schritt mit [ADSI] ein Directoryentry darauf und kann anschliessend Eigenschaften des Objekts verändern.

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

#PDCe und distinguishedName auslesen
$domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
$PDCe=$domain.PdcRoleOwner.Name

#User suchen
$ds=([ADSISearcher]"LDAP://$PDCe") 
$ds.filter="(&(ObjectCategory=user)(samaccountname=KarlNapf))"
$ds.pagesize=1000
$ds.findone().path #=DN des gefundenen Users

#$user auf Objekt binden
$user=[ADSI]($ds.findone().path)

#Eigenschaft setzen
$user.invokeset("TerminalServicesProfilePath","\\192.168.47.11\`$TSHome") # `zum Maskieren des $-Zeichens
$user.invokeset("accountexpires","129159288000000000")
$user.setinfo()

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 Suchen des Users und das Binden kann man in einer einzigen Zeile auch so bewerkstelligen

[ADSI](([ADSISearcher]"(&(ObjectCategory=user)(samaccountname=KarlNapf))").findone().path)

ist Geschmackssache. Ich finde es so etwas unübersichtlich.

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


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 distinguishedname 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)

 

5.4 Queries mit ADO (ADSI OLE DB provider)

5.5 LDAP Calls analysieren

5.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