Friday, September 25, 2026

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated (with ServiceMessage Examples)

You've enabled a system-assigned managed identity on an Azure Automation Account, and the runbook needs to read Microsoft 365 Message Center posts through Microsoft Graph. You open Entra ID → Enterprise applications → your identity → Permissions and there's no Add a permission button, and Grant admin consent is greyed out.

This article explains why, how to assign the permission with PowerShell, and why one permission people commonly ask for (ServiceMessageViewpoint.Write) fails with a confusing Edm.Guid error.


1. Why the portal can't do it

A managed identity is a service principal with no app registration behind it. In the portal, the API permissions blade (where you click Add a permission → Microsoft Graph) exists only on app registrations. The enterprise app Permissions blade just shows what has already been granted.

To give a managed identity Graph permissions, you create the grant directly on its service principal:

Permission typeGraph object createdPowerShell cmdletWorks for managed identity at runtime?
Application (app role)appRoleAssignmentNew-MgServicePrincipalAppRoleAssignment✅ Yes
Delegated (scope)oauth2PermissionGrantNew-MgOauth2PermissionGrant❌ No, a managed identity has no signed-in user

You can do this with PowerShell, the Azure CLI (az rest), or Graph Explorer. All three call the same Graph API.


2. Know your permission before you assign it

The service message permissions are a good example of how application and delegated permissions differ. From the Microsoft Graph permissions reference:

PermissionApplicationDelegatedPurpose
ServiceMessage.Read.All✅ 1b620472-6534-4fe6-9df2-4680e8aa28ec✅Read Message Center posts
ServiceHealth.Read.All✅ 79c261e0-fe76-4144-aad5-bdc68fbe4037✅Read service health and incidents
ServiceMessageViewpoint.Write❌ none✅Mark posts read, archived or favourite for the signed-in user

Two things to note:

  • There is no ServiceMessage.ReadWrite.All. For app-only automation, ServiceMessage.Read.All is the highest service message permission available.
  • ServiceMessageViewpoint.Write is delegated only. Read, archived and favourite status is stored per user, so Graph doesn't offer it for app-only access.

Tip: Permission names are the same in both lists, but their GUIDs differ. Always look up the ID from the correct collection: AppRoles for application permissions, Oauth2PermissionScopes for delegated ones.


3. Prerequisites

ItemRequirement
RoleGlobal Administrator or Privileged Role Administrator
ModulesMicrosoft.Graph.Applications, Microsoft.Graph.Identity.SignIns
IdentityManaged identity enabled on the resource (e.g. Automation Account → Identity → System assigned → On)

4. Assign an application permission (the one your runbook can use)

Run these blocks one at a time. Replace aa-automation-demo with your resource name.

# 1. Install and import the module (first time only)
Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Force -AllowClobber
Import-Module Microsoft.Graph.Applications

# 2. Sign in
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All" -NoWelcome

# 3. Get the managed identity's service principal
$miName = "aa-automation-demo"
$miSp = Get-MgServicePrincipal -Filter "displayName eq '$miName'"
$miSp | Select-Object DisplayName, Id, ServicePrincipalType     # expect: ManagedIdentity

# 4. Get the Microsoft Graph service principal
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# 5. Find the application permission (app role)
$role = $graphSp.AppRoles | Where-Object { $_.Value -eq "ServiceMessage.Read.All" -and $_.AllowedMemberTypes -contains "Application" }
$role | Select-Object Value, Id                                  # must return a row

# 6. Check whether it's already assigned
$existing = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.ResourceId -eq $graphSp.Id -and $_.AppRoleId -eq $role.Id }

# 7. Assign it
if (-not $role) {
    Write-Warning "Not an application permission. Check the name, or use the delegated section."
} elseif ($existing) {
    Write-Host "Already assigned" -ForegroundColor Yellow
} else {
    New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -PrincipalId $miSp.Id -ResourceId $graphSp.Id -AppRoleId $role.Id
    Write-Host "Assigned $($role.Value)" -ForegroundColor Green
}

# 8. Check the result
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.ResourceId -eq $graphSp.Id } |
    ForEach-Object { $rid = $_.AppRoleId; ($graphSp.AppRoles | Where-Object { $_.Id -eq $rid }).Value }

Disconnect-MgGraph

Refresh Enterprise applications → your identity → Permissions in the portal. The permission shows with type Application.


5. The Edm.Guid error explained

If you put ServiceMessageViewpoint.Write into step 5 above, you'll get:

New-MgServicePrincipalAppRoleAssignment : Cannot convert the literal '' to the expected type 'Edm.Guid'.
Status: 400 (BadRequest)
ErrorCode: Request_BadRequest
SymptomCause
Step 5 returns nothingThe permission isn't in $graphSp.AppRoles because it's delegated-only
$role.Id is emptyGraph receives appRoleId: ""
Edm.Guid 400 errorAn empty string can't be converted to a GUID

