Add worker permissions

To grand the corrects permissions to the created worker follow the steps below:

Copy the script below into a file, save the file as add-permission

1

Copy code and save file.

Copy the code and save the file to grand-worker-permissions.ps1

#!/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 })

2

Open a PowerShell window

Run the PowerShell file using the example below.

.\grand-worker-permissions.ps1 -TenantId "6a80764a-..." -WorkerName "my-worker"

Last updated