Einleitung
2 Anlage von Usern
   Beispiel 1: Anlage einer einfachen OU-Struktur
   Beispiel 2: Anlage von Testusern
   Beispiel 3: Anlage eines Users (mit TerminalserverProfil-Eigenschaften)
   Beispiel 4: Useranlage mit Daten aus einer csv-Datei
3 Lesen und Verändern von Userproperties
   Beispiel 1: Auslesen von Userkonten, die sich seit xx-Tagen nicht mehr an der Domäne angemeldet haben ([ADSI])
   Beispiel 2a: Exportieren von Usernkonten (Konfiguration im Skript)
   Beispiel 2b: Exportieren von Usernkonten (Konfiguration in einer externen ini-Datei)      
4 Löschen eines User


 


1 Einleitung

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 finden sich an mehreren Stellen unter:
MSDN: User Class, MSDN: IADSUser Interface sowie für Terminalservereigenschaften unter MSDN: IADsTSUserEx Interface. Es gibt also für viele Attribute zwei Interfaces zwei unterschiedliche Namen zum Skripten von ein- und derselben Eigenschaft (Beispiel: givenname oder firstname). In den Anmerkungen des 2. Beispiels zur Useranlage im nächsten Kapitel gehe ich ein bischen näher darauf ein.

Verwirrenderweise wird außerdem in der Powershellwelt von Properties also Eigenschaften gesprochen, in der ActiveDirectorywelt dagegen von Attributen.

2 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"]

..und 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 (UserAccountControl / UserFlags) und nicht als einzelne Eigenschaft abgespeichert werden. Die wichtigste davon ist wahrscheinlich "AccountDisabled", oder auch "SmartCardRequired". Die meisten UAC-Eigenschaften muss man als Wert setzen, Accountdisabled kann man auch direkt über InvokeSet setzen, da diese Methode im "IADsUser Interface" enthalten ist. Siehe "Beispiel 2" wieter unten

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

Beispiel 1: Anlage einer einfachen OU-Struktur

dieses Skript legt eine einfache OU-Struktur an, in die in den weiteren Beispielen Testobjekte angelegt werden können

Set-StrictMode -Version "2.0"
Clear-Host

#Variablendefinition
$OUPath1 = "Ou=Scripting"
$OUPath2 = "Ou=Benutzer"
$OUPath3 = "Ou=Computer"

#DomänenDaten bestimmen
$DomainDN = ([ADSI]"LDAP://RootDSE").DefaultNamingContext

#Erstellen der ersten OU "Scripting"
$ADSI=[ADSI]"LDAP://$DomainDN"
$Class = "OrganizationalUnit"
$NewOU = $ADSI.Create($Class,$OUPath1)
$NewOU.CommitChanges()

#Erstellen der SubOU "Benutzer"
$ADSI=[ADSI]"LDAP://$OUPath1,$DomainDN"
$Class = "OrganizationalUnit"
$NewOU = $ADSI.Create($Class,$OUPath2)
$NewOU.CommitChanges()

#Erstellen der SubOU "Computer"
$ADSI=[ADSI]"LDAP://$OUPath1,$DomainDN"
$Class = "OrganizationalUnit"
$NewOU = $ADSI.Create($Class,$OUPath3)
$NewOU.CommitChanges()

Anmerkung:

Wie im letzten Kapitel 3.1 Überblick über die [ADSI]-Provider beschrieben, ist die Kurzschreibweise [ADSI]"LDAP://... nur einer andere Darstellung der DirectorEntry-Klasse. MSDN: DirectoryEntry-Klasse 
Anstatt der Zeilen

#DomänenDaten bestimmen
$DomainDN = ([ADSI]"LDAP://RootDSE").DefaultNamingContext

würde folgende Schreibweise genauso funktionieren:

