Multi-Tenant-Umschaltung + Pro-Tenant-Vorgaben, RPA deaktiviert

- Multi-Tenant: Settings-Schalter Single/Multi, Profile mit je eigener
  App-Registrierung, Topbar-Umschalter mit Sofort-Reconnect; verlustfreie
  Migration (Get-ActiveConnection/-TenantProfile, /api/tenants[/switch]).
- Pro-Tenant-Overrides mit globalem Fallback: Abteilungs-Praefixe sowie
  Required-/Available-Gruppen-Naming (Get-DepartmentPrefixes/Get-GroupNaming
  tenant-bewusst; New-GroupEndpoint nutzt Resolver).
- RPA komplett deaktiviert: Mode-Tab, globaler Settings-Abschnitt und
  Test-Anzeige entfernt.
- Setup-Zwang gelockert: nur Tenant ID + Client ID Pflicht; Abteilungs-
  Praefixe optional (kein harter Setup-Blocker mehr).
- api(): "Failed to fetch" -> klare Meldung "Server nicht erreichbar…".
- Hilfe-Footer: "Entwickelt von WendeIT – Marco Wende".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:16:34 +02:00
co-authored by Claude Opus 4.8
parent f36e9c5ba0
commit 37ae32bb94
7 changed files with 638 additions and 105 deletions
+104 -21
View File
@@ -47,6 +47,9 @@ function Invoke-ApiHandler {
"POST /api/assignments/apply" { return Invoke-ApplyEndpoint -Body $Body }
"GET /api/tenants" { return Get-TenantsEndpoint }
"POST /api/tenants/switch" { return Switch-TenantEndpoint -Body $Body }
"GET /api/policies/compliance" { return Get-CompliancePoliciesEndpoint }
"GET /api/policies/configuration" { return Get-ConfigurationProfilesEndpoint }
"GET /api/policies/settingscatalog" { return Get-SettingsCatalogPoliciesEndpoint }
@@ -167,9 +170,11 @@ function Get-SettingsEndpoint {
function Sync-ConfigFromSettings {
# $script:Config spiegelt die settings.connection-Werte. Wird nach jedem
# Save aufgerufen damit Connect-Aufrufe sofort die neuen IDs verwenden.
$script:Config.TenantId = $script:Settings.connection.tenantId
$script:Config.ClientId = $script:Settings.connection.clientId
$script:Config.ClientIdRo = $script:Settings.connection.clientIdRo
# Im Multi-Tenant-Modus kommen TenantId/ClientId aus dem aktiven Profil.
$active = Get-ActiveConnection -Settings $script:Settings
$script:Config.TenantId = $active.tenantId
$script:Config.ClientId = $active.clientId
$script:Config.ClientIdRo = $active.clientIdRo
$script:Config.Scopes = @($script:Settings.connection.scopes)
$script:Config.ScopesRo = @($script:Settings.connection.scopesRo)
# Policy Export/Import braucht den Configuration-Scope. Immer sicherstellen —
@@ -207,17 +212,26 @@ function Save-SettingsEndpoint {
# Vergleich altes vs. neues Setting — Cache nur leeren wo wirklich noetig.
$old = $script:Settings
# Aktive Verbindung vergleichen (deckt Single-Felder UND das aktive
# Multi-Tenant-Profil ab), plus Moduswechsel Single<->Multi.
$activeOld = Get-ActiveConnection -Settings $old
$activeNew = Get-ActiveConnection -Settings $merged
$multiOld = ($old.connection.PSObject.Properties['multiTenant'] -and [bool]$old.connection.multiTenant)
$multiNew = ($merged.connection.PSObject.Properties['multiTenant'] -and [bool]$merged.connection.multiTenant)
$connectionChanged = (
$merged.connection.tenantId -ne $old.connection.tenantId -or
$merged.connection.clientId -ne $old.connection.clientId -or
(($merged.connection.scopes -join "|") -ne ($old.connection.scopes -join "|"))
$activeNew.tenantId -ne $activeOld.tenantId -or
$activeNew.clientId -ne $activeOld.clientId -or
$activeNew.clientIdRo -ne $activeOld.clientIdRo -or
(($merged.connection.scopes -join "|") -ne ($old.connection.scopes -join "|")) -or
($multiNew -ne $multiOld)
)
# Normalisierte Praefix-Listen vergleichen (deckt sowohl 'prefixes' als auch
# den alten Single-String 'prefix' ab; Reihenfolge ignoriert, Case ignoriert).
$deptOld = @(Get-DepartmentPrefixes -Settings $old) | Sort-Object -Property { $_.ToLowerInvariant() }
$deptNew = @(Get-DepartmentPrefixes -Settings $merged) | Sort-Object -Property { $_.ToLowerInvariant() }
$deptChanged = (($deptOld -join '|') -ne ($deptNew -join '|'))
$rpaChanged = (($merged.rpa.groupNames -join "|") -ne ($old.rpa.groupNames -join "|"))
# Tenant-bewusst: vergleicht die aufgeloesten RPA-Namen (Profil-Override oder global).
$rpaChanged = ((@(Get-RpaGroupNames -Settings $merged) -join "|") -ne (@(Get-RpaGroupNames -Settings $old) -join "|"))
$userSearchChanged = (
(($merged.userSearch.fields -join "|") -ne ($old.userSearch.fields -join "|"))
)
@@ -573,16 +587,24 @@ function Get-StatusEndpoint {
error = $cs.Error
}
}
$activeConn = Get-ActiveConnection -Settings $script:Settings
$activeId = ""
if ($script:Settings.connection.PSObject.Properties['activeTenantId']) {
$activeId = [string]$script:Settings.connection.activeTenantId
}
return @{
connected = $script:State.Connected
account = $script:State.Account
tenantId = $script:State.TenantId
readOnly = [bool]$script:State.ReadOnly
groupsLoaded = $script:State.Groups.Count
rpaLoaded = $script:State.RpaGroups.Count
appsLoaded = $script:State.Apps.Count
sessionCount = $script:State.Session.Count
connect = $connect
connected = $script:State.Connected
account = $script:State.Account
tenantId = $script:State.TenantId
readOnly = [bool]$script:State.ReadOnly
groupsLoaded = $script:State.Groups.Count
rpaLoaded = $script:State.RpaGroups.Count
appsLoaded = $script:State.Apps.Count
sessionCount = $script:State.Session.Count
connect = $connect
multiTenant = (Test-ConnectionMultiTenant)
tenantLabel = $activeConn.label
activeTenantId = $activeId
}
}
@@ -1021,6 +1043,66 @@ function Test-Connected {
return $null
}
# ============================================================
# Multi-Tenant
# ============================================================
function Test-ConnectionMultiTenant {
$c = $script:Settings.connection
return ($c -and $c.PSObject.Properties['multiTenant'] -and [bool]$c.multiTenant)
}
function Get-TenantsEndpoint {
# Liste der Tenant-Profile fuer den Topbar-Umschalter. Client-IDs werden
# nicht mitgeliefert (fuer die Anzeige nicht noetig).
$c = $script:Settings.connection
$isMulti = Test-ConnectionMultiTenant
$items = @()
if ($isMulti -and $c.PSObject.Properties['tenants']) {
foreach ($t in @($c.tenants | Where-Object { $_ })) {
$items += @{
id = [string]$t.id
label = [string]$t.label
tenantId = [string]$t.tenantId
hasRo = [bool]($t.clientIdRo -and ([string]$t.clientIdRo).Trim())
}
}
}
$activeId = if ($c.PSObject.Properties['activeTenantId']) { [string]$c.activeTenantId } else { "" }
if ($isMulti -and $items.Count -gt 0 -and -not (@($items | Where-Object { $_.id -eq $activeId }).Count)) {
$activeId = $items[0].id
}
return @{ multiTenant = $isMulti; activeId = $activeId; items = @($items) }
}
function Switch-TenantEndpoint {
param($Body)
if (-not (Test-ConnectionMultiTenant)) {
return @{ __status = 400; error = "Multi-Tenant-Modus ist nicht aktiv." }
}
$id = if ($Body) { [string]$Body.id } else { "" }
if ([string]::IsNullOrWhiteSpace($id)) { return @{ __status = 400; error = "Tenant-id fehlt." } }
$c = $script:Settings.connection
$tenants = @()
if ($c.PSObject.Properties['tenants']) { $tenants = @($c.tenants | Where-Object { $_ }) }
$profile = $tenants | Where-Object { [string]$_.id -eq $id } | Select-Object -First 1
if (-not $profile) { return @{ __status = 404; error = "Tenant-Profil nicht gefunden." } }
# Aktives Profil setzen + persistieren, Config spiegeln.
$script:Settings.connection.activeTenantId = $id
try { Write-Settings -Settings $script:Settings } catch {
Write-Host "[TENANT] Speichern des aktiven Profils fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor Yellow
}
Sync-ConfigFromSettings
# Bestehende Verbindung trennen (raeumt alle Tenant-Caches ab), dann sofort
# mit dem neuen Tenant neu verbinden.
Invoke-DisconnectEndpoint | Out-Null
Write-Host "[TENANT] Wechsel zu '$([string]$profile.label)' ($($profile.tenantId))" -ForegroundColor Cyan
return Invoke-ConnectEndpoint
}
function Get-AppCategoryNames {
# Extrahiert die Kategorie-Namen aus dem rohen Graph-Categories-Feld.
# Robust gegen alle Source-Types die MgGraph/Invoke-MgGraphRequest in
@@ -1152,7 +1234,8 @@ function Get-RpaGroupsEndpoint {
$err = Test-Connected
if ($err) { return $err }
$rpaNames = @($script:Settings.rpa.groupNames | Where-Object { $_ })
# Tenant-bewusst: aktives Profil kann eigene RPA-Gruppen definieren, sonst global.
$rpaNames = @(Get-RpaGroupNames -Settings $script:Settings)
if ($rpaNames.Count -eq 0) {
# Settings leer -> ehrlich melden, das Frontend zeigt einen Konfig-Hinweis.
return @{ items = @(); count = 0; notConfigured = $true }
@@ -1211,10 +1294,10 @@ function New-GroupEndpoint {
return @{ __status = 400; error = "Intent muss 'required' oder 'available' sein" }
}
$namingObj = if ($intent -eq "available") { $script:Settings.availableGroupNaming } else { $script:Settings.requiredGroupNaming }
$defaultSuffix = if ($intent -eq "available") { "-available" } else { "-required" }
$namingPrefix = if ($namingObj -and $namingObj.prefix) { $namingObj.prefix } else { "intune-win-app-" }
$namingSuffix = if ($namingObj -and $namingObj.suffix) { $namingObj.suffix } else { $defaultSuffix }
# Naming tenant-bewusst aufloesen (aktives Profil kann ueberschreiben).
$naming = Get-GroupNaming -Settings $script:Settings -Intent $intent
$namingPrefix = $naming.prefix
$namingSuffix = $naming.suffix
$cleaned = if ($customName) { Format-GroupNameSlug -Name $customName } else { Format-GroupNameSlug -Name $appName }
$displayName = "$namingPrefix$cleaned$namingSuffix".ToLower()