[PowerShell] Find-ADUser (Module zur Suche von AD-User im gesamten AD-Forest)

DPXone

Lieutenant
Registriert
Mai 2009
Beiträge
554
Hi,

hier mal eine Funktion (kann als Script und Module eingebunden werden), das ich damals aus eigener Initiative heraus auf die schnelle geschrieben habe.
Mit dieser Funktion lassen sich AD-User-Accounts im gesamten Active Directory Forest finden (nicht nur in der aktuellen Domäne).

Profitiere ich von dieser Funktion?

Diese Funktion richtet sich aufgrund der Forest-Suche im Global Catalog hauptsächlich erstmal nur an Multi-Domain-Forests.
Aufgrund der Switches, die die Funktion bietet, sollte es sich aber auch verinzelt für Single-Domain(-Forests)-Suchen eignen.

Diese Funktion ist, so gut es bisher möglich war, auf Performance ausgerichtet.
Deshalb gibt es u.a. keine Pipelines.
Alles wird über GenericList's, ForEach-Statement Keywords (NICHT: ForEach-Object alias %) und Hashtable's abgearbeitet.

Nach was kann man suchen, ohne das AD(/LDAP)-Attribute anzugeben?
  • CommonName (CN; i.d.R. auch der DisplayName; Kann aber auch anders sein!) (Wildcard * erlaubt)
  • SamAccountName: (=Username ohne Domain, z.B. abcEXF1) (Wildcard * erlaubt)
  • UserPrincipalName:(Username in Kombination mit der Domäne, z. B. MyUsernName@MyDomain.i) (Wildcard * erlaubt)
  • Surname (e.g. Nachname der Person) (Wildcard * erlaubt)
  • GivenName (e.g. Vorname der Person) (Wildcard * erlaubt)
  • Mail (e.g. Nachname der Person) (Wildcard * erlaubt)
  • SID (Kein Wildcard erlaubt!)
  • GUID (Kein Wildcard erlaubt!)
  • DistinguishedName (Kein Wildcard erlaubt!)


Welche Beispiele gibt es?

... folgt.
Bis dahin:
Im Multi-Domain-Forest mit dem Script einfach mal nach einem User anhand des Usernames oder der Mail-Adresse suchen, der nicht in der eigenen Domäne existiert.​


Welche Einschränkungen gibt es?

Die größte Einschränkung liegt im "Global Catalog".
Mein Script fragt standardmäßg nur den Global Catalog ab!
.
Ausnahme bildet der Switch -QueryCorrespondingDC

Mein Script kann, per Default, nur das finden, was im globalen Katalog (Global Catalog = GC) vorhanden ist!
Aber deshalb bitte nicht zu viele Eigenschaften durch den Forest replizieren!!!!.
Das produziert andere Probleme.​

Minimaler Exkurs der Nachteile vom Global Catalog im Generellen:
  • Gruppenmitgliedschaften, außer die von universellen Gruppen, werden nicht gesynct und sind im GC deshalb nicht abrufbar (betrifft diese Funktion nicht!)
  • fehlgeschlagene Anmeldeversuch sind am GC NICHT abrufbar
  • Anzahl der fehlgeschlagenen Anmeldeversuche sind am GC NICHT abrufbar
  • der TimeStamp der letzten Anmeldung ist am GC NICHT abrufbar
  • ....... und alles andere, was nicht in den GC gesynct wird.

Um für jeden User wirklich alle Details halbwegs abzufragen, gibt es den Switch $QueryCorrespondingDC.
Dadurch wird jeder User, am jeweils verantwortlichen DC per Domain, abgefragt, was bei größeren Abfragen, in größeren Forests, seine Zeit in Anspruch nimmt.​

Was bringt mir das Script (in Forests/mehreren Domänen)?

Man kann auf leichte Art und Weise nach User-Accounts suchen anhand der/s UPN, GUID, SID, DisplayNames/CN, Mail-Address, etc... suchen und
bekommt zusätzlich auch die Info, in welcher Domäne sich der User-Account befindet und welchen DC man für detaillierte Informationen abfragen sollte.​

