diff --git a/CHANGELOG_macOS_IntuneToolkit.md b/CHANGELOG_macOS_IntuneToolkit.md new file mode 100644 index 0000000..06c3eb2 --- /dev/null +++ b/CHANGELOG_macOS_IntuneToolkit.md @@ -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 diff --git a/OPERATIONS_LOG.md b/OPERATIONS_LOG.md new file mode 100644 index 0000000..d1ef1bd --- /dev/null +++ b/OPERATIONS_LOG.md @@ -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` diff --git a/Scripts/Initialize-IntuneAuth.ps1 b/Scripts/Initialize-IntuneAuth.ps1 new file mode 100644 index 0000000..93c6214 --- /dev/null +++ b/Scripts/Initialize-IntuneAuth.ps1 @@ -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 : " +} +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 ' 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