feat(auth): sync full Graph permission set and patch existing apps

- Unified required Microsoft Graph app roles in Initialize-IntuneAuth.ps1
- Added permission patching for existing app registrations
- Logs the change and operations for audit
This commit is contained in:
2026-04-14 12:15:14 +02:00
parent 9dace83cff
commit 87b7af25a7
3 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# macOS Intune Toolkit Changelog
## 2026-04-13 — API Permissions Sync for `Initialize-IntuneAuth.ps1`
### Modified
- **`Scripts/Initialize-IntuneAuth.ps1`**
- Unified the required Microsoft Graph application permissions into a single `$requiredRoles` list defined before app creation/reuse logic:
- `DeviceManagementApps.ReadWrite.All`
- `DeviceManagementConfiguration.ReadWrite.All`
- `DeviceManagementManagedDevices.ReadWrite.All`
- `DeviceManagementScripts.ReadWrite.All`
- `DeviceManagementServiceConfig.ReadWrite.All`
- `DeviceManagementRBAC.ReadWrite.All`
- `Group.ReadWrite.All`
- `Directory.Read.All`
- `User.Read.All`
- `Organization.Read.All`
- `Policy.ReadWrite.ConditionalAccess`
- `Agreement.ReadWrite.All`
- `CloudPC.ReadWrite.All`
- `Application.Read.All`
- **Existing app patching**: When reusing an existing app registration, the script now inspects its current `RequiredResourceAccess`. If any required permissions are missing, it patches the app via `Update-MgApplication`, refreshes the local app object, and the downstream admin-consent loop automatically grants consent for the newly added roles.
---
## Prior delivered changes (context summary)
### New scripts added
- `Scripts/Bulk-AppAssignment.ps1` — bulk-assign apps to groups/All Users/All Devices
- `Scripts/Bulk-AssignmentManager.ps1` — add/remove assignments for any policy type using correct `@odata.type` and bulk `/assign` endpoint
- `Scripts/Backup-Restore-Assignments.ps1` — JSON backup with cross-tenant group name resolution
- `Scripts/Export-AssignmentsToCsv.ps1` — CSV and Markdown documentation output
- `Scripts/Bulk-RenamePolicies.ps1` — search/replace, add/strip prefix across displayName/description
- `Scripts/Bulk-DeviceOperations.ps1` — delete/retire/wipe/lock/sync with `-WhatIf` safeguards
- `Scripts/Start-IntuneToolkit.ps1` — unified reverse-numbered `fzf`-based launcher
- `Scripts/Initialize-IntuneAuth.ps1` — one-time Entra app + secret + Keychain setup
### Core / Extensions / Headless changes
- **`Extensions/MSGraph.psm1`**
- `Invoke-GraphRequest` now throws on 4xx/5xx HTTP errors (was silently returning null)
- Added `-AllPages` support to `Get-GraphObjects` and toolkit queries for large tenants
- **`Headless/IntuneManagement.Headless.psm1`**
- Expanded `Get-DefaultIntunePolicyObjectTypes` to ~45 types, including `DeviceManagementIntents`
- Threaded `NameSearchPattern` / `NameReplacePattern` through export/import/action flows
- **Settings Catalog fixes**
- Uses `name` property instead of `displayName` for queries/labels
- Assignments use `#microsoft.graph.deviceManagementConfigurationPolicyAssignment` and the bulk `POST …/assign` endpoint
- **TUI / `fzf`**
- Spacebar toggle, Esc to go back, reverse numbering (10→1) in unified launcher

64
OPERATIONS_LOG.md Normal file
View File