Beispiele
  • Find-AdUser MyLastName
  • Find-AdUser han*
  • Find-AdUser "XXX*@MyDomain.i"
  • Find-Aduser "MyFisrtName.MyLastName@MyDomain.MyTLD"
  • Find-AdUser [SID]
  • Find-AdUser [objectGUID] -QueryCorrespondingDC
  • Find-AdUser *@hanswurst.de
  • Find-AdUser "MyFirstName MyLastName (MyCompanyName)"
  • Find-AdUser "MyFirstName MyLastname *"
  • "Hans*","Peter*",Gustav*" | Find-AdUser -QueryCorrespondingDC

Hinweise zu den Parametern [Nicht vollständig und richtig, da in Überarbeitung]:
  • $UserState
    Filter auf den Status des AD-User-Accounts (Enabled oder Disabled)
    Default = EGAL
    Mit "TRUE" kann man das Ergebnis auf aktive AD-Accounts beschränken.
  • $AddToDefaultProperties
    Damit lassen sich zusätzliche AD-Properties zum Ergebnis hinzufügen.
    Standardmäßig werden folgende Eigenschaften abgefragt und ausgegeben:
    'DisplayName' , 'CN' , 'SamAccountName' , 'Surname' , 'GivenName' , 'company' , 'mail' , 'Enabled' , 'UserPrincipalName' , 'SID' , 'ObjectGUID
  • $QueryCorrespondingDC
    Fragt für die Eigenschaften des AD-Accounts, den verantwortlichen Domain-Controller, der jeweiligen Domäne ab (nur in Forests/mehreren Domänen sinnvoll).

