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
+153 -17
View File
@@ -44,6 +44,10 @@ function Get-DefaultSettings {
# Theme) sind branchenuebliche Startpunkte und bleiben besetzt.
return [pscustomobject]@{
connection = [pscustomobject]@{
# Verbindungsmodus:
# $false = Single-Tenant (klassisch: die flachen Felder unten)
# $true = Multi-Tenant (Liste 'tenants' + 'activeTenantId')
multiTenant = $false
tenantId = ""
clientId = ""
clientIdRo = ""
@@ -53,6 +57,10 @@ function Get-DefaultSettings {
# msgraph-Skill gegen den offiziellen Graph-Permission-Index.
scopes = @("Group.ReadWrite.All", "GroupMember.ReadWrite.All", "User.Read.All", "DeviceManagementApps.ReadWrite.All")
scopesRo = @("Group.Read.All", "GroupMember.Read.All", "User.Read.All", "DeviceManagementApps.Read.All")
# Multi-Tenant-Profile: je Tenant eigene App-Registrierung(en).
# [{ id, label, tenantId, clientId, clientIdRo }]. Scopes gelten global.
tenants = @()
activeTenantId = ""
}
departments = [pscustomobject]@{
# Ein oder mehrere Praefixe fuer den Abteilungs-Modus (linke Spalte).
@@ -164,18 +172,27 @@ function Merge-Settings {
}
function Get-DepartmentPrefixes {
# Zentrale Quelle fuer die Abteilungs-Praefixe. Bevorzugt das neue
# 'prefixes'-Array; faellt sonst auf den alten Single-String 'prefix'
# zurueck (Rueckwaerts-Kompatibilitaet mit existierenden settings.json).
# Liefert immer ein String-Array (getrimmt, dedupliziert, ohne leere
# Eintraege), ggf. leer wenn nichts konfiguriert ist.
# Zentrale Quelle fuer die Abteilungs-Praefixe. Multi-Tenant: hat das aktive
# Profil eigene 'prefixes', gelten diese (Override); sonst die globalen
# departments.prefixes / alter Single-String 'prefix' (Rueckwaerts-Kompat).
# Liefert immer ein String-Array (getrimmt, dedupliziert, ohne leere).
param($Settings)
$out = [System.Collections.Generic.List[string]]::new()
if ($null -eq $Settings -or $null -eq $Settings.departments) { return ,$out.ToArray() }
$d = $Settings.departments
if ($null -eq $Settings) { return ,$out.ToArray() }
$candidates = @()
if ($d.PSObject.Properties['prefixes']) { $candidates += @($d.prefixes) }
if ($d.PSObject.Properties['prefix'] -and $d.prefix) { $candidates += @([string]$d.prefix) }
# 1) Tenant-Override (aktives Profil)
$prof = Get-ActiveTenantProfile -Settings $Settings
if ($prof -and $prof.PSObject.Properties['prefixes']) {
$profPfx = @($prof.prefixes | Where-Object { $_ -and ([string]$_).Trim() })
if ($profPfx.Count -gt 0) { $candidates = $profPfx }
}
# 2) Globale Vorgabe (Fallback, wenn kein Profil-Override)
if ($candidates.Count -eq 0 -and $null -ne $Settings.departments) {
$d = $Settings.departments
if ($d.PSObject.Properties['prefixes']) { $candidates += @($d.prefixes) }
if ($d.PSObject.Properties['prefix'] -and $d.prefix) { $candidates += @([string]$d.prefix) }
}
$seen = @{}
foreach ($p in $candidates) {
if ($null -eq $p) { continue }
@@ -189,6 +206,100 @@ function Get-DepartmentPrefixes {
return $out.ToArray()
}
function Get-RpaGroupNames {
# RPA-Gruppennamen fuer den aktuell aktiven Kontext. Multi-Tenant: aktives
# Profil kann eigene 'rpaGroups' definieren (Override); sonst global.
param($Settings)
if ($null -eq $Settings) { return ,@() }
$prof = Get-ActiveTenantProfile -Settings $Settings
if ($prof -and $prof.PSObject.Properties['rpaGroups']) {
$names = @($prof.rpaGroups | Where-Object { $_ -and ([string]$_).Trim() })
if ($names.Count -gt 0) { return ,@($names | ForEach-Object { [string]$_ }) }
}
if ($Settings.rpa -and $Settings.rpa.PSObject.Properties['groupNames']) {
return ,@($Settings.rpa.groupNames | Where-Object { $_ -and ([string]$_).Trim() } | ForEach-Object { [string]$_ })
}
return ,@()
}
function Get-GroupNaming {
# Naming-Schema (prefix/suffix) fuer required/available. Multi-Tenant: das
# aktive Profil kann prefix und/oder suffix ueberschreiben (pro Feld: nicht-
# leerer Profilwert gewinnt, sonst globale Vorgabe, sonst Default).
param($Settings, [string]$Intent)
$isAvail = ($Intent -eq 'available')
$defPrefix = 'intune-win-app-'
$defSuffix = if ($isAvail) { '-available' } else { '-required' }
$globalObj = if ($Settings) { if ($isAvail) { $Settings.availableGroupNaming } else { $Settings.requiredGroupNaming } } else { $null }
$prefix = if ($globalObj -and $globalObj.prefix) { [string]$globalObj.prefix } else { $defPrefix }
$suffix = if ($globalObj -and $globalObj.suffix) { [string]$globalObj.suffix } else { $defSuffix }
$prof = Get-ActiveTenantProfile -Settings $Settings
if ($prof) {
$key = if ($isAvail) { 'availableGroupNaming' } else { 'requiredGroupNaming' }
$pObj = if ($prof.PSObject.Properties[$key]) { $prof.$key } else { $null }
if ($pObj) {
if ($pObj.PSObject.Properties['prefix'] -and ([string]$pObj.prefix).Trim()) { $prefix = [string]$pObj.prefix }
if ($pObj.PSObject.Properties['suffix'] -and ([string]$pObj.suffix).Trim()) { $suffix = [string]$pObj.suffix }
}
}
return @{ prefix = $prefix; suffix = $suffix }
}
function Get-ActiveTenantProfile {
# Liefert das aktive Tenant-Profil-Objekt (Multi-Tenant) oder $null im
# Single-Tenant-Modus / wenn keine Profile existieren.
param($Settings)
if ($null -eq $Settings) { return $null }
$c = $Settings.connection
if ($null -eq $c) { return $null }
if (-not ($c.PSObject.Properties['multiTenant'] -and [bool]$c.multiTenant)) { return $null }
$tenants = @()
if ($c.PSObject.Properties['tenants']) { $tenants = @($c.tenants | Where-Object { $_ }) }
if ($tenants.Count -eq 0) { return $null }
$activeId = if ($c.PSObject.Properties['activeTenantId']) { [string]$c.activeTenantId } else { "" }
$p = $tenants | Where-Object { [string]$_.id -eq $activeId } | Select-Object -First 1
if (-not $p) { $p = $tenants[0] }
return $p
}
function Get-ActiveConnection {
# Liefert die aktuell zu verwendenden Verbindungswerte als PSCustomObject
# { tenantId; clientId; clientIdRo; label }. Im Multi-Tenant-Modus das
# aktive Profil (activeTenantId, sonst erstes Profil); sonst die flachen
# connection-Felder (Rueckwaerts-Kompatibilitaet / Single-Tenant).
param($Settings)
$c = $Settings.connection
$empty = [pscustomobject]@{ tenantId = ""; clientId = ""; clientIdRo = ""; label = "" }
if ($null -eq $c) { return $empty }
$isMulti = $false
if ($c.PSObject.Properties['multiTenant']) { $isMulti = [bool]$c.multiTenant }
if ($isMulti) {
$tenants = @()
if ($c.PSObject.Properties['tenants']) { $tenants = @($c.tenants | Where-Object { $_ }) }
if ($tenants.Count -eq 0) { return $empty }
$activeId = if ($c.PSObject.Properties['activeTenantId']) { [string]$c.activeTenantId } else { "" }
$profile = $tenants | Where-Object { [string]$_.id -eq $activeId } | Select-Object -First 1
if (-not $profile) { $profile = $tenants[0] }
return [pscustomobject]@{
tenantId = [string]$profile.tenantId
clientId = [string]$profile.clientId
clientIdRo = [string]$profile.clientIdRo
label = [string]$profile.label
}
}
return [pscustomobject]@{
tenantId = [string]$c.tenantId
clientId = [string]$c.clientId
clientIdRo = [string]$c.clientIdRo
label = ""
}
}
function Read-Settings {
$path = Get-SettingsPath
$defaults = Get-DefaultSettings
@@ -216,14 +327,39 @@ function Get-SettingsValidationErrors {
# Rudimentaere Validierung. Schwere Fehler -> Speichern ablehnen.
param($S)
$errs = @()
if (-not $S.connection.tenantId -or $S.connection.tenantId -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Tenant ID muss eine GUID sein."
}
if (-not $S.connection.clientId -or $S.connection.clientId -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Client ID (Read/Write) muss eine GUID sein."
}
if ($S.connection.clientIdRo -and $S.connection.clientIdRo.Trim() -and $S.connection.clientIdRo -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Client ID (Read Only) muss eine GUID sein (oder leer lassen)."
$isMulti = $false
if ($S.connection.PSObject.Properties['multiTenant']) { $isMulti = [bool]$S.connection.multiTenant }
if ($isMulti) {
# Multi-Tenant: jedes Profil validieren; mindestens eines erforderlich.
$tenants = @()
if ($S.connection.PSObject.Properties['tenants']) { $tenants = @($S.connection.tenants | Where-Object { $_ }) }
if ($tenants.Count -eq 0) {
$errs += "Multi-Tenant aktiv, aber kein Tenant-Profil angelegt."
} else {
$seenIds = @{}
foreach ($t in $tenants) {
$lbl = if ($t.label) { [string]$t.label } else { [string]$t.tenantId }
if (-not $t.id -or [string]::IsNullOrWhiteSpace($t.id)) { $errs += "Tenant-Profil ohne interne id."; continue }
if ($seenIds.ContainsKey([string]$t.id)) { $errs += "Tenant-Profil-id nicht eindeutig: $($t.id)"; continue }
$seenIds[[string]$t.id] = $true
if (-not $t.label -or [string]::IsNullOrWhiteSpace($t.label)) { $errs += "Tenant-Profil '$lbl': Bezeichnung erforderlich." }
if (-not $t.tenantId -or $t.tenantId -notmatch '^[0-9a-fA-F-]{36}$') { $errs += "Tenant-Profil '$lbl': Tenant ID muss eine GUID sein." }
if (-not $t.clientId -or $t.clientId -notmatch '^[0-9a-fA-F-]{36}$') { $errs += "Tenant-Profil '$lbl': Client ID (Read/Write) muss eine GUID sein." }
if ($t.clientIdRo -and ([string]$t.clientIdRo).Trim() -and $t.clientIdRo -notmatch '^[0-9a-fA-F-]{36}$') { $errs += "Tenant-Profil '$lbl': Client ID (Read Only) muss eine GUID sein (oder leer)." }
}
}
} else {
# Single-Tenant: klassische flache Felder.
if (-not $S.connection.tenantId -or $S.connection.tenantId -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Tenant ID muss eine GUID sein."
}
if (-not $S.connection.clientId -or $S.connection.clientId -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Client ID (Read/Write) muss eine GUID sein."
}
if ($S.connection.clientIdRo -and $S.connection.clientIdRo.Trim() -and $S.connection.clientIdRo -notmatch '^[0-9a-fA-F-]{36}$') {
$errs += "Client ID (Read Only) muss eine GUID sein (oder leer lassen)."
}
}
if (-not $S.connection.scopes -or @($S.connection.scopes).Count -eq 0) {
$errs += "Mindestens ein Scope erforderlich."