From c1fc6f8b942ad370ba459a1d2874f5e54be441db Mon Sep 17 00:00:00 2001 From: Marco Wende Date: Sun, 23 Aug 2026 11:36:42 +0200 Subject: [PATCH] =?UTF-8?q?v0.1.27=20=E2=80=94=20App-Registrierungs-Doku?= =?UTF-8?q?=20+=20Setup-Skript,=20Version-Bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/App-Registration.md: alle benoetigten Graph-Berechtigungen (Kern, Geraete-Tab, Read-Only) + Auth-Konfiguration - docs/Setup-AppRegistration.ps1: legt die App-Registrierung automatisch an (Rechte, Public-Client-Flow, Redirect-URIs, optional Admin-Consent); Berechtigungs-IDs werden live aufgeloest - README/CHANGELOG verlinkt, ToolVersion + build-local auf 0.1.27 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 5 +- README.md | 5 + Start.ps1 | 2 +- docs/App-Registration.md | 130 ++++++++++++++++++++ docs/Setup-AppRegistration.ps1 | 214 +++++++++++++++++++++++++++++++++ installer/build-local.ps1 | 2 +- 6 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 docs/App-Registration.md create mode 100644 docs/Setup-AppRegistration.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ddb3e..9dd1704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/) — solan --- -## [Unreleased] +## [0.1.27] - 2026-08-23 -Policy-Import härter, Administrative Vorlagen, Zuweisung nach Import, Git-Snapshot. +Policy-Import härter, Administrative Vorlagen, Zuweisung nach Import, Git-Snapshot, App-Registrierungs-Doku. ### Neu @@ -15,6 +15,7 @@ Policy-Import härter, Administrative Vorlagen, Zuweisung nach Import, Git-Snaps - **Zuweisung direkt nach Import**: Dialog listet die neu angelegten Policies und lässt **pro Policy Include-/Exclude-Gruppen** im Ziel-Tenant zuweisen (Typeahead über die vorhandene Gruppensuche). Neuer Endpoint `POST /api/policies/assign` (nutzt die typ-spezifische `/assign`-Action für alle vier Typen). - **Git-Snapshot (versioniertes Policy-Backup)**: Button „Git-Snapshot" schreibt alle Policies aller Typen als JSON in einen konfigurierten lokalen Git-Ordner und committet sie (optional `git push` über den vorhandenen Credential-Helper). Stabile Dateinamen + deterministische, zeitstempel-freie Ausgabe → saubere Diffs. Neue Settings-Sektion `policyBackup` (`gitRepoPath`, `push`). - Import akzeptiert jetzt auch **Git-Snapshot-Dateien** (Feld `policyType` zusätzlich zu `exportType`). +- **Doku + Provisioning-Skript für die App-Registrierung**: [`docs/App-Registration.md`](docs/App-Registration.md) listet alle benötigten delegierten Graph-Berechtigungen (Kern, Geräte-Tab, Read-Only) samt Auth-Konfiguration; [`docs/Setup-AppRegistration.ps1`](docs/Setup-AppRegistration.ps1) legt die Registrierung automatisch an bzw. aktualisiert sie (Rechte, Public-Client-Flow, Redirect-URIs, optional Admin-Consent) — Berechtigungs-IDs werden live aufgelöst, keine fest verdrahteten GUIDs. ### Behoben diff --git a/README.md b/README.md index 5640408..90dd7d6 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,11 @@ Settings → Verbindung > ⚠ Ohne `DeviceManagementManagedDevices.Read.All` zeigt der App-Report nur > Zaehler = 0 und der Geraete-Export liefert keine Eintraege. +**Vollstaendige Berechtigungsliste, Redirect-URIs und ein Skript, das die +App-Registrierung automatisch anlegt/konfiguriert:** +[docs/App-Registration.md](docs/App-Registration.md) — inkl. +[`Setup-AppRegistration.ps1`](docs/Setup-AppRegistration.ps1). + --- ## Navigation (Tabs) diff --git a/Start.ps1 b/Start.ps1 index 49323d8..ec2561d 100644 --- a/Start.ps1 +++ b/Start.ps1 @@ -188,7 +188,7 @@ $script:State = [pscustomobject]@{ Session = @() # geplante Zuweisungen } -$script:ToolVersion = "0.1.26" +$script:ToolVersion = "0.1.27" $script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "" diff --git a/docs/App-Registration.md b/docs/App-Registration.md new file mode 100644 index 0000000..a17854f --- /dev/null +++ b/docs/App-Registration.md @@ -0,0 +1,130 @@ +# App-Registrierung & Berechtigungen + +Der Intune Manager meldet sich als **delegierte** Anwendung an (Device-Code- bzw. +WAM-Login) — er handelt also immer **im Namen des angemeldeten Admins**, nie mit +Anwendungsrechten. Die App-Registrierung braucht daher **delegierte +Microsoft-Graph-Berechtigungen** plus die passende Authentifizierungs-Konfiguration. + +> **Schnellweg:** Statt alles von Hand zu klicken, legt das Skript +> [`Setup-AppRegistration.ps1`](Setup-AppRegistration.ps1) die Registrierung +> komplett an (Rechte + Public-Client-Flow + Redirect-URIs + optional Consent). +> Siehe [Automatische Einrichtung](#automatische-einrichtung). + +--- + +## Benötigte Berechtigungen + +Alle Berechtigungen sind vom Typ **Delegiert** (Microsoft Graph). + +### Kern (immer erforderlich) + +| Berechtigung | Wofür | +|---|---| +| `Group.ReadWrite.All` | Gruppen anlegen, App-/Policy-Zuweisungen setzen | +| `GroupMember.ReadWrite.All` | Gruppenmitglieder lesen/hinzufügen/entfernen, CSV-Import | +| `User.Read.All` | Benutzersuche, UPN-Auflösung | +| `DeviceManagementApps.ReadWrite.All` | Apps auflisten, zuweisen, umbenennen, löschen, Setup-Datei aktualisieren, App-Report | +| `DeviceManagementConfiguration.ReadWrite.All` | Policies (Settings Catalog, Compliance, Konfigurationsprofile, Administrative Vorlagen) lesen, exportieren, importieren, zuweisen | +| `offline_access` | Refresh-Token (Device-Code-Flow) | + +> `DeviceManagementConfiguration.ReadWrite.All` wird vom Tool automatisch zur +> Verbindung ergänzt — die App-Registrierung muss die Berechtigung aber besitzen +> und (Admin-)zugestimmt bekommen haben, sonst schlägt der Policy-Import mit +> `403 Forbidden` fehl. + +### Geräte-Tab (optional) + +Nur nötig, wenn der **Geräte-Tab** (Sync/Reboot/Lock/Diagnose/Wipe/Retire) genutzt wird: + +| Berechtigung | Wofür | +|---|---| +| `DeviceManagementManagedDevices.Read.All` | Geräte auflisten und Details anzeigen | +| `DeviceManagementManagedDevices.ReadWrite.All` | Sync, Neustart, Remote-Lock, Diagnose, BitLocker-Key-Rotation | +| `DeviceManagementManagedDevices.PrivilegedOperations.All` | Wipe, Retire, Autopilot-Reset | + +### Read-Only-Variante + +Für eine reine Anzeige-App (im Tool als `clientIdRo` hinterlegbar) genügen die +`*.Read.All`-Pendants: + +`Group.Read.All`, `GroupMember.Read.All`, `User.Read.All`, +`DeviceManagementApps.Read.All`, `DeviceManagementConfiguration.Read.All`, +`offline_access` (+ optional `DeviceManagementManagedDevices.Read.All`). + +--- + +## Authentifizierungs-Konfiguration + +Zusätzlich zu den Berechtigungen muss die App als **öffentlicher Client** nutzbar +sein — sonst scheitert der erste Login (`AADSTS500113` bzw. `AADSTS50011`): + +1. **Öffentliche Clientflows zulassen** → **Ja** + (Entra → App-Registrierung → *Authentifizierung* → ganz unten; + im Manifest `isFallbackPublicClient = true`). +2. **Redirect-URIs** unter *Mobilgerät- und Desktopanwendungen*: + ``` + https://login.microsoftonline.com/common/oauth2/nativeclient + ms-appx-web://Microsoft.AAD.BrokerPlugin/ + ``` + - Zeile 1 → **Device-Code-Flow** + - Zeile 2 → **WAM-Broker** (Windows-Anmeldefenster); `` ist die + AppId der Registrierung selbst. +3. **Multi-Tenant:** Wird dieselbe App gegen fremde Tenants genutzt, muss + `signInAudience` auf *Accounts in any organizational directory* + (`AzureADMultipleOrgs`) stehen und im Ziel-Tenant per Admin-Consent + bereitgestellt werden. Bei einer eigenen App **pro** Tenant ist Single-Tenant + ausreichend. + +--- + +## Automatische Einrichtung + +Das Skript [`Setup-AppRegistration.ps1`](Setup-AppRegistration.ps1) erledigt alles +oben Genannte über Microsoft Graph. Es braucht nur das Modul +`Microsoft.Graph.Authentication` (dieselbe Abhängigkeit wie das Tool) und löst die +Berechtigungs-IDs **live** aus dem Graph-Service-Principal auf — keine fest +verdrahteten GUIDs. + +```powershell +# Einmalig: +Install-Module Microsoft.Graph.Authentication -Scope CurrentUser + +# Neue Single-Tenant-App inkl. Admin-Consent: +.\docs\Setup-AppRegistration.ps1 -GrantAdminConsent + +# Mit Geräte-Rechten: +.\docs\Setup-AppRegistration.ps1 -IncludeDeviceActions -GrantAdminConsent + +# Multi-Tenant-App: +.\docs\Setup-AppRegistration.ps1 -MultiTenant -GrantAdminConsent + +# Bestehende App nur um Rechte/Redirect-URIs ergänzen: +.\docs\Setup-AppRegistration.ps1 -ClientId "" -GrantAdminConsent + +# Nur das Manifest-Snippet ausgeben (ohne etwas anzulegen wäre -WhatIf-artig — +# hier zusätzlich zum Anlegen): +.\docs\Setup-AppRegistration.ps1 -EmitManifest +``` + +Am Ende gibt das Skript **Tenant ID** und **Client ID** aus — diese Werte im +Intune Manager unter *Einstellungen → Verbindung* (bzw. im Tenant-Profil) +eintragen. + +Die zum Anlegen/Ändern nötigen Admin-Rechte (`Application.ReadWrite.All`, für +Consent zusätzlich `DelegatedPermissionGrant.ReadWrite.All`) werden beim +`Connect-MgGraph`-Aufruf des Skripts einmalig abgefragt. + +--- + +## Manuelle Einrichtung (Portal) + +1. **Entra** → *App-Registrierungen* → *Neue Registrierung* → Name vergeben, + Kontotyp wählen (Single- oder Multi-Tenant) → **Registrieren**. +2. *API-Berechtigungen* → *Berechtigung hinzufügen* → **Microsoft Graph** → + **Delegierte Berechtigungen** → die [Kern-Berechtigungen](#kern-immer-erforderlich) + (und bei Bedarf die [Geräte-Berechtigungen](#geräte-tab-optional)) auswählen → + **Administratorzustimmung erteilen**. +3. *Authentifizierung* → [Authentifizierungs-Konfiguration](#authentifizierungs-konfiguration) + wie oben setzen (Public-Client-Flow + beide Redirect-URIs). +4. **Übersichtsseite**: *Anwendungs-(Client-)ID* und *Verzeichnis-(Mandanten-)ID* + in den Intune Manager übernehmen. diff --git a/docs/Setup-AppRegistration.ps1 b/docs/Setup-AppRegistration.ps1 new file mode 100644 index 0000000..5111560 --- /dev/null +++ b/docs/Setup-AppRegistration.ps1 @@ -0,0 +1,214 @@ +<# +.SYNOPSIS + Legt die App-Registrierung fuer den Intune Manager an (oder aktualisiert sie) + inkl. Graph-Berechtigungen, Public-Client-Flow und Redirect-URIs. + +.DESCRIPTION + Deckt genau die Punkte ab, an denen der erste Login sonst mit + AADSTS500113 / AADSTS50011 scheitert: + * Delegierte Microsoft-Graph-Berechtigungen (nach Bedarf: RW, RO, Geraete) + * "Oeffentliche Clientflows zulassen" (isFallbackPublicClient = true) + * Redirect-URIs fuer Device-Code (nativeclient) und WAM-Broker + * optional Admin-Consent + + Nutzt nur Microsoft.Graph.Authentication (Invoke-MgGraphRequest) — dieselbe + Abhaengigkeit wie das Tool selbst. Berechtigungs-IDs werden LIVE aus dem + Graph-Service-Principal aufgeloest, es sind also keine fest verdrahteten + GUIDs noetig (die sonst leicht veralten). + +.PARAMETER DisplayName + Anzeigename der App-Registrierung. Default: "Intune Manager". + +.PARAMETER ClientId + AppId einer BESTEHENDEN Registrierung, die aktualisiert werden soll. + Ohne diesen Parameter wird eine NEUE App angelegt. + +.PARAMETER MultiTenant + App fuer mehrere Tenants (signInAudience = AzureADMultipleOrgs). + Default: nur der aktuelle Tenant (AzureADMyOrg). + +.PARAMETER ReadOnly + Verwendet die Read-Only-Berechtigungen (fuer eine reine Anzeige-/RO-App, + passend zu clientIdRo im Tool). + +.PARAMETER IncludeDeviceActions + Nimmt zusaetzlich die Berechtigungen fuer den Geraete-Tab auf + (Read + ReadWrite + PrivilegedOperations = Sync/Reboot/Lock/Wipe/Retire). + +.PARAMETER GrantAdminConsent + Erteilt direkt tenantweiten Admin-Consent fuer die gesetzten Scopes. + Benoetigt entsprechend privilegierte Anmeldung. + +.PARAMETER EmitManifest + Gibt zusaetzlich den requiredResourceAccess-Block als JSON aus (zum manuellen + Einfuegen in das App-Manifest im Portal). + +.EXAMPLE + # Neue Single-Tenant-App inkl. Consent: + .\Setup-AppRegistration.ps1 -GrantAdminConsent + +.EXAMPLE + # Bestehende App um Geraete-Rechte erweitern: + .\Setup-AppRegistration.ps1 -ClientId "51477347-...." -IncludeDeviceActions -GrantAdminConsent + +.NOTES + Vorher einmalig: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser +#> +[CmdletBinding()] +param( + [string]$DisplayName = 'Intune Manager', + [string]$ClientId, + [switch]$MultiTenant, + [switch]$ReadOnly, + [switch]$IncludeDeviceActions, + [switch]$GrantAdminConsent, + [switch]$EmitManifest +) + +$ErrorActionPreference = 'Stop' +$GraphAppId = '00000003-0000-0000-c000-000000000000' # Microsoft Graph + +# --- Benoetigte delegierte Berechtigungen zusammenstellen --------------------- +$perms = if ($ReadOnly) { + @('Group.Read.All','GroupMember.Read.All','User.Read.All', + 'DeviceManagementApps.Read.All','DeviceManagementConfiguration.Read.All','offline_access') +} else { + @('Group.ReadWrite.All','GroupMember.ReadWrite.All','User.Read.All', + 'DeviceManagementApps.ReadWrite.All','DeviceManagementConfiguration.ReadWrite.All','offline_access') +} +if ($IncludeDeviceActions) { + $perms += if ($ReadOnly) { + @('DeviceManagementManagedDevices.Read.All') + } else { + @('DeviceManagementManagedDevices.Read.All', + 'DeviceManagementManagedDevices.ReadWrite.All', + 'DeviceManagementManagedDevices.PrivilegedOperations.All') + } +} +$perms = $perms | Select-Object -Unique + +# --- Verbinden ---------------------------------------------------------------- +Write-Host "[1/6] Mit Microsoft Graph verbinden (Admin noetig)..." -ForegroundColor Cyan +$connectScopes = @('Application.ReadWrite.All') +if ($GrantAdminConsent) { $connectScopes += 'DelegatedPermissionGrant.ReadWrite.All' } +Import-Module Microsoft.Graph.Authentication -ErrorAction Stop +Connect-MgGraph -Scopes $connectScopes -NoWelcome +$ctx = Get-MgContext +if (-not $ctx) { throw 'Keine Graph-Verbindung.' } +Write-Host " verbunden mit Tenant $($ctx.TenantId) als $($ctx.Account)" -ForegroundColor DarkGray + +# --- Graph-Service-Principal + Berechtigungs-IDs aufloesen -------------------- +Write-Host "[2/6] Graph-Berechtigungen aufloesen..." -ForegroundColor Cyan +$graphSp = (Invoke-MgGraphRequest -Method GET ` + -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '$GraphAppId'&`$select=id,oauth2PermissionScopes").value | Select-Object -First 1 +if (-not $graphSp) { throw 'Graph-Service-Principal nicht gefunden.' } +$graphSpId = $graphSp.id + +$scopeMap = @{} +foreach ($s in $graphSp.oauth2PermissionScopes) { $scopeMap[$s.value] = $s.id } + +$resourceAccess = @() +foreach ($p in $perms) { + if (-not $scopeMap.ContainsKey($p)) { throw "Delegierte Berechtigung '$p' nicht im Graph-SP gefunden." } + $resourceAccess += @{ id = $scopeMap[$p]; type = 'Scope' } +} +$requiredResourceAccess = @(@{ resourceAppId = $GraphAppId; resourceAccess = $resourceAccess }) + +if ($EmitManifest) { + Write-Host "`n--- requiredResourceAccess (Manifest) ---" -ForegroundColor Yellow + ($requiredResourceAccess | ConvertTo-Json -Depth 6) + Write-Host "-----------------------------------------`n" -ForegroundColor Yellow +} + +# --- App anlegen oder aktualisieren ------------------------------------------- +$signInAudience = if ($MultiTenant) { 'AzureADMultipleOrgs' } else { 'AzureADMyOrg' } + +if ($ClientId) { + Write-Host "[3/6] Bestehende App $ClientId laden..." -ForegroundColor Cyan + $app = (Invoke-MgGraphRequest -Method GET ` + -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=appId eq '$ClientId'&`$select=id,appId").value | Select-Object -First 1 + if (-not $app) { throw "App mit appId $ClientId nicht gefunden." } + $objId = $app.id + $appId = $ClientId + $patch = @{ + signInAudience = $signInAudience + isFallbackPublicClient = $true + requiredResourceAccess = $requiredResourceAccess + publicClient = @{ redirectUris = @( + 'https://login.microsoftonline.com/common/oauth2/nativeclient' + "ms-appx-web://Microsoft.AAD.BrokerPlugin/$appId" + ) } + } + Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/applications/$objId" ` + -Body ($patch | ConvertTo-Json -Depth 8) -ContentType 'application/json' | Out-Null + Write-Host " App aktualisiert." -ForegroundColor Green +} else { + Write-Host "[3/6] Neue App '$DisplayName' anlegen..." -ForegroundColor Cyan + # Broker-Redirect-URI braucht die appId -> erst mit nativeclient anlegen, + # danach die Broker-URI per PATCH ergaenzen. + $create = @{ + displayName = $DisplayName + signInAudience = $signInAudience + isFallbackPublicClient = $true + requiredResourceAccess = $requiredResourceAccess + publicClient = @{ redirectUris = @('https://login.microsoftonline.com/common/oauth2/nativeclient') } + } + $app = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/applications' ` + -Body ($create | ConvertTo-Json -Depth 8) -ContentType 'application/json' + $objId = $app.id + $appId = $app.appId + + $patch = @{ publicClient = @{ redirectUris = @( + 'https://login.microsoftonline.com/common/oauth2/nativeclient' + "ms-appx-web://Microsoft.AAD.BrokerPlugin/$appId" + ) } } + Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/applications/$objId" ` + -Body ($patch | ConvertTo-Json -Depth 6) -ContentType 'application/json' | Out-Null + Write-Host " App angelegt: appId $appId" -ForegroundColor Green +} + +# --- Service-Principal (Enterprise-App) sicherstellen ------------------------- +Write-Host "[4/6] Service-Principal sicherstellen..." -ForegroundColor Cyan +$sp = (Invoke-MgGraphRequest -Method GET ` + -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '$appId'&`$select=id").value | Select-Object -First 1 +if (-not $sp) { + $sp = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/servicePrincipals' ` + -Body (@{ appId = $appId } | ConvertTo-Json) -ContentType 'application/json' +} +$spId = $sp.id + +# --- Optional: Admin-Consent -------------------------------------------------- +Write-Host "[5/6] Admin-Consent..." -ForegroundColor Cyan +if ($GrantAdminConsent) { + $scopeString = ($perms -join ' ') + # Bestehenden Grant fuer (Client -> Graph) suchen und ersetzen, sonst neu. + $existing = (Invoke-MgGraphRequest -Method GET ` + -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$spId' and resourceId eq '$graphSpId'").value | Select-Object -First 1 + $grantBody = @{ + clientId = $spId + consentType = 'AllPrincipals' + resourceId = $graphSpId + scope = $scopeString + } + if ($existing) { + Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($existing.id)" ` + -Body (@{ scope = $scopeString } | ConvertTo-Json) -ContentType 'application/json' | Out-Null + } else { + Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants' ` + -Body ($grantBody | ConvertTo-Json) -ContentType 'application/json' | Out-Null + } + Write-Host " Admin-Consent erteilt fuer: $scopeString" -ForegroundColor Green +} else { + Write-Host " uebersprungen (-GrantAdminConsent nicht gesetzt)." -ForegroundColor DarkGray + Write-Host " Consent im Portal: Entra -> App-Registrierungen -> $DisplayName -> API-Berechtigungen -> Administratorzustimmung erteilen" -ForegroundColor DarkGray +} + +# --- Ergebnis ----------------------------------------------------------------- +Write-Host "[6/6] Fertig." -ForegroundColor Cyan +Write-Host "" +Write-Host " Im Intune Manager eintragen:" -ForegroundColor White +Write-Host " Tenant ID : $($ctx.TenantId)" +Write-Host " Client ID : $appId" +Write-Host "" +Write-Host " Gesetzte delegierte Berechtigungen:" -ForegroundColor White +$perms | ForEach-Object { Write-Host " - $_" } diff --git a/installer/build-local.ps1 b/installer/build-local.ps1 index dfbf905..8666646 100644 --- a/installer/build-local.ps1 +++ b/installer/build-local.ps1 @@ -12,7 +12,7 @@ if (Test-Path "$x64Dotnet\dotnet.exe") { $env:DOTNET_ROOT = "C:\Program Files\dotnet" } -$version = "0.1.26" +$version = "0.1.27" $src = "\\Mac\Home\ClaudePRJ\Intune Manager" $stage = "$env:TEMP\IntuneManagerStage" $installerDir = "$src\installer"