PowerShell:
Function Find-ADUser { 
	<#
    .Synopsis
   
    .DESCRIPTION
    
        - The variables $CachedDomainsHashT and $CachedDCsHashT are created in global scope.
          Therefore these variables are available for all subsequent calls and other scripts.
    .EXAMPLE
   
    .EXAMPLE
   
    .NOTES

    #> 
	[CmdletBinding(DefaultParameterSetName = 'Properties')] 
	Param (
		[Alias("N")] 
		[Parameter(Mandatory = $true , Position = 0 , ValueFromPipeline = $true , ValueFromRemainingArguments = $true)] 
		[string[]] $Name , 
		
		[Alias("US")] 
		[ValidateSet($True , $False)] 
		[String] $UserState = $null , 
		
		[Alias("P")] 
		[Parameter(ParameterSetName = 'Properties')] 
		[System.Collections.Generic.List[string]] $Properties = ('DisplayName' , 'CN' , 'SamAccountName' , 'Surname' , 'GivenName' , 'company' , 'mail' , 'Enabled' , 'UserPrincipalName' , 'SID' , 'ObjectGUID') , 
		
		[Alias("AP")] 
		[Parameter(ParameterSetName = 'AddToDefaultProperties')] 
		[System.Collections.Generic.List[string]] $AddToDefaultProperties , 
		
		[Alias("P2k")] 
		[switch] $ShowPreWindows2000Name , 
		
		[Alias("DC")] 
		[switch] $ShowDC , 
		
		[Alias("Q")] 
		[switch] $QueryCorrespondingDC , # not GlobalCatalog              
		
		[Alias("FD")] 
		[switch] $ForceDcDiscovery # force domain controller discovery (clears cached domain controller information)              
	) 
	
	Begin { 
		If (-not $PSCmdlet.MyInvocation.ExpectingInput) { # If not Pipeline-Input                                   
			[string] $Name = $name -join ' ' 
		} 
		
		$Properties.AddRange([System.Collections.Generic.List[String]]('CanonicalName' , 'DistinguishedName')) 
		$Properties = $Properties | Sort-Object -Unique 
		
		
		If (-not($AddToDefaultProperties -eq $null)) { $Properties.AddRange($AddToDefaultProperties) } 
		
		$GlobalCatalogDC = Get-ADDomainController -NextClosestSite -Discover -ForceDiscover -Service PrimaryDC 
		$ServerName = $GlobalCatalogDC.HostName 
		$port = 3268 # Global Catalog                                                                                  
		$userList = [System.Collections.Generic.List[psobject]]::new() 
		
		If ($global:CachedDCsHashT -isnot [hashtable]) { 
			$global:CachedDCsHashT = @{ } # cache DCs in global scope for subsequent queries     
		} 
		If ($global:CachedDomainsHashT -isnot [hashtable]) { 
			$global:CachedDomainsHashT = @{ } # cache DCs in global scope for subsequent queries     
		} 
		
		If ($UserState -notin '' , $null) { 
			$Filter = {((DistinguishedName -eq $N) -or (ObjectGUID -eq $N) -or (UserPrincipalName -like $N) -or (sAMAccountName -like $N) -or (mail -like $N) -or (givenname -like $N) -or (sn -like $N) -or (Displayname -like $N) -or (cn -like $N)) -and (Enabled -eq $UserState) } 
		} Else { 
			$Filter = {((DistinguishedName -eq $N) -or (ObjectGUID -eq $N) -or (UserPrincipalName -like $N) -or (sAMAccountName -like $N) -or (mail -like $N) -or (givenname -like $N) -or (sn -like $N) -or (Displayname -like $N) -or (cn -like $N)) } 
		} 
		
		$QueryParameter = @{ 
			Filter = $Filter 
			Server = "$ServerName`:$port" 
		} 
	} 
	
	Process { 
		Foreach ($N In $Name) { 
			$users = Get-ADUser @QueryParameter -Properties $Properties 
			
			$UniqueDomainsHashT = @{ } 
			$CanonicalNames = $users.CanonicalName 
			Foreach ($CanonicalName In $CanonicalNames) { 
				$DomainName = ($CanonicalName -split '/')[0] 
				If (! $UniqueDomainsHashT.ContainsKey($DomainName)) { 
					$UniqueDomainsHashT[$DomainName] = '' 
				} 
			} 
			$UniqueDomains = $UniqueDomainsHashT.Keys 
			
			If ($ShowPreWindows2000Name) { 
				If ($global:CachedDomainsHashT -notin '' , $null) { # caching domains avoids unnecessary subsequent queries for the same domain     
					$domainsDiff = Compare-Object -ReferenceObject $global:CachedDomainsHashT.Keys -DifferenceObject $UniqueDomains # hashtable key is domain name     
					
					$DomainsToQuery = Foreach ($diff In $domainsDiff) { 
						If ($diff.Sideindicator -eq '=>') { 
							$diff.InputObject 
						} 
					} 
				} Else { 
					$DomainsToQuery = $UniqueDomains 
				} 
				
				If ($DomainsToQuery -notin '' , $null) { 
					Foreach ($DomainName In $DomainsToQuery) { 
						$Domain = Get-ADDomain -Identity $DomainName 
						$global:CachedDomainsHashT[$DomainName] = $Domain 
					} 
				} 
			} 
			
			If ($ShowDC -or $QueryCorrespondingDC) { 
				If ($global:CachedDCsHashT -notin '' , $null) { # caching DCs of domains avoids unnecessary subsequent queries for the same domain     
					$dcDiff = Compare-Object -ReferenceObject $global:CachedDCsHashT.Keys -DifferenceObject $UniqueDomains # hashtable key is domain name     
					
					$DomainsToQueryForDCs = Foreach ($diff In $dcDiff) { 
						If ($diff.Sideindicator -eq '=>') { 
							$diff.InputObject 
						} 
					} 
				} Else { 
					$DomainsToQueryForDCs = $UniqueDomains 
				} 
				
				If ($DomainsToQueryForDCs -notin '' , $null) { 
					Foreach ($DomainName In $DomainsToQueryForDCs) { 
						$DC = Get-ADDomainController -DomainName $DomainName -Discover -ForceDiscover: $ForceDcDiscovery 
						$global:CachedDCsHashT[$DomainName] = $DC 
					} 
				} 
				
				If ($QueryCorrespondingDC) { 
					$users = Foreach ($user In $Users) { 
						$DomainName = ($user.CanonicalName -split '/')[0] 
						$DC = $global:CachedDCsHashT[$DomainName].Hostname[0] 
						Get-ADUser $user.DistinguishedName -Server $DC -Properties $Properties 
					} 
				} 
			} 
			
			Foreach ($user In $users) { 
				$DomainName = ($user.CanonicalName -split '/')[0] 
				Add-Member -InputObject $user -MemberType NoteProperty -Name 'Domain' -Value $DomainName -Force 
				
				If ($ShowPreWindows2000Name) { 
					$DomainNetBiosName = $global:CachedDomainsHashT[$DomainName].NetBiosName 
					$PreWindows2000Name = "$($DomainNetBiosName)\$($User.SamAccountName)" 
					Add-Member -InputObject $user -MemberType NoteProperty -Name 'preWindows2000Name' -Value $PreWindows2000Name -Force 
				} 
				
				If ($ShowDC -or $QueryCorrespondingDC) { 
					$DcHostName = $global:CachedDCsHashT[$DomainName].Hostname[0] 
					Add-Member -InputObject $user -MemberType NoteProperty -Name 'DomainController' -Value $DcHostName -Force 
				} 
				
				$userList.Add($user) 
			} 
		} 
	} 
	
	End { 
		If ($userList) { 
			$FinalPropertiesHashT = @{ } 
			
			Foreach ($prop In $Properties) { 
				If ($prop -notmatch '\*') { 
					If (! $FinalPropertiesHashT.ContainsKey($prop)) { 
						$FinalPropertiesHashT[$prop] = $null 
					} 
				} 
			} 
			
			Foreach ($item In $userList) { 
				$itemProps = (Get-Member -InputObject $item -MemberType Properties).Name 
				
				Foreach ($itemProp In $itemProps) { 
					If ($itemProp -notin @('PropertyNames' , 'AddedProperties' , 'RemovedProperties' , 'ModifiedProperties' , 'PropertyCount')) { 
						If (! $FinalPropertiesHashT.ContainsKey($itemProp)) { 
							$FinalPropertiesHashT[$itemProp] = $null 
						} 
					} 
				} 
			} 
			
			$FinalPropertiesName = $FinalPropertiesHashT.Keys | Sort-Object 
			
			Foreach ($item In $userList) { 
				Select-Object -InputObject $item -Property $FinalPropertiesName 
			} 
		} 
	} 
}

