Add worker permissions
1
Copy code and save file.
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Assigns Microsoft Graph application permissions to a Worker's managed identity.
.DESCRIPTION
Uses Microsoft.Graph.Authentication module for interactive browser login (supports
Conditional Access / MFA). All Graph calls use Invoke-MgGraphRequest.
.PARAMETER TenantId
Your Azure AD Tenant ID.
.PARAMETER WorkerName
The App Service name (matches the system-assigned managed identity display name).
.PARAMETER Permissions
Array of Microsoft Graph application permission names to assign.
Defaults to the full set required by IntuneAssistant Worker.
.EXAMPLE
# Use defaults (all required permissions)
.\grand-worker-permissions.ps1 -TenantId "6a80764a-..." -WorkerName "my-worker"
.EXAMPLE
# Assign only specific permissions
.\grand-worker-permissions.ps1 -TenantId "6a80764a-..." -WorkerName "my-worker" `
-Permissions @('Mail.Send', 'Group.Read.All')
.NOTES
Requires: Application Administrator or Global Administrator role in Azure AD.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, HelpMessage = "Your Azure AD Tenant ID")]
[ValidateNotNullOrEmpty()]
[string]$TenantId,
[Parameter(Mandatory = $true, HelpMessage = "App Service name (= managed identity display name)")]
[ValidateNotNullOrEmpty()]
[string]$WorkerName,
[Parameter(Mandatory = $false, HelpMessage = "Graph application permissions to assign")]
[string[]]$Permissions = @(
# Mail
'Mail.Send',
# Identity & directory
'Group.Read.All',
'GroupMember.Read.All',
'User.ReadBasic.All',
'RoleManagement.Read.Directory',
'AuditLog.Read.All',
'Application.Read.All',
'AppRoleAssignment.Read.All',
# Device management
'DeviceManagementConfiguration.Read.All',
'DeviceManagementScripts.Read.All',
'DeviceManagementRBAC.Read.All',
'DeviceManagementApps.Read.All',
'DeviceManagementServiceConfig.Read.All',
'DeviceManagementManagedDevices.Read.All',
# Policy & monitoring
'Policy.Read.ConditionalAccess',
'ConfigurationMonitoring.ReadWrite.All' # non-standard — skipped if not found
)
)
$ErrorActionPreference = 'Stop'
# ─────────────────────────────────────────────────────────────────────────────
# CONSTANTS
# ─────────────────────────────────────────────────────────────────────────────
$MsGraphAppId = '00000003-0000-0000-c000-000000000000' # Microsoft Graph
$GraphBaseUrl = 'https://graph.microsoft.com/v1.0'
# ─────────────────────────────────────────────────────────────────────────────
# ENSURE MODULE (authentication only — not the full Graph SDK)
# ─────────────────────────────────────────────────────────────────────────────
$moduleName = 'Microsoft.Graph.Authentication'
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
Write-Host "Module '$moduleName' not found. Installing..." -ForegroundColor Yellow
Install-Module $moduleName -Scope CurrentUser -Force -AllowClobber
Write-Host " ✓ Installed" -ForegroundColor Green
}
Import-Module $moduleName -ErrorAction Stop
# ─────────────────────────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────────────────────────
function Invoke-Graph {
param(
[string]$Path,
[string]$Method = 'GET',
[hashtable]$Body
)
$params = @{
Uri = "$GraphBaseUrl$Path"
Method = $Method
OutputType = 'PSObject'
}
if ($Body) { $params.Body = ($Body | ConvertTo-Json -Depth 5) }
return Invoke-MgGraphRequest @params
}
function Get-GraphAllPages {
param([string]$Path)
$items = [System.Collections.Generic.List[object]]::new()
$uri = "$GraphBaseUrl$Path"
while ($uri) {
$resp = Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject
if ($resp.value) { $items.AddRange([object[]]$resp.value) }
$uri = $resp.'@odata.nextLink'
}
return $items
}
# ─────────────────────────────────────────────────────────────────────────────
# BANNER
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host " Worker Managed Identity — Graph Permission Setup" -ForegroundColor Cyan
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host " TenantId : $TenantId"
Write-Host " WorkerName : $WorkerName"
Write-Host " Permissions: $($Permissions.Count) requested"
Write-Host ""
# ─────────────────────────────────────────────────────────────────────────────
# STEP 1: INTERACTIVE LOGIN
# ─────────────────────────────────────────────────────────────────────────────
Write-Host "[1/5] Opening browser for interactive login..." -ForegroundColor Yellow
Write-Host " (MFA and Conditional Access are fully supported)" -ForegroundColor Gray
Connect-MgGraph `
-TenantId $TenantId `
-Scopes 'Application.Read.All', 'AppRoleAssignment.ReadWrite.All' `
-NoWelcome
$ctx = Get-MgContext
Write-Host " ✓ Signed in as : $($ctx.Account)" -ForegroundColor Green
Write-Host " ✓ Tenant : $($ctx.TenantId)" -ForegroundColor Gray
# ─────────────────────────────────────────────────────────────────────────────
# STEP 2: FIND MANAGED IDENTITY SERVICE PRINCIPAL
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "[2/5] Searching for managed identity: '$WorkerName'..." -ForegroundColor Yellow
$encoded = [uri]::EscapeDataString("displayName eq '$WorkerName'")
$spResp = Invoke-Graph -Path "/servicePrincipals?`$filter=$encoded&`$select=id,displayName,appId,servicePrincipalType"
$allMatches = @($spResp.value)
if ($allMatches.Count -eq 0) {
Write-Error @"
No service principal found with displayName '$WorkerName'.
Make sure:
1. The App Service exists in this tenant
2. System-assigned managed identity is ENABLED
Azure Portal → App Service → Identity → System assigned → Status: On
3. The name matches exactly (case-sensitive)
"@
Disconnect-MgGraph | Out-Null
exit 1
}
$managedIdentity = $allMatches |
Where-Object { $_.servicePrincipalType -eq 'ManagedIdentity' } |
Select-Object -First 1
if (-not $managedIdentity) { $managedIdentity = $allMatches[0] }
Write-Host " ✓ Found: $($managedIdentity.displayName)" -ForegroundColor Green
Write-Host " Object ID : $($managedIdentity.id)" -ForegroundColor Gray
Write-Host " Type : $($managedIdentity.servicePrincipalType)" -ForegroundColor Gray
if ($managedIdentity.servicePrincipalType -ne 'ManagedIdentity') {
Write-Warning "Type is not 'ManagedIdentity' — verify system-assigned identity is enabled on the App Service."
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 3: FIND MICROSOFT GRAPH SP + REQUIRED APP ROLES
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "[3/5] Finding Microsoft Graph service principal and app roles..." -ForegroundColor Yellow
$graphFilter = [uri]::EscapeDataString("appId eq '$MsGraphAppId'")
$graphResp = Invoke-Graph -Path "/servicePrincipals?`$filter=$graphFilter&`$select=id,displayName,appRoles"
$graphSp = @($graphResp.value)[0]
if (-not $graphSp) {
Write-Error "Microsoft Graph service principal not found in tenant '$TenantId'."
Disconnect-MgGraph | Out-Null
exit 1
}
Write-Host " ✓ Found Microsoft Graph (Object ID: $($graphSp.id))" -ForegroundColor Green
$roleMap = [ordered]@{}
foreach ($permName in $Permissions) {
$role = $graphSp.appRoles | Where-Object {
$_.value -eq $permName -and $_.allowedMemberTypes -contains 'Application'
}
if ($role) {
$roleMap[$permName] = $role.id
Write-Host " ✓ Located : $permName" -ForegroundColor Gray
} else {
Write-Warning " Skipped : '$permName' — not found as application permission in Microsoft Graph"
}
}
if ($roleMap.Count -eq 0) {
Write-Error "None of the requested permissions could be located in Microsoft Graph."
Disconnect-MgGraph | Out-Null
exit 1
}
# ─────────────────────────────────────────────────────────────────────────────
# STEP 4: CHECK EXISTING ASSIGNMENTS
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "[4/5] Checking existing assignments on managed identity..." -ForegroundColor Yellow
$existing = Get-GraphAllPages -Path "/servicePrincipals/$($managedIdentity.id)/appRoleAssignments"
Write-Host " Found $($existing.Count) existing app role assignment(s)" -ForegroundColor Gray
# ─────────────────────────────────────────────────────────────────────────────
# STEP 5: ASSIGN MISSING PERMISSIONS
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "[5/5] Assigning missing permissions..." -ForegroundColor Yellow
$assigned = 0; $alreadyHad = 0; $failed = 0
foreach ($permName in $roleMap.Keys) {
$roleId = $roleMap[$permName]
$alreadyAssigned = $existing | Where-Object {
$_.resourceId -eq $graphSp.id -and $_.appRoleId -eq $roleId
}
if ($alreadyAssigned) {
Write-Host " ○ Already assigned : $permName" -ForegroundColor Cyan
$alreadyHad++
continue
}
try {
Invoke-Graph `
-Path "/servicePrincipals/$($managedIdentity.id)/appRoleAssignments" `
-Method POST `
-Body @{
principalId = $managedIdentity.id
resourceId = $graphSp.id
appRoleId = $roleId
} | Out-Null
Write-Host " ✓ Assigned : $permName" -ForegroundColor Green
$assigned++
}
catch {
Write-Host " ✗ Failed : $permName — $($_.Exception.Message)" -ForegroundColor Red
$failed++
}
}
# ─────────────────────────────────────────────────────────────────────────────
# SUMMARY
# ─────────────────────────────────────────────────────────────────────────────
Disconnect-MgGraph | Out-Null
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host " Summary" -ForegroundColor Cyan
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host " Worker : $($managedIdentity.displayName)"
Write-Host " Object ID : $($managedIdentity.id)"
Write-Host " Newly assigned : $assigned" -ForegroundColor $(if ($assigned -gt 0) { 'Green' } else { 'White' })
Write-Host " Already had : $alreadyHad"
Write-Host " Failed : $failed" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'White' })
if ($failed -gt 0) {
Write-Host ""
Write-Host " Failures may indicate insufficient role." -ForegroundColor Yellow
Write-Host " Required: Application Administrator or Global Administrator" -ForegroundColor Yellow
}
if ($assigned -gt 0 -or $alreadyHad -gt 0) {
Write-Host ""
Write-Host " IMPORTANT — Exchange Online step still required for Mail.Send" -ForegroundColor Yellow
Write-Host " Connect-ExchangeOnline" -ForegroundColor White
Write-Host " Test-ApplicationAccessPolicy -Identity <sender@domain> -AppId $($managedIdentity.id)" -ForegroundColor White
}
Write-Host ""
Write-Host " Done!" -ForegroundColor Green
Write-Host ""
exit $(if ($failed -gt 0) { 1 } else { 0 })Last updated