#DomänenDaten bestimmen
$DomainDN = $(New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")).DefaultNamingContext

Beispiel 2: Anlage von Testusern

Wenn mit Beispiel 1 die OU-Struktur angelegt wurde, könnt ihr mit diesem Skript einige User anlegen

Set-StrictMode -Version "2.0"
Clear-Host

#Domänendaten ermitteln
$DomainDN = ([ADSI]"LDAP://rootDSE").defaultnamingcontext
$DomFqdn=$env:USERDNSDOMAIN

#Variablendefinition
$PreName="Karl"
$CountStart=14000
$CountEnd = 14200
$OuPath = "OU=Benutzer,OU=Scripting,$DomainDn"

#Useranlage
for ($i=$CountStart;$i -le $CountEnd;$i++){

  #Teil 0 Usereigenschaften definieren
  $Name="$PreName$i"
  $CNUser="CN=$Name"
  $GivenName="gn$Name"
  $Sn="sn$Name"
  $Description="de$Name"
  $Class = "user"
  $SamAccountname=$Name
  $UserPrincipalName="$SamAccountName@$DomFqdn"
 
  #Teil 1 User anlegen
  $UserPrincipalName #Bildschirmausgabe
  $AdsiOU=[ADSI]"LDAP://$OuPath"
  $AdsiUser = $AdsiOU.Create($Class,$CnUser)
  $AdsiUser.InvokeSet("SamAccountName", $SamAccountName)
  $AdsiUser.CommitChanges()
 
  #Teil 2 Eigenschaften setzen
  $AdsiUser.InvokeSet("UserPrincipalName",$UserPrincipalName)
  $AdsiUser.InvokeSet("GivenName",$GivenName)
  $AdsiUser.InvokeSet("Sn",$Sn)
  $AdsiUser.InvokeSet("Description",$Description)
  $AdsiUser.Invoke("SetPassword","Hurra123")

 
  #Teil 3 Eigenschaften der UAC setzen
  $AdsiUser.InvokeSet("AccountDisabled",$False) #Alternative zu Useraccountcontol
  $AdsiUser.CommitChanges()
}

Lässt man die For-Schleife weg und setzt für $Name einen festen Wert (beispielsweise $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 3: Anlage eines Users (mit TerminalserverProfil-Eigenschaften)

Set-StrictMode -Version "2.0"
Clear-Host

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

#Teil 0 , Usereigenschaften definieren
#Basisinformationen für die initiale Anlage
$Sn="Napf5"
$GivenName="Karl"
$SamAccountName="A90020"
$OuPath="OU=Benutzer,OU=Scripting,$domainDN"
$Class ="User"

#Eigenschaften des Users
$CNUser="CN= $SN $givenName"
$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="Hurra123"
$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"
$AdsiUser = $ADSI.create($CLass,$CnUser)
$ADSIUser.InvokeSet("SamaccountName", $SamAccountName)
$AdsiUser.CommitChanges()

#Teil 2 Usereigenschaften setzen
$AdsiUser.InvokeSet("UserPrincipalName", $UserPrincipalname)
$AdsiUser.properties["UserWorkstations"].Add("$UserWorkstations") #$AdsiUser.InvokeSet("userworkstations",$userworkstations) #geht nicht

$AdsiUser.InvokeSet("Sn",$Sn)
$AdsiUser.InvokeSet("GivenName",$GivenName)
$AdsiUser.InvokeSet("Description",$Description)
$AdsiUser.InvokeSet("TelephoneNumber",$TelephoneNumber)
$AdsiUser.OtherTelephone=$OtherTelephone #mit $AdsiUser.InvokeSet("OtherTelephone".$OtherTelephone)scheitert man
$AdsiUser.InvokeSet("Mail",$Mail)

$AdsiUser.InvokeSet("Homedirectory",$HomeDirectory)
$AdsiUser.InvokeSet("ProfilePath",$ProfilePath)
$AdsiUser.InvokeSet("ScriptPath",$ScriptPath)

$AdsiUser.InvokeSet("Terminalservicesprofilepath",$TerminalServicesProfilePath)
$AdsiUser.InvokeSet("TerminalServicesHomeDirectory",$TerminalServicesHomeDirectory)
$AdsiUser.InvokeSet("TerminalServicesWorkDirectory",$TerminalServicesWorkDirectory)

$AdsiUser.InvokeSet("Title",$Title)
$AdsiUser.InvokeSet("Department",$Department)

$AdsiUser.InvokeSet("StreetAddress",$StreetAddress)
$AdsiUser.InvokeSet("PostalCode",$PostalCode)
$AdsiUser.InvokeSet("company",$company)
$AdsiUser.InvokeSet("mobile",$mobile)
$AdsiUser.HomePhone=$HomePhone #mit $AdsiUser.InvokeSet("homephone".$homephone) scheitert man
$AdsiUser.OtherHomePhone=$OtherHomePhone

$AdsiUser.Setpassword($InitialPassword)
#$AdsiUser.invoke("SetPassword",$initialpassword) #alternativ
#$AdsiUser.InvokeSet("userpassword",$initialpassword) setzt nicht!!! das AD-Password
$AdsiUser.InvokeSet("PwdLastSet",$PwdLastSet)

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


#Kap 4.1.2 Beispiel 2: Anlage eines Users mit TerminalserverProfil-Eigenschaften


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

regsvr32.exe tsuserex.dll

Ask the Directory Services Team: Getting the Terminal Services Tabs to Appear in AD Users and Computers

Mit den Eigenschaften aus MSDN: IADsTSUserEx interface lassen sich die RemoteDesktop-Properties setzen

Anmerkung 2) 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 verhält sich Powershell flexibler.

Anmerkung 3) Die meisten dieser Attribute der AD-Userklasse - Ausnahme sind die Terminalserverattribute - findet ihr auf der bereits erwähnten Seite MSDN: All Attributes. Im Skript ist der LDAP-Display-Name , beispielsweise .($AdsiUser.InvokeSet("GivenName",$GivenName) zu benutzen. Viele Attributnamen sind in AdsiEdit.msc zu finden, aber bei weitem nicht alle.

MSDN: given-Name Attribute

Diese Informationen kann man sich ebenso selbst per Skript aus dem System besorgen, siehe Kapitel 4 Schema Klassen


Anmerkung 4) Ein weiteres Interface mit Userattributen ist MSDN: IADsUser Interface

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

