v4.0.1: per-user app naming, auth deletion, TUI onboarding flow, PIM docs

This commit is contained in:
2026-04-16 15:40:33 +02:00
parent 1ff059342f
commit 70679cba48
4 changed files with 200 additions and 25 deletions

View File

@@ -1,5 +1,25 @@
# macOS Intune Toolkit Changelog # macOS Intune Toolkit Changelog
## 2026-04-16 — v4.0.1 — Accountability, PIM & Auth Management
### Modified
- **`Scripts/Initialize-IntuneAuth.ps1`**
- App registrations are now named after the **authenticated Entra user** (e.g., `IntuneManagement-tomas.kracmar@cqre.net`) instead of the local OS username. This improves audit-log traceability when multiple admins use the toolkit against the same tenant.
- Added `-Delete` switch to remove local tenant credentials (`Settings.json` + macOS Keychain) without touching the Entra app registration.
- Added `-DeleteApp` switch to delete both the **Entra app registration** and local credentials.
- Onboarding now automatically caches the tenant display name after auth setup, so the TUI shows friendly names immediately.
- Added `Organization.Read.All` to the `Connect-MgGraph` scopes to support tenant name caching.
- **`Scripts/Start-IntuneToolkit.ps1`**
- Added menu items **14** (delete local auth) and **15** (delete auth + app registration) to the TUI.
- Selecting **"[+ Onboard new tenant]"** now runs the auth initializer immediately and restarts the launcher, instead of dropping into the main menu for an unconfigured tenant.
- The TUI now exits cleanly after deleting tenant auth.
- **`README.md`**
- Added **Accountability & PIM caveats** section explaining the trade-offs of app-only auth versus delegated auth, and how app naming affects audit logs.
---
## 2026-04-13 — API Permissions Sync for `Initialize-IntuneAuth.ps1` ## 2026-04-13 — API Permissions Sync for `Initialize-IntuneAuth.ps1`
### Modified ### Modified

View File

