add group export
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user