LDAP-DisplayName

Eigenschaften und Methoden des IADsUser Interface

sn

lastname

givenname

firstname

commitchanges()

setinfo()

useraccountcontrol

0x00002

accountdisabled()

die beiden Aufrufe setzen beide den Vornamen des Userobjects

$AdsiUser.InvokeSet("GivenName",$GivenName)  #LDAPDisplayname
$AdsiUser.InvokeSet("FirstName",$GivenName)  #IADsUser Interface

 
Anmerkung 5) Hinweise, wie UserAccountControls zu behandeln sind, gibt es in den Beispielen am Ende von Kapitel 3 System.DirectoryServices.Directoryentry [ADSI]. Dort wird auch auf das Prinzip von [ADSI] / DirectoryEntry genauer eingegangen


Beispiel 4: Useranlage mit Daten aus einer csv-Datei

Liegen die Userdaten in einer Exceltabelle oder im CSV-Format vor, so kann man 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=Benutzer,OU=Scripting,DC=Dom2,DC=intern;A90001;Bananenbieger und Tulpenknicker;Pa$$word123;true
2;Napf;Franz-Josef;A90002;OU=Benutzer,OU=Scripting,DC=Dom2,DC=intern;A90002;Sohn von Karl Napf;Pa$$word124;true
3;Napf;Fred;A90003;OU=Benutzer,OU=Scripting,DC=Dom2,DC=intern;A90003;;Pa$$word125;false
4;Napf;Heinz;A90004;OU=Benutzer,OU=Scripting,DC=Dom2,DC=intern;A90004;Bruder von Karl Napf;Pa$$word126;false

Diese Datei herunterladen (Kap 4.1.2 Beispiel 3 .csv.txt) Kap 4.1.2 Beispiel 3 .csv.txt

Powershellcode:

Set-StrictMode -Version "2.0"
Clear-Host

$Users=Import-Csv "c:\powershell\user.csv” -delimiter ";"
$Users | foreach {
   try {
     $SamAccountName=$_.Samaccountname
     $Nummer=$_.Nummer
     $OuPath=$_.OuPath
     $CN=$_.CN
         
     #Teil 1: Useranlage
     $Class = "user"
     $ADSI=[ADSI]"LDAP://$OuPath"
         
     $User = $ADSI.Create($class,"CN=$CN")
     $User.InvokeSet("SamaccountName", $SamAccountName)
     $User.CommitChanges()

     #Teil 2 Usereigenschaften setzen
     $Description=$_.Description
     $Sn=$_.Sn
     $GivenName=$_.GivenName
     $UserPassword=$_.UserPassword

     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
     $AccountDisabled=$_.AccountDisabled
     $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

 
#Kap 4.1.2 Beispiel 3 - Useranlage aus csv-Datei.ps1.txt

Diese Datei herunterladen (Kap 4.1.2 Beispiel 3 - Useranlage aus csv-Datei.ps1.txt) Kap 4.1.2 Beispiel 3 - Useranlage aus csv-Datei.ps1.txt

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

In Kapitel 5 findet ihr ein analoges Beispiel, welches das cmdelt New-AdUser verwendet. Der Code ist kompakter, aber nicht ganz so vielseitig wie hier, da unter anderem keine Terminalservereigenschaften gesetzt werden können.
 

3 Auslesen von Userproperties

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

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

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

Beispiel 1a: Auslesen von Userkonten, die sich seit xx-Tagen nicht mehr an der Domäne angemeldet haben ([ADSI]) (aufwändige Variante!)

Das Beispiel 1a stammt noch aus meiner frühen Powershellphase :-). Es ist nicht falsch, daher lasse ich es stehen, aber mittlerweile habe ich über LDAP-Queries einen besseren Weg gefunden, der das exakt gleiche Ergebnis zurückliefert. (-> Beispiel 1b)
Bei größeren Umgebungen ist Beispiel 1b) auch sehr viel schneller als dieser Code!

Set-StrictMode -Version "2.0"
Clear-Host

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
}

#DomänenInformationen

$DomainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Domain=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe=$Domain.PdcRoleOwner.Name


#VariablenDefinition
$OuDn="OU=Benutzer,OU=Scripting" #User werden aus dieser OU gelesen
$MaxAgeInDays=90

$Users=@()

