add group export

This commit is contained in:
2026-06-17 08:14:10 +02:00
parent 2f271265e9
commit e98660134a
8 changed files with 558 additions and 0 deletions
+87
View File
@@ -29,6 +29,7 @@ function Invoke-ApiHandler {
"GET /api/groups" { return Get-GroupsEndpoint -Query $Query }
"GET /api/groups/rpa" { return Get-RpaGroupsEndpoint }
"GET /api/groups/search" { return Search-GroupsEndpoint -Query $Query }
"POST /api/groups" { return New-GroupEndpoint -Body $Body }
"POST /api/groups/check" { return Test-GroupNameEndpoint -Body $Body }
@@ -45,6 +46,11 @@ function Invoke-ApiHandler {
}
# 2-segment fallbacks (z.B. /api/groups/<id>/members)
# Export-Route VOR der generischen /members-Route pruefen
if ($Method -eq "GET" -and $Path -match "^/api/groups/([^/]+)/members/export$") {
return Get-GroupMembersExportEndpoint -GroupId $matches[1]
}
if ($Method -eq "GET" -and $Path -match "^/api/groups/([^/]+)/members$") {
return Get-GroupMembersEndpoint -GroupId $matches[1]
}
@@ -1185,6 +1191,87 @@ function Get-GroupMembersEndpoint {
return @{ items = $items; count = $items.Count }
}
function Search-GroupsEndpoint {
param($Query)
$err = Test-Connected
if ($err) { return $err }
$term = [string]$Query.q
if ([string]::IsNullOrWhiteSpace($term) -or $term.Length -lt 2) {
return @{ items = @(); count = 0 }
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$raw = @(Search-GraphGroups -SearchTerm $term -Top 50)
$sw.Stop()
Write-Host " [GSEARCH] '$term': $($raw.Count) Treffer in $($sw.ElapsedMilliseconds)ms" -ForegroundColor DarkGray
$items = @()
foreach ($g in $raw) {
$id = if ($g.id) { $g.id } else { $g.Id }
$name = if ($g.displayName) { $g.displayName } else { $g.displayname }
if ($id) {
$items += [pscustomobject]@{
Id = [string]$id
DisplayName = [string]$name
Description = [string]$g.description
}
}
}
return @{ items = $items; count = $items.Count }
}
function Get-GroupMembersExportEndpoint {
# Loest alle Benutzer einer Gruppe auf, einschliesslich Mitglieder aus
# verschachtelten Unter-Gruppen. Gibt die flache, deduplizierte Benutzer-
# liste mit SourcePath-Angabe zurueck — das Frontend erzeugt daraus den CSV.
param([string]$GroupId)
$err = Test-Connected
if ($err) { return $err }
if ([string]::IsNullOrWhiteSpace($GroupId)) {
return @{ __status = 400; error = "GroupId fehlt" }
}
$groupName = $GroupId
try {
$g = Get-GraphGroupById -Id $GroupId -Property @("id","displayName")
if ($g.displayName) { $groupName = [string]$g.displayName }
} catch {
return @{ __status = 404; error = "Gruppe nicht gefunden: $($_.Exception.Message)" }
}
Write-Host "[EXPORT] Starte Aufloesung: '$groupName' ($GroupId)" -ForegroundColor Cyan
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$allUsers = @(Resolve-GroupMembersWithNesting -GroupId $GroupId -GroupName $groupName)
# Deduplizieren: erster Treffer (= direktes Mitglied) gewinnt
$seen = @{}
$unique = [System.Collections.Generic.List[object]]::new()
$dups = 0
foreach ($u in $allUsers) {
if (-not $seen.ContainsKey($u.Id)) {
$seen[$u.Id] = $true
$unique.Add($u)
} else {
$dups++
}
}
$sw.Stop()
Write-Host " -> $($unique.Count) eindeutige Benutzer, $dups Duplikate, $($sw.ElapsedMilliseconds)ms" -ForegroundColor Green
return @{
groupId = $GroupId
groupName = $groupName
users = $unique.ToArray()
count = $unique.Count
duplicates = $dups
resolvedMs = [int]$sw.ElapsedMilliseconds
}
}
# ============================================================
# Users
# ============================================================
@@ -101,6 +101,83 @@ function Get-GraphGroupMembers {
return Get-GraphPaged -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members?`$select=id,displayName,userPrincipalName"
}
function Search-GraphGroups {
# Sucht Gruppen nach displayName. Versucht zuerst startswith (schnell),
# dann $search-Fallback (Substring, braucht ConsistencyLevel: eventual).
param([string]$SearchTerm, [int]$Top = 50)
$term = $SearchTerm -replace "'", "''"
try {
$filter = "startswith(displayName,'$term')"
$uri = "https://graph.microsoft.com/v1.0/groups?`$select=id,displayName,description&`$filter=$([uri]::EscapeDataString($filter))&`$top=$Top"
$resp = Invoke-MgGraphRequest -Uri $uri -Method GET
$items = @()
if ($resp -and $resp.value) { $items = @($resp.value) }
if ($items.Count -gt 0) { return $items }
} catch {
Write-Host " [GSEARCH] startswith fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor DarkYellow
}
try {
$clean = $SearchTerm -replace '"', ''
$searchExpr = [uri]::EscapeDataString('"displayName:' + $clean + '"')
$uri = "https://graph.microsoft.com/v1.0/groups?`$select=id,displayName,description&`$search=$searchExpr&`$top=$Top"
$resp = Invoke-MgGraphRequest -Uri $uri -Method GET -Headers @{ ConsistencyLevel = "eventual" }
if ($resp -and $resp.value) { return @($resp.value) }
} catch {
Write-Host " [GSEARCH] search-Fallback fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor DarkYellow
}
return @()
}
function Resolve-GroupMembersWithNesting {
# Loest alle Benutzer einer Gruppe rekursiv auf (inkl. verschachtelter Unter-
# gruppen). Gibt eine flache Liste zurueck; jeder Eintrag traegt den Pfad
# der Herkunfts-Gruppen (z.B. "Top-Gruppe > Unter-Gruppe").
# Zirkulaere Referenzen werden ueber ein HashSet vermieden.
param(
[Parameter(Mandatory=$true)][string]$GroupId,
[string]$GroupName = "",
[System.Collections.Generic.HashSet[string]]$Visited = $null,
[string[]]$Path = @()
)
if ($null -eq $Visited) {
$Visited = New-Object System.Collections.Generic.HashSet[string]
}
if (-not $Visited.Add($GroupId)) { return @() }
$thisPath = if ($GroupName) { @($Path) + @($GroupName) } else { $Path }
$sourcePath = $thisPath -join " > "
$raw = @()
try {
$raw = @(Get-GraphPaged -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members?`$select=id,displayName,userPrincipalName,mail")
} catch {
Write-Host " [EXPORT] Mitglieder von $GroupId konnten nicht geladen werden: $($_.Exception.Message)" -ForegroundColor DarkYellow
return @()
}
$result = [System.Collections.Generic.List[object]]::new()
foreach ($m in $raw) {
$type = [string]$m.'@odata.type'
if ($type -like '*#microsoft.graph.group*') {
$sub = Resolve-GroupMembersWithNesting `
-GroupId ([string]$m.id) `
-GroupName ([string]$m.displayName) `
-Visited $Visited `
-Path $thisPath
foreach ($u in $sub) { $result.Add($u) }
} else {
$result.Add([pscustomobject]@{
Id = [string]$m.id
DisplayName = [string]$m.displayName
UserPrincipalName = [string]$m.userPrincipalName
Mail = [string]$m.mail
SourcePath = $sourcePath
})
}
}
return $result.ToArray()
}
function Get-GraphGroupMembersTransitive {
param([string]$GroupId)
return Get-GraphPaged -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/transitiveMembers?`$select=id,displayName,userPrincipalName"