Behandelte Themen
Einleitung Powershell und ActiveDirectory unter W2K8R2, Win7, W2K3 System.DirectoryServices.Directoryentry [ADSI]
3.1Ein 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.2Beschreibung des WinNT- und ADSI-Provider
3.2.1WinNT 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.2LDAP Provider
3.2.2.1LDAP Connection Strings
Beispiel: dynamisches Ermitteln des distinguishedNames der Domäne, des PDCEmulators und des FQDNs der Domäne
3.2.2.2Connection auf die Domäne
Beispiel 1: Verbindung auf die Domäne
Beispiel 2: Anzeigen von Domäneneigenschaften
3.2.2.3Connection auf RootDSE
Beispiel 1: Zugriff auf RootDSE
Beispiel 2: Ermitteln einiger LDAP-Eigenschaften des RootDSE
3.2.2.4Kerberos / NTLM Authentifizierung
Beispiel 1: Bindung an RootDSE über das Kerberosprotokoll
Beispiel 2: Bindung an RootDSE über das NTLMProtokoll
3.3Useraccountcontrol / UserFlags
Klassen im AD
4.1User
Beispiel 1: Anzeige aller MultivaluedAttribute der Userklasse
Beispiel 2: Abfrage, ob eine Eigenschaft Multi- oder SingleValued ist
4.1.1Anlegen 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.2Lesen 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.2Gruppen
4.2.1Anlage von Gruppen
Beispiel 1: Massenanlage von Testgruppen
4.2.2Gruppenmitgliedschaften
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.3OUs/ GPOs (noch leer)
4.4Forests 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.5Standorte und Dienste
Beispiel 1: Verbinden auf eine bestimmte Site
Beispiel 2: Auflisten aller Sites
4.6ActiveDirectory Schema
4.7Wenns mal nicht so läuft - ADSI Errorcodes Queries
5.1einfache Queries mit [ADSI]
Beispiel 1: Auflisten aller Objekte einer OU (ohne Rekursion)
Beispiel 2: Suchen ab einem Eintiegspunkt nach einem Namensbestandteil (mit Rekursion)
5.2Queries mit [ADSISearcher]
Beispiel 1: vier verschiedene Syntaxvarianten für die .Net-Klasse System.Directoryservices.Directorysearcher
5.2.1Methoden von [ADSISearcher]
Beispiel 1: Die Methoden findall() und findone()
5.2.2Eigenschaften von [ADSISearcher]
5.2.2.1Eigenschaft Asynchronous
Beispiel 1: Eigenschaft asynchronous
5.2.2.2Eigenschaft CachedResults
Beispiel 1: Eigenschaft CachedResults
5.2.2.3Eigenschaft Filter
5.2.2.3.1Hilfsmittel zum Filterbau
5.2.2.3.2FilterAttribute objectCategory und objectClass
Beispiel 1: gemeinsamer Einsatz von objectClass und objectCategory
5.2.2.3.3Resourcenverbrauch von Filtern
5.2.2.3.4LDAPControls
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.5Zeit in LDAPFiltern (ADS_UTC_TIME)
Beispiel: Alle Benutzer ausgeben, die nach dem 22.08.2010 angelegt wurden
5.2.2.4Eigenschaft Pagesize
5.2.2.5Eigenschaft Searchroot
Beispiel 1: Eigenschaft searchroot
Beispiel 2: Suche im Schemacontainer
5.2.2.6Eigenschaft Searchscope
5.2.2.7Eigenschaft Tombstone
Beispiel 1: Anzeigen aller tombstoned User und der Eigenschaft "whenchanged"
5.3Kombination 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.5LDAP 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
Mit der Powershell V2 und nur in Verbindung mit einem W2k8R2-Domaincontroller oder einem Windows7 Client mit installierten RSAT-Tools
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
Windows2008R2
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
Import des GroupPolicy Modules
Mit diesem GPO-Module erreicht man dieselbe Funktionalität, die die VB-Skripte der GPMC angeboten haben.
Activedirectory-Module und Windows2003 Server (ADWS)
3 System.DirectoryServices.Directoryentry [ADSI]
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.
Die möglichen Protokolle (oder Provider) von [ADSI], sowie weitere Hintergründe findet man im ScriptingGuide:
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 |
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
#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
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
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:
Unterstützte und nicht unterstützte Methoden und Eigenschaften:
Beispiel zum Verändern einer lokalen Gruppenmitgliedschaft:
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
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
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
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
"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
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
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
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
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.
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
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
4 Klassen
In Windows 2008 gibt es mittlerweile 228 Klassen, die im ActiveDirectory definiert sind. Eine Aufstellung dieser Klassen liefert:
In den verzweigenden Sublinks zu jeder Klasse wird jede Klasse in Abhängigkeit vom verwendeten Betriebssystem (Win2000 bis Win2008) detailliert beschrieben.
Beispiel:
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
Ebenso werde ich in diesem Kapitel nur solche Methoden verwenden, die keine zusätzlichen
Einfache Portierbarkeit, Support, Stabilität und die Vermeidung von Abhängigkeiten erscheinen mir wichtiger, als eine mögliche Vereinfachung des Scriptens durch Zusatzmodule.
Das
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
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 (
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
$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:
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
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:
c) viele Usereigenschaften und Methoden, die zwar dasselbe Attribut oder diesselbe Methode wie unter a) bedeuten, aber einen anderen Namen besitzen, findet man unter
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
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
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
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
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
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.
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
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:
Eine Aufstellung aller Methoden und Attribute der Groupklasse findet man unter
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
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
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
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
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
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}
Beispiel 2: Auflisten aller GlobalCatalogserver eines Forests
$myForest = [System.DirectoryServices.ActiveDirectory.Forest]::getcurrentforest()
$DCGCs=$myforest.findallglobalcatalogs()
$DCGCs | ft Name, IPAddress, domain, osversion, roles, sitename
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())"
}
}
isglobalcatalog() ist eine Methode der DomainControllerClass
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
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
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.6 ActiveDirectory - Schema
Scripting Guy:
4.7 Wenns mal nicht so läuft - ADSI Errorcodes
5 Queries
Zum Einstieg in das Thema "Suchen im ActiveDirectory" zwei gute, allerdings nicht unbedingt leicht verdauliche Artikel.
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
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 "
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
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
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.1 Methoden von [ADSISearcher]
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
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!
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
Beschreibung der MSDN: Ruft einen Wert ab, der angibt, ob die Suche asynchron ausgeführt wird, oder legt diesen fest.
Beispiel:
$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
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
5.2.2.3 Eigenschaft Filter
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:
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:
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
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.
<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:
ein ausformuliertes, dokumentiertes Beispiel findet man auf diesem Blog:
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. | |
| 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
$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
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.
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
Extended Controls sind über die .Net Klasse
5.2.2.3.5 Zeit in LDAPFiltern (ADS_UTC_TIME)
Zu dieser Eigenschaft ein wiederholter Verweis auf
"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)
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
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.
Membername | Beschreibung | |
|---|---|---|
Base | Beschränkt die Suche auf das Basisobjekt. Das Ergebnis enthält maximal ein Objekt.Wenn die | |
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.7 Eigenschaft Tombstone
MSDN:
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:
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 (
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 (
Anmerkung 4: die Methode
5.4 Queries mit ADO (ADSI OLE DB provider)
5.5 LDAP Calls analysieren
5.5.1 Tracelog.exe
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:
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