#ADSI-Binden
$AdsiOu = [ADSI]"LDAP://$PDCe/$OUdn,$domainDN"

#Userobjekte der OU auslesen und weiterverarbeiten
$Users=$AdsiOu.Children | Where {$_.ObjectClass -eq "User"}  #nur Userobjekte werden exportiert
$Users | foreach{
  $LastLogonTimeStamp=0
  Try{
  $Cn=$_.invokeget("Cn")
  $LastLogonTimeStamp=[DateTime]::FromFileTimeUtc((ConvertADSLargeInteger $_.InvokeGet("LastLogonTimestamp")))

  if($((Get-date)-$LastLogonTimeStamp).days -gt $MaxAgeInDays){
    #$_.InvokeSet("AccountDisabled",$True)
    #$_.CommitChanges()
    $IsDisabled=$_.InvokeGet("AccountDisabled")
    "{0};{1:d};{2}" -f $cn,$LastLogonTimeStamp,$isDisabled
   }#if
 }#Try

 Catch{
   "{0} hat sich noch nie an {1} angemeldet" -f $cn,$($DomainDN)
  }#Catch
} #foreach Users


#Kap 4.1.3 Beispiel 1 - Userkonten ohne Anmeldung

#mögliche Ausgabe

C15008;09.06.2012;True
C15009;24.11.2010;False
Karl Napf;17.08.2012;True

Anmerkung 1:

solange die Zeilen 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 all diese Konten zusätzlich Disabled.

Anmerkung 2:

die Funktion ConvertADSLargeInteger habei ich aus BSonPoSH: Dealing iADSLargeInteger in Powershell übernommen

Anmerkung 3: Die Children()-Methode ist einfach anzuwenden, erlaubt dafür keine rekursive Suche in SubOUs. Ist dies gewünscht, muss man die FindAll() Methode anwenden. -> Kapitel 6 Queries . Am Ende der Seite findet ihr das etwas abgewandelte Skript zum Download,welches auch das Durchsuchen von SubOus erlaubt. 

Anmerkung 4: Durch Aktivieren des Codes im Catch-Block erhält man auch alle User, die sich noch nie an der Domäne angemeldet haben
 

Beispiel 1b: Auslesen von Userkonten, die sich seit xx-Tagen nicht mehr an der Domäne angemeldet haben ([ADSI]) (kompakte Variante!)

Clear-Host
Set-Strictmode -Version "2.0"

Function Main {
  $MaxAgeInDays = 124 #Tage
  $SearchRoot = "LDAP://OU=Scripting,DC=Dom1,DC=Intern"
 
  $pwdlastsetAfter = (Get-Date).AddDays(-$MaxAgeIndays)
  $pwdlastsetAfterFileTime = $([DateTime]$pwdlastsetAfter).ToFileTime()

  $Filter = "(&(objectcategory=user)(objectclass=user)(pwdlastset>=$pwdlastsetAfterFileTime))"
 
  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.Filter = $Filter
  $DirectorySearcher.Searchroot = $SearchRoot
  $DirectorySearcher.SearchScope = "subtree"
  $DirectorySearcher.FindAll() | ForEach {
    $SamAccountName = $($_.Properties).samaccountname
    $pwdlastset = [DateTime]::FromFileTime($($_.Properties.pwdlastset))
    $IsDisabled = IsDisabled $_
    $PWDAgeInDays = $((get-date)-$pwdlastset).Days
    "{0} {1:d} {2} {3}" -f   $($SamAccountName),$pwdlastset,$PWDAgeInDays,$IsDisabled

    }
} #end function main

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

Main
#mögliche Ausgabe

Munich_Karl_1000356 13.11.2013 26 False
Munich_Karl_1000357 13.11.2013 15 False
Munich_Karl_1000358 13.11.2013 93 False
Munich_Karl_1000359 13.11.2013 126 False

Weitere Informationen und Beispiele zu LDAP-Queries habe ich hier zusammengeschrieben: Queries mit .Net

 

Beispiel 2a: Exportieren von Usernkonten (Konfiguration im Skript)

Set-StrictMode -Version "2.0"
Clear-Host

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
}

#allgemeine Informationen bestimmen
$DomainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Domain=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe=$Domain.PdcRoleOwner.Name
Try{
    $ThisScriptPath = Split-Path $($MyInvocation.InvocationName) -Parent
   }Catch{
     Write-Host "Skript vor dem ersten Einsatz bitte abspeichern!" -BackGroundColor Red
     Break
   }

#in diesem Block das Skript anpassen und die Variablen wie gewünscht setzen (Erklärung unten)
$OuDn = "OU=BenutzerA,OU=Scripting,$DomainDN"
$SearchScope = "SubTree"
$Properties = @("SamAccountName","Terminalservicesprofilepath","LastLogonTimeStamp","whenChanged","UserAccountControl")
$MyOFS = "#"
$SortedBy = "SamAccountName DESC"
$Filter = "(SamAccountName Not Like 'S*') And (UserAccountControl Like '*220')" #Nur TextFilter!!!! Keine Datum etc.!!
$CsvExportPath = "$ThisScriptPath\ExportUser.csv"