Updates:
  • 2018-03-29:
    • Performance-Verbesserungen durch Verringerung der Domains- und DC-Abfragen mithilfe von zwischengespeicherten Hash-Tables.
    • Switch ($ShowPreWindows2000Name) für Ausgabe des Usernames im PreWin2000-Format hinzugefügt.
    • Diverse Anpassungen für Performance-Verbesserungen
  • 2019-02-05:
    • Überarbeitung der Paramter
    • Diverse Performance-Verbesserungen
 
Zuletzt bearbeitet:
Kann man nicht einfach per Get-ADForest und dann for each Domain nach den Usern suchen? Dann hat man nicht das Problem mit dem Global Catalog.
 
Sowas in der Art?

Code:
(Get-AdForest).Domains | % { Get-AdUser -Server $_ -Filter *}

wobei an dieser Stelle der -Filter natürlich weiter parametrisiert werden kann, sei es über den sAMAccountName oder sonstworüber.

Und dran denken,daß -Filter einen String will, keinen Scriptblock. Die Online-Hilfe ist da ein wenig irreführend. Mit den { } um den Filter funktioniert es nicht, wenn man parametrisiert filtern will, nicht als [ScriptBlock] und auch nicht, wenn die {} Teil des Filterstrings sind.
 
Renegade334 schrieb:
Kann man nicht einfach per Get-ADForest und dann for each Domain nach den Usern suchen? Dann hat man nicht das Problem mit dem Global Catalog.

