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:
49
CHANGELOG_macOS_IntuneToolkit.md
Normal file
49
CHANGELOG_macOS_IntuneToolkit.md
Normal 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
64
OPERATIONS_LOG.md
Normal 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`
|
||||
333
Scripts/Initialize-IntuneAuth.ps1
Normal file
333
Scripts/Initialize-IntuneAuth.ps1
Normal 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
|
||||
Reference in New Issue
Block a user