#Userobjekte in das Array $Users einlesen
$Users=@()
$DirectorySearcher=([ADSISearcher]"LDAP://$PDCe")
$DirectorySearcher.Filter="(&(ObjectCategory=User)(Objectclass=User))"
$DirectorySearcher.SearchScope= $SearchScope
$DirectorySearcher.SearchRoot="LDAP://$PDCe/$OuDn"
$DirectorySearcher.PageSize=1000  #normalerweise nicht verändern!
$DirectorySearcher.FindAll() | ForEach{
   $Users += [ADSI]($_.path)
   }  

#Anlage der DataTabelle mit Spalten für jede Property in Properties
$DataTable=New-Object System.Data.DataTable("ExportUsers")
$Properties | foreach {
  $Column = New-Object System.Data.DataColumn($_)
  $DataTable.Columns.Add($Column)
  }
 
#Befüllen der DataTable mit Daten
ForEach ($User in $Users){
  $PropertiesInRow =@()
  Foreach($Property in $Properties) {
    $OFS=$MyOFS
     #$($User.InvokeGet($Property)).ToString()
    Try{
     If($($User.InvokeGet($Property).ToString()) -eq "System.__ComObject"){  #Datumswerten
       $Date = $([DateTime]::FromFileTimeUtc((ConvertADSLargeInteger $User.InvokeGet($Property))))
       $PropertiesInRow += $Date.ToString("d")
     }#if
     ElseIf($($User.InvokeGet($Property)) -eq $null){  #nicht existente Property
       $PropertiesInRow += ''
     }#ElseIf
     ElseIf($Property -eq "UserAccountControl"){
       $PropertiesInRow += $("0x{0:x}" -f $($User.InvokeGet($Property)) )
     }#ElseIf    
     Else{
       $PropertiesInRow += [String]$($User.InvokeGet($Property))
     }#Else
   }#Try
     Catch{$PropertiesInRow += ''} #Notwendig für die TS-Eigenschaften
          
     $OFS="+"  #Zurücksetzen auf Default
  }#Foreach($Property in $Properties)
    
    $DataTable.Rows.Add($PropertiesInRow) | Out-Null
}#Foreach ($User in $Users)
 
#DataView erzeugen
$DataView = New-Object System.Data.DataView($DataTable)
#Sortieren
$DataView.Sort = "$SortedBy"
#Filtern
$DataView.RowFilter = $Filter

#Ausgabe
$DataView | Format-Table -Auto #Bildschirm
if([string]::isNullOrEmpty($CsvExportPath) -ne $true){  #CSV-Datei
  $DataView | Export-Csv -Path $CsvExportPath -Delimiter ";"
  Write-Host "Die Datei $CsvExportPath wurde erfolgreich erstellt" -ForeGroundColor Red
  }#if
#mögliche Ausgabe

SamAccountName Terminalservicesprofilepath LastLogonTimeStamp whenChanged         UserAccountControl
-------------- --------------------------- ------------------ -----------         ------------------
C15008         \\TFS01\TSProfile\C15008    09.06.2012         01/10/2013 11:31:17 0x50220           
C15005                                                        04/27/2011 09:34:21 0x220             
C15004                                                        04/27/2011 09:34:21 0x220             
C15003                                                        04/27/2011 09:34:21 0x220             
C15002                                                        01/10/2013 11:32:03 0x10220           
C15000                                                        01/10/2013 11:29:27 0x220
     
 
Die Datei C:\Powershell\MyScripts\DataTable\ExportUser.csv wurde erfolgreich erstellt      

Diese Datei herunterladen (Kap 4.1.3 Beispiel 2a - Exportieren von Usernkonten.ps1.txt) Kap 4.1.3 Beispiel 2a - Exportieren von Usernkonten.ps1.txt

Kurzbeschreibung:

Das Skript dient dazu, Userkonten mit bestimmten Attributen auszulesen. Die Konfiguration findet im SourceCode statt. Das Skript liest ausschließlich Informationen aus und verändert im AD nichts. Ein großer Vorteil ist, dass die gewünschten Attribute an nur einer Stelle im Skript "Array $Properties" gesetzt werden brauchen. Das Skript kann die meisten dieser Properties aus der AD-Datenbank auslesen und sinnvoll als String darstellen.