Theoretisch schon. Aber glaub mir, wenn du in einem Forest unterwegs bist, bei dem die Domänen komplett verstreut auf der Welt gehostet werden, merkst du, was ein Global Catalog ausmacht (für was ein GC geschaffen wurde).
Dieses Skript ist bewusst dafür da, um im Handumdrehen Ergebnisse zu liefern.
Das bloße abfragen jeder Domäne mag zwar plausibel klingen, aber nicht in einem WW-DomänenNetzwerk.
Der GlobalCatalog liegt schließlich auf jedem DC bereit. Also auch vor Ort bei dir im Netzwerk (sofern es keine Site ohne DC ist).

Ich hab ein Skript am laufen, welches einige Details von Mitgliedern von universellen Gruppen im Forest abfragt.
Das Skript schafft es weltweit > 40.000 User auf einmal abzufragen in kürzester Zeit (<1 Minute; paar Sekunden um genau zu sein).
Würde ich jede AD-Gruppe und für jeden AD-Account die jeweilige Domäne abfragen, würde sich die Zeit enorm erhöhen (und mit "enorm" meine ich teilweise Zeiten von > 10 Minuten).
Jeder Befehl geht hier schließlich von deinem Standort an den jeweiligen DC im jeweiligen Standort.

PS:
Ich hab ja bewusst den Paramter $QueryCorrespondingDC eingebaut, um Attribut-Werte zu erhalten, die im Forest nicht standardmäßig in den GC gesynct werden.
 
Zuletzt bearbeitet:
Muß dann aber damit leben, daß der GC eben nicht "alle" Informationen vorhält. Lokale Gruppen finden sich da beispielsweise nicht drin.

Jetzt weiß ich ja nicht, wie Du das implementiert hast (oder implementieren würdest) und auch nicht, wie die Netzwerksituation aussieht... wäre es vielleicht eine Idee, Abfragen teilweise zu parallelisieren? Man muß die auf der Welt verteilten DC ja nicht der Reihe nach abfragen.
 
RalphS schrieb:
Muß dann aber damit leben, daß der GC eben nicht "alle" Informationen vorhält. Lokale Gruppen finden sich da beispielsweise nicht drin.

Jetzt weiß ich ja nicht, wie Du das implementiert hast (oder implementieren würdest) und auch nicht, wie die Netzwerksituation aussieht... wäre es vielleicht eine Idee, Abfragen teilweise zu parallelisieren? Man muß die auf der Welt verteilten DC ja nicht der Reihe nach abfragen.

Bei meinem genannten Beispiel der AD-Gruppen-Abfrage kommen universelle Gruppen zum Einsatz. Ist aber eigentlich gerade auch nicht ganz das Thema. Hier geht's ja um die Abfrage von AD-Usern und nicht AD-Gruppen ;)
Das Ding ist halt, wenn du z. B. eine GUID sucht, dann würdest du total unnötig diverse Domänen absuchen, weil der AD-User hinter der GUID nur einmal existiert. Diese Abfrage macht mein Script in < 1 Sekunde.
 
Eh, die Gruppen kommen nicht zu Einsatz, sondern die Gruppen sind da, oder eben auch nicht da. :)

Wir sind uns auch sicherlich einig, daß "einen GC schnell abfragen" sehr viel sinnvoller ist als "alle möglichen DC kontaktieren und nerven und nach ihren Infos befragen". Jedenfalls dann, wenn man vom GC dieselbe Antwort bekommt, die man von den DCs bekommen hätte (quantitativ).

Aber, genau an dem letzteren Punkt stotter ich ein bißchen, weil ich meine mich zu erinnern, daß im GC eben nicht die gesamte Information da ist (wieder quantitativ; qualitativ liegt sowieso auf der Hand).

Wenn das aber natürlich so ist und wenn Du vom GC das geliefert bekommst was Du brauchst und auch nix dabei unter den Tisch fällt (mal von Replikationsverzögerungen abgesehen) dann ist doch alles perfekt. :)
 