@@ -0,0 +1,64 @@
# Agent Operations Log
## Session: 2026-04-13T13:31:37+02:00
### Task
Update `Initialize-IntuneAuth.ps1` to ensure the Entra app registration is granted all Microsoft Graph permissions required by the expanded macOS Intune Toolkit, and log all operations.
---
## Operations performed
1. **ReadFile**
- Path: `/Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1`
- Line offset: `140`, Lines: `60`
- Purpose: Inspect the existing roles array and app creation logic.
2. **StrReplaceFile**
- Path: `/Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1`
- Change: Replaced the inline `$roles` array + app creation block with a unified `$requiredRoles` list and added logic to patch missing permissions into an existing app registration via `Update-MgApplication`.
- Impact: New apps get the full permission set; existing apps are updated if roles are missing.
3. **Grep**
- Path: `/Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1`
- Pattern: `New-MgServicePrincipalAppRoleAssignment`
- Purpose: Verify the downstream admin-consent loop would pick up updated roles.
4. **ReadFile**
- Path: `/Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1`
- Line offset: `220`, Lines: `50`
- Purpose: Inspect the consent-granting region around `New-MgServicePrincipalAppRoleAssignment`.
5. **Grep**
- Path: `/Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1`
- Pattern: `\$roles`
- Purpose: Confirm no stale `$roles` references remained after the refactor.
6. **Shell**
- Command: `pwsh -Command "$ErrorActionPreference='Stop'; Get-Command /Users/avedelphina/Local/IntuneManagement/Scripts/Initialize-IntuneAuth.ps1 | Out-Null; Write-Host 'Syntax OK'"`
- Result: Syntax OK (PowerShell parsed the script without errors).
7. **Shell**
- Command: `cd /Users/avedelphina/Local/IntuneManagement && git diff -- Scripts/Initialize-IntuneAuth.ps1`
- Result: No diff output (file is untracked in git).
8. **Shell**
- Command: `cd /Users/avedelphina/Local/IntuneManagement && git status --short`
- Result: Listed modified and untracked files, confirming `Scripts/Initialize-IntuneAuth.ps1` is `??` (untracked).
9. **WriteFile**
- Path: `/Users/avedelphina/Local/IntuneManagement/CHANGELOG_macOS_IntuneToolkit.md`
- Content: High-level changelog of the permissions update and prior context.
10. **WriteFile**
- Path: `/Users/avedelphina/Local/IntuneManagement/OPERATIONS_LOG.md`
- Content: This file.
---
## Files modified in this session
- `Scripts/Initialize-IntuneAuth.ps1`
## Files created in this session
- `CHANGELOG_macOS_IntuneToolkit.md`
- `OPERATIONS_LOG.md`

View File