Features:

  • Keine Konfiguration bzgl. der Domäne erforderlich
  • Alle Anpassungen werden in einem kompakten Block innerhalb des Skripts durchgeführt
  • Abfragen werden gegen den PDCe-Emulator ausgeführt
  • OUs können auf einer Ebene oder Rekursiv durchsucht werden
  • Ausgabe auf dem Bildschirm und/ oder in einer csv-Datei
  • Ausgabe von Single- und Multivalued-Attributen (Trennzeichen konfigurierbar)
  • Ausgabe der Attribute aus dem RemoteDesktopDienste-Profile MSDN: IADsTSUserEx interface
  • Umrechnung von LargeInteger-Werten wie BadPassworttime oder LastLogonTimeStamp
  • Umrechnung des Attributs userAccountControl als Hexadezimal-Wert
  • Ausgabe ist sortierbar
  • Ausgabe ist umfangreich filterbar (Textfilter!).
    Wenn der Filter im Skript nicht ausreicht, kann man die Daten der csv-Datei weiter verarbeiten (Excel, Access, etc.)

Voraussetzungen

  • das Skript muss vor dem ersten Aufruf einmal lokal gespeichert werden.
  • läuft das Skript auf einem Win7-Client muss die tsuserex.dll registriert sein, wenn Eigenschaften aus dem RemoteDesktopDienste-Benutzerprofil verwendet werden. siehe Anmerkungen  4.1.2 Anlage von Usern -> Beispiel 2, weiter oben in diesem Kapitel

Anleitung

In dem Block "#in diesem Block das Skript anpassen und die Variablen wie gewünscht setzen" findet ihr die Variablen, mit denen ihr die Eigenschaften des Skripts anpassen könnt. Diesen Block habe ich im nächsten Beispiel 2b in eine Ini-Datei ausgelagert.


$OuDn
Aus dieser Ou werden die Objekte ausgelesen. Ihr könnt entweder den kompletten DistinguishedName verwenden, oder mit der Variablen $DomainDN" den Domänenteil aus dem Skript holen

Beispiele:
$OuDn = "OU=BenutzerA,OU=scripting,$DomainDN"
$OuDN = "OU=BenutzerA,OU=scripting,DC=Dom1,DC=Intern"
$OuDn = "$DomainDN"  #Root
$SearchScope
SearchScope definiert, ob die in OuDn angegebene Ou nur in der ersten Ebene oder rekursiv durchsucht wird

Beispiele:
$SearchScope = "OneLevel"
$SearchScope = "Subtree"
$Properties
Array der Userobjekte, die ausgelesen werden sollen.
Achtet darauf, dass bei Verwendung der Variablen "SortedBy" und "OutPutFilter" die dort verwendeten Properties auch in dieser Liste enthalten sind!

Beispiele
$Properties = @("SamAccountName","Terminalservicesprofilepath","BadPassWordTime","UserAccountControl")
$Properties = @("CN","DistinguishedName","LastLogonTimeStamp","AccountDisabled")
$MyOFS
Trennzeichen für MultiValued-Properties

Beispiele
$MyOFS = "#"
$MyOFS = "++"
$SortedBy
Sortiert nach dieser Spalte
Bei Ausgabe in eine Csv-Datei kann man die Sortierung auch per Excel oder Access mit all deren Möglichkeiten durchführen.
Achtet darauf, dass die Eigenschaft, nach der sortiert werden soll, auch in dem Array $Properties oben enthalten ist

Beispiele
$SortedBy = "SamAccountName DESC"         #absteigende Reihenfolge
$SortedBy = "SamAccountName ASC"           #aufsteigende Reihenfolge
$SortedBy = ""                                                #keine Sortierung
$SortedBy = $Null                                          #keine Sortierung
 
$CsvExportPath
Diese Variable bestimmt, ob und wohin die AusgabeDatei geschrieben wird

Beispiele
$CsvExportPath = "C:\temp\ExportUser.csv"                    #Pfad muss vorhanden sein
$CsvExportPath = "$ThisScriptPath\ExportUser.csv"        #die Datei wird im Skriptverzeichnis erstellt
$CsvExportPath = ""                                                         #Nur Bildschirmausgabe, keine Ausgabe in eine Datei
$CsvExportPath = $Null                                                    #Nur BildSchirmausgabe, keine Ausgabe in eine Datei
$Filter
Den OutPut kann man dank der DataTable-Struktur umfangreich filten.
Achtet darauf, dass die Eigenschaft, nach der sortiert werden soll, auch in dem Array $Properties oben enthalten ist

Beispiele
$Filter =  (SamaccountName LIKE 'C15*') 
$Filter =  (SamaccountName NOT LIKE 'C15*')
$Filter = ""                                                    #Keine Filterung
$Filter = $Null                                               #Keine Filterung

Auch wenn oben,schon erwähnt, als mögliche Properties sollten bei diesem und dem nächsten Skript die meisten Attribute aus diesen Schnittstellen sinnvolle Rückgabewerte liefern

Beispiel 2b: Exportieren von Usernkonten (Konfiguration in einer externen ini-Datei)