@@ -2,7 +2,7 @@
Cross-platform, headless Intune policy export/import with PowerShell. Cross-platform, headless Intune policy export/import with PowerShell.
**Current version:** `4.0.0` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes. **Current version:** `4.0.1` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes.
This repository is now CLI-first. The old WPF application surface has been removed from the repo. The supported workflow is: This repository is now CLI-first. The old WPF application surface has been removed from the repo. The supported workflow is:
@@ -154,3 +154,12 @@ pwsh ./Start-HeadlessIntune.ps1 `
* Browser auth uses the system browser and a loopback redirect. * Browser auth uses the system browser and a loopback redirect.
* If you omit `-AppId` with `-AuthMode Browser`, the CLI defaults to the Microsoft Graph PowerShell public client app id `14d82eec-204b-4c2f-b7e8-296a70dab67e`. * If you omit `-AppId` with `-AuthMode Browser`, the CLI defaults to the Microsoft Graph PowerShell public client app id `14d82eec-204b-4c2f-b7e8-296a70dab67e`.
* If your own app registration does not allow loopback redirects, pass `-AppId` and `-RedirectUri "http://localhost"` and configure the same redirect URI in Entra ID. * If your own app registration does not allow loopback redirects, pass `-AppId` and `-RedirectUri "http://localhost"` and configure the same redirect URI in Entra ID.
## Accountability & PIM caveats
By default `Initialize-IntuneAuth.ps1` creates an **app-only** registration. Every Graph call is authenticated as the service principal, not as an individual user.
* **Audit logs** show the app's display name (e.g., `IntuneManagement-tomas.kracmar@cqre.net`), not the admin's UPN. The initializer now automatically names the app after the **authenticated Entra user** to improve traceability.
* **PIM is not enforced** for app-only secrets. The service principal has standing permissions, so write operations can occur outside an elevated PIM window.
* If you need strict PIM compliance, use **delegated authentication** (`-AuthMode Browser` or `-AuthMode DeviceCode`) so calls are made in the signed-in user's context. Note that `DeviceCode` may be blocked by Conditional Access policies.
* To fully remove a tenant's local credentials **and** the Entra app registration, use menu item **15** in the TUI or run `./Scripts/Initialize-IntuneAuth.ps1 -TenantId "<id>" -DeleteApp`.

View File

@@ -17,7 +17,7 @@ The Microsoft Entra tenant ID (GUID). If omitted, the script reads from
existing settings or prompts interactively. existing settings or prompts interactively.
.PARAMETER DisplayName .PARAMETER DisplayName
The display name for the app registration. Default: IntuneManagement-Headless. The display name for the app registration. Default: IntuneManagement-<current user name>.
.PARAMETER SettingsFile .PARAMETER SettingsFile
Path to the JSON settings file. If omitted, defaults to the macOS_IntuneManagement Path to the JSON settings file. If omitted, defaults to the macOS_IntuneManagement
@@ -25,16 +25,28 @@ settings folder (~/Library/Application Support/macOS_IntuneManagement/Settings.j
.PARAMETER Force .PARAMETER Force
Recreate the app registration and secret even if existing credentials are found. Recreate the app registration and secret even if existing credentials are found.
.PARAMETER Delete
Remove the saved tenant credentials from the local settings file (and macOS Keychain if applicable).
Does not delete the app registration in Entra ID.
.PARAMETER DeleteApp
Remove the app registration from the Entra tenant and clean up local credentials.
Requires the same Microsoft Graph permissions as initialization.
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$TenantId, [string]$TenantId,
[string]$DisplayName = "IntuneManagement-Headless", [string]$DisplayName = "IntuneManagement-$([Environment]::UserName)",
[string]$SettingsFile, [string]$SettingsFile,
[switch]$Force [switch]$Force,
[switch]$Delete,
[switch]$DeleteApp
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -67,6 +79,45 @@ function Get-AuthSetting
param($Key, [string]$SubPath = "", $DefaultValue = $null) param($Key, [string]$SubPath = "", $DefaultValue = $null)
Get-Setting -SubPath $SubPath -Key $Key -DefaultValue $DefaultValue Get-Setting -SubPath $SubPath -Key $Key -DefaultValue $DefaultValue
} }
function Remove-LocalAuthSettings
{
param([string]$TenantId, [string]$AppId)
if ($global:JsonSettingsObj)
{
if ($global:JsonSettingsObj.ContainsKey($TenantId))
{
$global:JsonSettingsObj.Remove($TenantId) | Out-Null
Write-Host "Removed tenant settings for $TenantId from $SettingsFile" -ForegroundColor Green
}
if ($global:JsonSettingsObj["TenantId"] -eq $TenantId)
{
$global:JsonSettingsObj.Remove("TenantId") | Out-Null
Write-Host "Removed default TenantId from $SettingsFile" -ForegroundColor Green
}
$global:JsonSettingsObj | ConvertTo-Json -Depth 30 | Out-File -LiteralPath $global:JSonSettingFile -Force -Encoding utf8
}
if ($AppId)
{
if ($IsMacOS)
{
$null = security delete-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" 2>$null
Write-Host "Removed client secret for AppId $AppId from macOS Keychain" -ForegroundColor Green
}
else
{
Write-Host "Client secret was stored in $SettingsFile and has been removed along with the tenant node." -ForegroundColor Green
}
}
else
{
Write-Warning "No saved credentials found for tenant $TenantId."
}
}
#endregion #endregion
#region Determine TenantId #region Determine TenantId
@@ -85,6 +136,96 @@ if (-not $TenantId)
} }
#endregion #endregion
#region Delete saved credentials
if ($Delete)
{
$appIdToClean = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId"
Remove-LocalAuthSettings -TenantId $TenantId -AppId $appIdToClean
return
}
#endregion
#region Microsoft Graph modules
$requiredModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Applications")
foreach ($mod in $requiredModules)
{
if (-not (Get-Module $mod -ListAvailable))
{
throw "Module '$mod' is not installed. Run: Install-Module Microsoft.Graph -Scope CurrentUser"
}
}
Import-Module Microsoft.Graph.Authentication -Force
Import-Module Microsoft.Graph.Applications -Force
#endregion
#region Connect to Graph
Write-Host ""
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Write-Host "A browser window will open for authentication." -ForegroundColor Cyan
Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Organization.Read.All" -NoWelcome
#endregion
#region Resolve authenticated user for app naming
if (-not $PSBoundParameters.ContainsKey('DisplayName'))
{
try
{
$ctx = Get-MgContext -ErrorAction Stop
if ($ctx -and $ctx.Account)
{
$DisplayName = "IntuneManagement-$($ctx.Account)"
Write-Host "Using app display name: $DisplayName" -ForegroundColor DarkGray
}
}
catch { }
}
#endregion
#region Cache tenant name
try
{
$org = Get-MgOrganization -ErrorAction Stop
if ($org -and $org.DisplayName)
{
Save-AuthSetting -SubPath $TenantId -Key "TenantName" -Value $org.DisplayName
Write-Host "Cached tenant name: $($org.DisplayName)" -ForegroundColor Green
}
}
catch
{
Write-Warning "Failed to cache tenant name: $($_.Exception.Message)"
}
#endregion
#region Delete app registration and local credentials
if ($DeleteApp)
{
$appIdToClean = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId"
if ($appIdToClean)
{
$appToDelete = Get-MgApplication -Filter "appId eq '$appIdToClean'" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($appToDelete)
{
Remove-MgApplication -ApplicationId $appToDelete.Id
Write-Host "Deleted app registration $($appToDelete.DisplayName) ($appIdToClean) from tenant $TenantId" -ForegroundColor Green
}
else
{
Write-Warning "App registration $appIdToClean not found in tenant $TenantId."
}
}
else
{
Write-Warning "No AppId found in local settings for tenant $TenantId."
}
Remove-LocalAuthSettings -TenantId $TenantId -AppId $appIdToClean
Disconnect-MgGraph | Out-Null
return
}
#endregion
#region Check for existing credentials #region Check for existing credentials
$existingAppId = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId" $existingAppId = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId"
if ($existingAppId -and -not $Force) if ($existingAppId -and -not $Force)
@@ -116,27 +257,6 @@ if ($existingAppId -and -not $Force)
} }
#endregion #endregion
#region Microsoft Graph modules
$requiredModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Applications")
foreach ($mod in $requiredModules)
{
if (-not (Get-Module $mod -ListAvailable))
{
throw "Module '$mod' is not installed. Run: Install-Module Microsoft.Graph -Scope CurrentUser"
}
}
Import-Module Microsoft.Graph.Authentication -Force
Import-Module Microsoft.Graph.Applications -Force
#endregion
#region Connect to Graph
Write-Host ""
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Write-Host "A browser window will open for authentication." -ForegroundColor Cyan
Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" -NoWelcome
#endregion
#region App registration #region App registration
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" $graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
if (-not $graphSp) if (-not $graphSp)

View File

@@ -261,6 +261,12 @@ if(-not $TenantId)
Write-Host "No tenant ID provided. Exiting." -ForegroundColor Yellow Write-Host "No tenant ID provided. Exiting." -ForegroundColor Yellow
exit 0 exit 0
} }
$initPath = Join-Path $projectRoot "Scripts/Initialize-IntuneAuth.ps1"
& $initPath -TenantId $TenantId
Write-Host "`nOnboarding complete. Restarting launcher..." -ForegroundColor Green
Start-Sleep -Seconds 1
& $PSCommandPath
exit 0
} }
else else
{ {
@@ -300,6 +306,8 @@ $commonParams = @{
} }
$menuItems = @( $menuItems = @(
"15. Delete tenant auth and app registration"
"14. Delete local tenant auth only"
"13. Refresh tenant names" "13. Refresh tenant names"
"12. Initialize auth (one-time setup)" "12. Initialize auth (one-time setup)"
"11. Deploy baseline (dry-run / WhatIf)" "11. Deploy baseline (dry-run / WhatIf)"
@@ -360,6 +368,8 @@ while($true)
11 { $script = "Scripts/Deploy-IntuneBaseline.ps1"; $commonParams.WhatIf = $true } 11 { $script = "Scripts/Deploy-IntuneBaseline.ps1"; $commonParams.WhatIf = $true }
12 { $script = "Scripts/Initialize-IntuneAuth.ps1" } 12 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
13 { $script = $null } 13 { $script = $null }
14 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
15 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
default { continue } default { continue }
} }
@@ -424,9 +434,25 @@ while($true)
@("AppId","Secret","Certificate","AuthMode","RedirectUri","Interactive","Mode","WhatIf") | ForEach-Object { $launchParams.Remove($_) } @("AppId","Secret","Certificate","AuthMode","RedirectUri","Interactive","Mode","WhatIf") | ForEach-Object { $launchParams.Remove($_) }
} }
if($choiceNumber -eq 14)
{
$launchParams.Delete = $true
}
if($choiceNumber -eq 15)
{
$launchParams.DeleteApp = $true
}
# Execute in same process so TUI flows naturally # Execute in same process so TUI flows naturally
& $scriptPath @launchParams & $scriptPath @launchParams
if($choiceNumber -eq 14 -or $choiceNumber -eq 15)
{
Write-Host "`nTenant auth deleted. Exiting." -ForegroundColor Yellow
exit 0
}
Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
} }