To confirm which list a permission belongs to:

# Application permissions
$graphSp.AppRoles | Where-Object { $_.Value -like "ServiceMessage*" } | Select-Object Value, Id

# Delegated permissions
$graphSp.Oauth2PermissionScopes | Where-Object { $_.Value -like "ServiceMessage*" } | Select-Object Value, Id

6. Granting a delegated permission to a managed identity (and why it won't help)

You can grant a delegated scope to a managed identity's service principal with an admin-consent (AllPrincipals) oauth2PermissionGrant. It will then appear on the Permissions blade with type Delegated:

Import-Module Microsoft.Graph.Identity.SignIns
Connect-MgGraph -Scopes "Application.Read.All","DelegatedPermissionGrant.ReadWrite.All" -NoWelcome

$miSp    = Get-MgServicePrincipal -Filter "displayName eq 'aa-automation-demo'"
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$scopeName = "ServiceMessageViewpoint.Write"

$grant = Get-MgOauth2PermissionGrant -All |
    Where-Object { $_.ClientId -eq $miSp.Id -and $_.ResourceId -eq $graphSp.Id -and $_.ConsentType -eq "AllPrincipals" }

if (-not $grant) {
    New-MgOauth2PermissionGrant -BodyParameter @{
        clientId    = $miSp.Id
        consentType = "AllPrincipals"
        resourceId  = $graphSp.Id
        scope       = $scopeName
    }
} elseif (($grant.Scope -split " ") -contains $scopeName) {
    Write-Host "Already granted" -ForegroundColor Yellow
} else {
    Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id -Scope (($grant.Scope.Trim() + " " + $scopeName).Trim())
}

Disconnect-MgGraph

But the runbook still can't use it. A managed identity always gets an app-only token, and app-only tokens contain roles (application permissions), never scp (delegated scopes). Calls to markRead or archive from the runbook will still return 403.


7. Marking Message Center posts read or archived (the delegated way)

If you need viewpoint actions, run them as a signed-in admin:

Connect-MgGraph -Scopes "ServiceMessage.Read.All","ServiceMessageViewpoint.Write" -NoWelcome

$body = @{ messageIds = @("MC000001","MC000002") } | ConvertTo-Json
$base = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages"

Invoke-MgGraphRequest -Method POST -Uri "$base/markRead" -Body $body -ContentType "application/json"
Invoke-MgGraphRequest -Method POST -Uri "$base/archive"  -Body $body -ContentType "application/json"
ActionEndpoint
Mark read / unread/markRead, /markUnread
Archive / unarchive/archive, /unarchive
Favourite / unfavourite/favorite, /unfavorite

Status changes apply only to the signed-in user. Other admins still see the posts unchanged.


8. Auditing: who added a permission, and how?

Every assignment is logged. Go to Enterprise applications → your identity → Audit logs and look for:

ActivityMeaning
Add app role assignment to service principalAn application permission was assigned
Add delegated permission grantA delegated scope was granted

The User-Agent field shows the tool that was used. For example, AZURECLI/… cloud-shell means Azure CLI in Cloud Shell, and a Graph PowerShell SDK agent string means Microsoft.Graph PowerShell.


9. Removing a permission

# Application permission
$assignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -All |
    Where-Object { $_.AppRoleId -eq $role.Id }
Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miSp.Id -AppRoleAssignmentId $assignment.Id

# Delegated grant (entire grant)
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id

You can also use Review permissions on the enterprise app's Permissions blade.


10. Key takeaways

#Takeaway
1The portal can't add Graph permissions to a managed identity. Use PowerShell, the CLI or Graph Explorer
2Application permission → New-MgServicePrincipalAppRoleAssignment; delegated → New-MgOauth2PermissionGrant
3Look up permission IDs from AppRoles (application) or Oauth2PermissionScopes (delegated)
4Cannot convert the literal '' to 'Edm.Guid' usually means the permission isn't an application permission
5ServiceMessageViewpoint.Write is delegated only, so a managed identity can't use it at runtime
6Managed identity tokens are cached, so allow time before a new permission takes effect in a running job

References

Friday, September 18, 2026

Power Automate Desktop: Find One-to-Many Values Between Two Columns

Power Automate Desktop: Find One-to-Many Values Between Two Columns

The Problem

You have a table with two columns, A and B. You want to find every A value that is linked to more than one different B value.

✅ One-to-many❌ Not one-to-many
Apple → Red, GreenBanana → Yellow, Yellow (same value repeated)
Date → Brown (only one value)

No scripts needed. Only built-in Power Automate Desktop (PAD) actions.


Sample Data

AB
AppleRed
AppleGreen
BananaYellow
BananaYellow
CherryRed
CherryBlack
DateBrown
FigPurple
FigGreen
GrapePurple

How It Works (3 Simple Steps)

StepWhat happens
1Get the unique list of A values
2For each A value, collect its unique B values
3If there are more than 1 B values → add it to the result

Variables Used

VariableTypePurpose
InputDTData tableSource data (A, B)
DistinctAValuesListUnique A values
OneToManyResultsData tableFinal result
CurrentRowData rowRow being read (Step 1)
CurrentAValueTextA value being checked
BValuesForThisAListUnique B values for current A
DataRowData rowRow being read (Step 2)
BValuesTextTextB values joined as "Red, Green"

Build the Flow

Step 1 — Create the input table

Variables → Create new data table → add columns A and B → enter the sample rows → name it InputDT.

In real projects, use Excel → Read from Excel worksheet instead.

Step 2 — Get unique A values

ActionSetting
Create new listName: DistinctAValues
For eachLoop InputDT, item: CurrentRow
IfDistinctAValues Does not contain CurrentRow['A']
Add item to listAdd CurrentRow['A'] to DistinctAValues

Result: Apple, Banana, Cherry, Date, Fig, Grape

Step 3 — Create the result table

Variables → Create new data table → columns A Value and Matching B Values → name it OneToManyResults.

💡 The visual builder adds one empty row by default. Delete it in the builder (or add Clear data table right after) so your result doesn't start with a blank line.

Step 4 — Check each A value

ActionSetting
For eachLoop DistinctAValues, item: CurrentAValue
Create new listName: BValuesForThisA (inside the loop, so it resets each time)
For eachLoop InputDT, item: DataRow
IfDataRow['A'] Equal to CurrentAValue
IfBValuesForThisA Does not contain DataRow['B']
Add item to listAdd DataRow['B'] to BValuesForThisA
IfBValuesForThisA.Count Greater than 1
Join textJoin BValuesForThisA with ,  → BValuesText
Insert row into data tableAdd [CurrentAValue, BValuesText] to OneToManyResults

Step 5 — Show the result

Message boxes → Display message → show OneToManyResults.


Output

A ValueMatching B Values
AppleRed, Green
CherryRed, Black
FigPurple, Green

Banana, Date and Grape are skipped. ✔️


Quick Tips

TipWhy
Create BValuesForThisA inside the loopOtherwise old values carry over
Use %CurrentRow[0]% if no headersColumn index starts at 0
Good for small/medium tablesNested loops slow down on thousands of rows

Complete Code

Copy the code below and paste it directly into the PAD designer (Ctrl + V).

Variables.CreateNewDatatable InputTable: { ^['A', 'B'], [$'''Apple''', $'''Red'''], [$'''Apple''', $'''Green'''], [$'''Banana''', $'''Yellow'''], [$'''Banana''', $'''Yellow'''], [$'''Cherry''', $'''Red'''], [$'''Cherry''', $'''Black'''], [$'''Date''', $'''Brown'''], [$'''Fig''', $'''Purple'''], [$'''Fig''', $'''Green'''], [$'''Grape''', $'''Purple'''] } DataTable=> InputDT
Variables.CreateNewList List=> DistinctAValues
LOOP FOREACH CurrentRow IN InputDT
    IF NotContains(DistinctAValues, CurrentRow['A'], False) THEN
        Variables.AddItemToList Item: CurrentRow['A'] List: DistinctAValues
    END
END
Variables.CreateNewDatatable InputTable: { ^['A Value', 'Matching B Values'], [$'''''', $''''''] } DataTable=> OneToManyResults
LOOP FOREACH CurrentAValue IN DistinctAValues
    Variables.CreateNewList List=> BValuesForThisA
    LOOP FOREACH DataRow IN InputDT
        IF DataRow['A'] = CurrentAValue THEN
            IF NotContains(BValuesForThisA, DataRow['B'], False) THEN
                Variables.AddItemToList Item: DataRow['B'] List: BValuesForThisA
            END
        END
    END
    IF BValuesForThisA.Count > 1 THEN
        Text.JoinText.JoinWithCustomDelimiter List: BValuesForThisA CustomDelimiter: $''', ''' Result=> BValuesText
        Variables.AddRowToDataTable.AppendRowToDataTable DataTable: OneToManyResults RowToAdd: [CurrentAValue, BValuesText]
    END
END
Display.ShowMessageDialog.ShowMessage Title: $'''One-to-Many Results''' Message: OneToManyResults Icon: Display.Icon.None Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: False ButtonPressed=> ButtonPressed







References

Featured Post

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated

Assigning Microsoft Graph Permissions to a Managed Identity: Application vs Delegated (with ServiceMessage Examples) You've enabled a sy...

Popular posts