Set-StrictMode -Version "2.0"
Clear-Host
 
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 aus bsonposh.com
}

#allgemeine Informationen bestimmen
$DomainDN = ([ADSI]"LDAP://rootDSE").defaultNamingContext
$Domain=[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PDCe=$Domain.PdcRoleOwner.Name
Try{
 $ThisScriptPath = Split-Path $($MyInvocation.InvocationName) -Parent
 }Catch{
 Write-Host "Skript bitte abspeichern!" -BackGroundColor Red
  Break
 }

#gegebenfalls hier den Pfad zur Input.ini setzen. Dort das Skript weiter konfigurieren
#$InputPath = "c:\temp\Input.ini"
$InputPath = "$ThisScriptPath\Input.Ini" #hier liegt das Skripts selbst

#Einlesen der Werte aus der CSV-Datei in eine Hashtable
$VariableHash=@{}
$(Get-Content $InputPath) | foreach{
  #Kommentare in der ini-Datei die mit # oder $null oder " " beginnen ignorieren
  if(($_[0] -ne "#") -and ($_[0] -ne $Null) -and ($_[0] -ne " ")) {
    $Key=$($_.Split(";"))[0]
    $Value=$($_.Split(";"))[1]
    #Falls Keys mehrfach auftauchen
    Try{
      $VariableHash.Add($Key,$Value)
    }Catch{"Key `"$Key`" mehrfach gesetzt"} #if
    }#if
  }#foreach
if($VariableHash.count -eq 0) {"wahrscheinlich ist `"$InputPath`" nicht vorhanden"}
 
#automatisiertes Setzen der Variablen aus der Hashtable
$OuDn = $VariableHash.Get_item("OuDN")
$SearchScope = $VariableHash.Get_item("SearchScope")
$Properties = $($VariableHash.Get_item("Properties")).Split(",")
$myOFS = $VariableHash.Get_item("myOFS")
$SortedBy = $VariableHash.Get_item("SortedBy")
If(($SortedBy -like "*False") -or ($SortedBy -like "*Null")) {
  $SortedBy = $Null
 }
$CsvExportPath = $VariableHash.Get_item("CsvExportPath")
If(($CsvExportPath -like "*False") -or ($CsvExportPath -like "*Null"))  {
  $CsvExportPath = $False
 }Elseif($CsvExportPath -like ".")  {
  $CsvExportPath = "$ThisScriptPath\ExportUser.csv"
  }
if([string]::isNullOrEmpty($CsvExportPath) -eq $true){  #CSV-Datei
  $CsvExportPath = "$ThisScriptPath\UserExport.csv"
  }
$Filter = $VariableHash.Get_item("OutPutFilter")
If(($Filter -like "*False") -or ($Filter -like "*Null")) {
  $Filter = $Null
  }
 
#Userobjekte in das Array $Users einlesen
$Users=@()
$DirectorySearcher=([ADSISearcher]"LDAP://$PDCe")
$DirectorySearcher.Filter="(&(ObjectCategory=User)(Objectclass=User))"
$DirectorySearcher.SearchScope= $SearchScope  #Subtree oder Onelevel
$DirectorySearcher.SearchRoot="LDAP://$PDCe/$OuDn"
$DirectorySearcher.PageSize=1000  #normalerweise nicht verändern!
$DirectorySearcher.FindAll() | ForEach{
   $Users += [ADSI]($_.path)
   }  

#Anlage der DataTabelle mit Spalten für jede Property in Properties
$DataTable=New-Object System.Data.DataTable("ExportUsers")
$Properties | foreach {
  $Column = New-Object System.Data.DataColumn($_)
  $DataTable.Columns.Add($Column)
 }
 
#Befüllen der DataTable mit Daten
ForEach ($User in $Users){
  $PropertiesInRow =@()
  Foreach($Property in $Properties) {
    $OFS=$myOFS
    #$($User.InvokeGet($Property)).ToString()
    Try{
     If($($User.InvokeGet($Property).ToString()) -eq "System.__ComObject"){  #Datumswerte)
       $Date = $([DateTime]::FromFileTimeUtc((ConvertADSLargeInteger $User.InvokeGet($Property))))
       $PropertiesInRow += $Date.ToString("d")
     }#If
     ElseIf($($User.InvokeGet($Property)) -eq $null){
       $PropertiesInRow += ''
     }#ElseIf
     ElseIf($Property -eq "UserAccountControl"){
       $PropertiesInRow += $("0x{0:x}" -f $($User.InvokeGet($Property)) )
     }#Elseif    
     Else{
       $PropertiesInRow += [String]$($User.InvokeGet($Property))
     }#Else
   }#Try
     Catch{$PropertiesInRow += ''} #Notwendig für die TS-Eigenschaften
          
     $OFS="+"  #Zurücksetzen auf Default
 }#Foreach($Property in $Properties)
    
    $DataTable.Rows.Add($PropertiesInRow) | Out-Null
}#Foreach ($User in $Users)
 
#DataView erzeugen
$DataView = New-Object System.Data.DataView($DataTable)
#Sortieren
$DataView.Sort = "$SortedBy"
#Filtern
$DataView.RowFilter = $Filter

#Ausgabe
$DataView | Format-Table -Auto #Bildschirm
if([String]::isNullOrEmpty($CsvExportPath) -ne $true){  #CSV-Datei
  $DataView | Export-Csv -Path $CsvExportPath -Delimiter ";"
  Write-Host "Die Datei $CsvExportPath wurde erfolgreich erstellt" -ForeGroundColor Red
 }#if   
#mögliche Ausgabe

SamAccountName Terminalservicesprofilepath BadPassWordTime UserAccountControl
-------------- --------------------------- --------------- ------------------
Karl_Napf      \\TFS02\TSProfile\Napf      09.06.2012      0x50200           
Heinz_Hinz     \\TFS02\TSProfile\Hinz      01.01.1601      0x202             
C15042                                     10.01.2013      0x150222          
C15041         \\TFS02\TSProfile\C15041    01.01.1601      0x222             
C15039         \\TFS01\TSProfile\C15039    01.01.1601      0x40222           

       
Die Datei c:\temp\test.csv wurde erfolgreich erstellt

 Kap 4.1.3 Beispiel 2b - Exportieren von Usernkonten.ps1.txt

Beispiel einer Ini-Datei für das letzte Skript

#Beispiel für eine Input.ini  
 
#In dieser Datei die Variablen für das Skript setzen. Bitte keine Anführungszeichen verwenden
#Als Trennzeichen den Strichpunkt ";" benutzen
#Links vom Strichpunkt den Variablennamen, rechts den oder die durch Komma getrennten Werte
#Die Reihenfolge der Variablen sowie Groß- Kleinschreibung sind egal.
#Kommentare sollten mit dem #-Zeichen beginnen

#Bedeutungen
#OuDn: Aus dieser Ou werden die Objekte ausgelesen.
#SearchScope: wird die in OuDn angegebene Ou nur in der ersten Ebene oder rekursiv durchsucht
#Properties: Array der Userobjekte, die ausgelesen werden sollen.
#MyOFS: Trennzeichen für MultiValued-Properties
#SortedBy: Sortiert nach dieser Spalte
#CsVExportPath: Diese Variable bestimmt, ob und wohin die AusgabeDatei geschrieben wird
#Filter: Den OutPut kann man dank der DataTable/ Dataview-Struktur gut filten.

#Skriptparameter setzen
OuDn;OU=BenutzerA,OU=scripting,DC=Dom1,DC=Intern  
SearchScope;SubTree
Properties;SamAccountName,Terminalservicesprofilepath,BadPassWordTime,UserAccountControl
MyOFS;#
SortedBy;SamAccountName DESC
CsVExportPath;c:\temp\test.csv
Filter;SamaccountName NOT LIKE 'C1500*'

# weitere Beispiele:
# OuDn;OU=BenutzerA,OU=scripting,DC=Dom1,DC=Intern
# OuDN;DC=Dom1,DC=Intern   #Root
   
# Properties;SamAccountName,Terminalservicesprofilepath,BadPassWordTime,UserAccountControl
# Properties;CN,DistinguishedName,LastLogonTimeStamp,AccountDisabled
# Properties;SamAccountName,Memberof

# MyOFS;#
# MyOFS;"++"

# SortedBy;SamAccountName DESC"   #DESC, ASC
# SortedBy;""                               #keine Sortierung
# SortedBy;False ($False, Null, $Null)      #keine Sortierung

# CsVExportPath;False ($False, Null, $Null) #keine Ausgabe
# CsVExportPath;.                           #Ausgabe in den SkriptPfad
# CsVExportPath;c:\temp\test.csv

# Filter;SamaccountName LIKE 'C150*'
# Filter;False ($False, Null, $Null)  #keine Filterung                     

 Kap 4.1.3 Beispiel 2b - Input.ini.txt

Im Gegensatz zum Skript aus dem letzten Beispiel wird hier die Konfiguration nahezu vollständig in einer externen Input.ini vorgenommen. Einzig wenn die Input.ini nicht im SkriptPfad liegen soll, muss man den Pfad im Skript selbst anpassen. Ansonsten gelten dieselben Voraussetzungen und Features wie oben.

Die beiden letzten Skripte sind doch etwas länger geraten. Ich habe daher, um den Rahmen nicht noch weiter zu sprengen, nicht jeden möglichen EingabeFehler abgefangen. Testet daher bitte vor einem produktiven Einsatz das Skript mit der üblichen Sorgfalt.

4 Löschen eines User 

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