@@ -0,0 +1,333 @@
<#
.SYNOPSIS
One-time setup helper for IntuneManagement headless authentication.
.DESCRIPTION
Creates a Microsoft Entra app registration (or reuses an existing one),
adds required Microsoft Graph permissions, creates a client secret, and
stores credentials securely:
- TenantId and AppId are saved to the JSON settings file.
- Client secret is saved to the macOS Keychain (default on macOS).
Requires: Microsoft.Graph.Authentication, Microsoft.Graph.Applications
Install if missing: Install-Module Microsoft.Graph -Scope CurrentUser
.PARAMETER TenantId
The Microsoft Entra tenant ID (GUID). If omitted, the script reads from
existing settings or prompts interactively.
.PARAMETER DisplayName
The display name for the app registration. Default: IntuneManagement-Headless.
.PARAMETER SettingsFile
Path to the JSON settings file. If omitted, defaults to the macOS_IntuneManagement
settings folder (~/Library/Application Support/macOS_IntuneManagement/Settings.json).
.PARAMETER Force
Recreate the app registration and secret even if existing credentials are found.
#>
[CmdletBinding()]
param(
[string]$TenantId,
[string]$DisplayName = "IntuneManagement-Headless",
[string]$SettingsFile,
[switch]$Force
)
$ErrorActionPreference = "Stop"
#region Helper: settings file
$coreModule = Join-Path (Split-Path -Parent $PSScriptRoot) "Core.psm1"
if (-not (Test-Path $coreModule))
{
throw "Could not find Core.psm1 at $coreModule"
}
Import-Module $coreModule -Force -Global
if (-not $SettingsFile)
{
$dataFolder = Get-CloudApiDataFolder
$SettingsFile = Join-Path $dataFolder "Settings.json"
}
$global:JSonSettingFile = $SettingsFile
Initialize-JsonSettings
function Save-AuthSetting
{
param($Key, $Value, [string]$SubPath = "")
Save-Setting -SubPath $SubPath -Key $Key -Value $Value
}
function Get-AuthSetting
{
param($Key, [string]$SubPath = "", $DefaultValue = $null)
Get-Setting -SubPath $SubPath -Key $Key -DefaultValue $DefaultValue
}
#endregion
#region Determine TenantId
if (-not $TenantId)
{
$TenantId = Get-AuthSetting -Key "TenantId"
if (-not $TenantId)
{
$TenantId = Read-Host "Enter your Microsoft Entra Tenant ID (GUID)"
}
}
if (-not $TenantId)
{
throw "TenantId is required."
}
#endregion
#region Check for existing credentials
$existingAppId = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId"
if ($existingAppId -and -not $Force)
{
$hasSecret = $false
if ($IsMacOS)
{
try
{
$keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$existingAppId" -w 2>$null
if ($keychainSecret) { $hasSecret = $true }
}
catch { }
}
else
{
$plainSecret = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppSecret"
if ($plainSecret) { $hasSecret = $true }
}
if ($hasSecret)
{
Write-Host ""
Write-Host "Existing credentials already configured for tenant $TenantId (AppId: $existingAppId)." -ForegroundColor Green
Write-Host "Use -Force to recreate the app registration and secret." -ForegroundColor Yellow
Write-Host ""
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" -NoWelcome
#endregion
#region App registration
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
if (-not $graphSp)
{
throw "Could not retrieve Microsoft Graph service principal."
}
$requiredRoles = @(
"DeviceManagementApps.ReadWrite.All",
"DeviceManagementConfiguration.ReadWrite.All",
"DeviceManagementManagedDevices.ReadWrite.All",
"DeviceManagementScripts.ReadWrite.All",
"DeviceManagementServiceConfig.ReadWrite.All",
"DeviceManagementRBAC.ReadWrite.All",
"Group.ReadWrite.All",
"Directory.Read.All",
"User.Read.All",
"Organization.Read.All",
"Policy.ReadWrite.ConditionalAccess",
"Agreement.ReadWrite.All",
"CloudPC.ReadWrite.All",
"Application.Read.All"
)
$resourceAccess = @()
foreach ($roleName in $requiredRoles)
{
$appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq $roleName } | Select-Object -First 1
if (-not $appRole)
{
Write-Warning "Could not find app role: $roleName"
continue
}
$resourceAccess += @{ id = $appRole.Id; type = "Role" }
}
$app = $null
$updatedPermissions = $false
if (-not $Force)
{
$existingApps = Get-MgApplication -Filter "displayName eq '$DisplayName'" -All
if ($existingApps)
{
$app = $existingApps | Select-Object -First 1
Write-Host "Reusing existing app registration: $($app.DisplayName) ($($app.AppId))" -ForegroundColor Yellow
# Check for missing permissions and patch if needed
$existingRra = $app.RequiredResourceAccess | Where-Object { $_.resourceAppId -eq "00000003-0000-0000-c000-000000000000" }
$existingIds = @()
if($existingRra -and $existingRra.resourceAccess)
{
$existingIds = $existingRra.resourceAccess | Select-Object -ExpandProperty id
}
$missingAccess = $resourceAccess | Where-Object { $_.id -notin $existingIds }
if($missingAccess)
{
Write-Host "Adding missing Graph API permissions to existing app..." -ForegroundColor Cyan
$newRra = @(@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = @($existingIds | ForEach-Object { @{ id = $_; type = "Role" } }) + $missingAccess
})
Update-MgApplication -ApplicationId $app.Id -RequiredResourceAccess $newRra
$updatedPermissions = $true
# Refresh app object
$app = Get-MgApplication -ApplicationId $app.Id
}
}
}
if (-not $app)
{
Write-Host "Creating new app registration: $DisplayName" -ForegroundColor Cyan
$appParams = @{
DisplayName = $DisplayName
SignInAudience = "AzureADMyOrg"
RequiredResourceAccess = @(@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = $resourceAccess
})
}
$app = New-MgApplication @appParams
}
#endregion
#region Service Principal & Admin Consent
$sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" -ErrorAction SilentlyContinue
if (-not $sp)
{
Write-Host "Creating service principal for the app..." -ForegroundColor Cyan
$sp = New-MgServicePrincipal -AppId $app.AppId
}
$consentGranted = $false
if ($sp)
{
Write-Host "Granting admin consent for Microsoft Graph permissions..." -ForegroundColor Cyan
$requiredAppRoles = $app.RequiredResourceAccess[0].resourceAccess
foreach ($ra in $requiredAppRoles)
{
$existing = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
Where-Object { $_.AppRoleId -eq $ra.id }
if (-not $existing)
{
try
{
New-MgServicePrincipalAppRoleAssignment `
-ServicePrincipalId $sp.Id `
-PrincipalId $sp.Id `
-ResourceId $graphSp.Id `
-AppRoleId $ra.id | Out-Null
$consentGranted = $true
}
catch
{
Write-Warning "Failed to grant consent for role $($ra.id): $($_.Exception.Message)"
}
}
else
{
$consentGranted = $true
}
}
}
#endregion
#region Client Secret
Write-Host "Creating client secret..." -ForegroundColor Cyan
$passwordCred = @{
displayName = "IntuneManagementSecret"
endDateTime = (Get-Date).AddYears(1)
}
$secret = Add-MgApplicationPassword -ApplicationId $app.Id -PasswordCredential $passwordCred
#endregion
#region Save settings
Write-Host "Saving settings to $SettingsFile ..." -ForegroundColor Cyan
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId" -Value $app.AppId
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppLogin" -Value $true
Save-AuthSetting -Key "TenantId" -Value $TenantId
if ($IsMacOS)
{
Write-Host "Storing client secret in macOS Keychain..." -ForegroundColor Cyan
$null = security add-generic-password -a "IntuneManagement" -s "IntuneMgmt-$($app.AppId)" -w "$($secret.SecretText)" -U 2>$null
}
else
{
Write-Warning "Not running on macOS. Storing client secret in the settings file (less secure)."
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppSecret" -Value $secret.SecretText
}
#endregion
#region Summary
Write-Host ""
Write-Host "=============================================================" -ForegroundColor Green
Write-Host "Authentication setup complete!" -ForegroundColor Green
Write-Host "=============================================================" -ForegroundColor Green
Write-Host "TenantId : $TenantId"
Write-Host "AppId : $($app.AppId)"
Write-Host "Settings : $SettingsFile"
if ($IsMacOS)
{
Write-Host "Secret : <stored in macOS Keychain>"
}
else
{
Write-Host "Secret : $($secret.SecretText)"
}
Write-Host "=============================================================" -ForegroundColor Green
if (-not $consentGranted)
{
Write-Host "IMPORTANT: Admin consent could not be granted automatically." -ForegroundColor Yellow
Write-Host " Go to the Entra portal > API Permissions and click" -ForegroundColor Yellow
Write-Host " 'Grant admin consent for <tenant>' before using" -ForegroundColor Yellow
Write-Host " the app for Export or Import." -ForegroundColor Yellow
Write-Host "=============================================================" -ForegroundColor Green
}
else
{
Write-Host "Admin consent granted successfully." -ForegroundColor Green
Write-Host "=============================================================" -ForegroundColor Green
}
Write-Host ""
Write-Host "You can now run exports without specifying -AppId or -Secret:" -ForegroundColor Cyan
Write-Host " ./Scripts/Export-Policies.ps1 -TenantId `"$TenantId`" -ExportPath `"/tmp/intune-export`" -IncludeAssignments" -ForegroundColor Cyan
Write-Host ""
Disconnect-MgGraph | Out-Null
#endregion