RalphS schrieb:
Aber, genau an dem letzteren Punkt stotter ich ein bißchen, weil ich meine mich zu erinnern, daß im GC eben nicht die gesamte Information da ist (wieder quantitativ; qualitativ liegt sowieso auf der Hand).

Wenn das aber natürlich so ist und wenn Du vom GC das geliefert bekommst was Du brauchst und auch nix dabei unter den Tisch fällt (mal von Replikationsverzögerungen abgesehen) dann ist doch alles perfekt. :)

Da hast du schon recht, dass nicht alle Infos in den GC repliziert sind.

Man muss ja folgende Replizierungs-Details im Hinterkopf behalten:
  • Im GC sind standardmäßig nur von Microsoft als wichtig und oft replizierbare Informationen enthalten (sAMAccountName, UserPrincipalName, mail, objectGUID, sn, givenName, uvm.) <- Sind auch ausreichend; für Gruppenabfragen hab ich noch ein (seit langer Zeit) nicht fertig geschriebenes Modul (hatte keine Zeit zur Fertigstellung). Das betrifft aber wie gesagt keine direkte User-Abfragen, sondern nur wenn man die Member oder Details von einer lokalen/globalen Gruppe möchte.
  • Innerhalb der Domain werden auf den DCs Mitgliedschaften von lokalen und globalen Gruppen bzw. die Gruppen an sich repliziert. Zudem werden auch nicht im GC vorhandene User-Infos repliziert.
  • Für LastLogonTimestamps, badpwdcount, lastLogon, logonCount, lastLogoff etc muss man sogar direkt alle DCs innerhalb der Domain abfragen. Da hilft es nicht irgendeinen DC abzufragen. Man muss kumulieren bzw. den kleinsten oder größten Wert wählen.

Was mein Script ja auch von Haus aus bietet, ist, die Domäne und einen verantwortlichen DC in der jeweiligen Domäne im Ergebnis zurückzugeben.
Heißt: Ich suche nach einen sAMAccountName und bekomme alle GC replizierten Infos + Domäne + DC in der Domäne zurück und kann damit schnell und einfach (innerhalb eines eigenen Scripts) weitere Dinge einholen/bearbeiten, oder man benutzt einfach den Switch $QueryCorrespondingDC, dadurch bekommt man die Details, die der DC in der Domäne in der der User gehostet wird, zurück (also ohne GC).

Möchte man mehr User-Infos im GC, gibt es ja das AD Schema, das man hierfür bearbeiten kann.
Hab mir vor längerer Zeit hierfür ein Excel mit VBA gebastelt (paar Code-Snipsel aus dem Netz + Erweiterungen der Snipsel und schöne Aufbereitung meinerseits; Quellen stehen im VBA Code ), das die aktuelle Domäne (das Schema) nach LDAP-Attributen und den Flags abfragt.

PS: Habs mal angehängt. Einfach den Button oben klicken, wenn in einer Domäne.
Modul4 wird nicht genutzt. War damals nur ein Versuch.
 
Zuletzt bearbeitet:
Ja okay, qualitativ ist ja eh klar.... daß da nicht zu jedem Objekt alle Eigenschaften da sind.

Aber wenn alle Objekte, die da sein sollten, auch wirklich da sind (quantitativ) dann paßt das ja.

AD-Schema für sowas würd ich mir jetzt allerdings nicht antun. Abfragen ja, aber Schemaänderungen sind buchstäblich GANZ OBEN, und nicht immer hat man den Zugriff auf "ganz oben" und richtig machen muß man es auch, sonst fällt einem mit Pech irgendeine Applikation (oder ein OS-Upgrade mit Schema-Upgrade) auf die Füße. Reicht schon, daß man im Forest nur eine Exchange-Version haben kann. Da muß man nicht noch nachhelfen mit unbedachten Schemaänderungen. :)
 
Zurück
Oben