From 7d82347b8536d2717a757058659ed8127b3d524b Mon Sep 17 00:00:00 2001 From: Pallavi Vemulapalli Date: Tue, 11 Aug 2026 12:55:46 -0500 Subject: [PATCH 1/3] Adding script to add graph claims to 3P App for Cross Tenant email sync to Dataverse --- .../Grant-SssGraphApplicationPermissions.ps1 | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 diff --git a/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 new file mode 100644 index 000000000..b6bc33fd9 --- /dev/null +++ b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 @@ -0,0 +1,266 @@ +<# +.SYNOPSIS +Configures and grants consent for all Microsoft Graph application permissions +required by the cross-tenant SSS workload. + +.DESCRIPTION +Adds the complete Graph application-permission set to an existing Microsoft +Entra app registration, preserves its other API permissions, and grants the +corresponding app roles to its enterprise application. + +The script grants Mail.ReadWrite, Mail.Send, Calendars.ReadWrite, +Contacts.ReadWrite, MailboxSettings.ReadWrite, Tasks.ReadWrite.All, +Chat.Read.All, OnlineMeetings.ReadWrite.All, CallRecords.Read.All, +MailboxConfigItem.ReadWrite, and User.Read.All. There are no workload switches. + +PREREQUISITES + +Before running the script: +1. Install Microsoft Graph PowerShell: + Install-Module Microsoft.Graph -Scope CurrentUser +2. Sign in with an activated Privileged Role Administrator or Global + Administrator role. +3. Confirm that both the app registration and its enterprise application + already exist in the target tenant. + +Use -WhatIf first to review the proposed changes. The script prompts for +confirmation before changing the app registration and before granting each +permission. + +.PARAMETER TenantId +The Microsoft Entra tenant ID containing the app registration and Exchange +Online mailboxes. + +.PARAMETER ClientId +The Application (client) ID of the existing cross-tenant SSS app registration. + +.EXAMPLE +.\Grant-SssGraphApplicationPermissions.ps1 ` + -TenantId "11111111-2222-3333-4444-555555555555" ` + -ClientId "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" ` + -WhatIf + +Previews every proposed app-registration and consent change without applying +it. + +.EXAMPLE +.\Grant-SssGraphApplicationPermissions.ps1 ` + -TenantId "11111111-2222-3333-4444-555555555555" ` + -ClientId "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + +Connects to Microsoft Graph, configures all required application permissions, +prompts for confirmation, grants administrator consent, and displays the final +consent state. + +.NOTES +This script doesn't create the app registration, create credentials, configure +Exchange Application RBAC, or deploy Dynamics 365 App for Outlook. +#> + +[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] +param( + [Parameter(Mandatory)] + [guid]$TenantId, + + [Parameter(Mandatory)] + [guid]$ClientId +) + +$ErrorActionPreference = "Stop" +$graphAppId = "00000003-0000-0000-c000-000000000000" +$requiredPermissions = @( + "Mail.ReadWrite" + "Mail.Send" + "Calendars.ReadWrite" + "Contacts.ReadWrite" + "MailboxSettings.ReadWrite" + "Tasks.ReadWrite.All" + "Chat.Read.All" + "OnlineMeetings.ReadWrite.All" + "CallRecords.Read.All" + "MailboxConfigItem.ReadWrite" + "User.Read.All" +) + +$requiredCommands = @( + "Connect-MgGraph" + "Get-MgApplication" + "Update-MgApplication" + "Get-MgServicePrincipal" + "Get-MgServicePrincipalAppRoleAssignment" + "New-MgServicePrincipalAppRoleAssignment" +) + +$missingCommands = $requiredCommands | Where-Object { + -not (Get-Command $_ -ErrorAction SilentlyContinue) +} + +if ($missingCommands) { + throw @" +Microsoft Graph PowerShell is required. Install it, then rerun this script: + +Install-Module Microsoft.Graph -Scope CurrentUser + +Missing commands: $($missingCommands -join ", ") +"@ +} + +Connect-MgGraph ` + -TenantId $TenantId ` + -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" ` + -NoWelcome + +$context = Get-MgContext +if ($context.TenantId -ne "$TenantId") { + throw "Connected to tenant '$($context.TenantId)', but '$TenantId' was requested." +} + +$application = @( + Get-MgApplication ` + -Filter "appId eq '$ClientId'" ` + -Property "id,appId,displayName,requiredResourceAccess" +) +if ($application.Count -ne 1) { + throw "Expected one app registration with client ID '$ClientId'; found $($application.Count)." +} +$application = $application[0] + +$clientServicePrincipal = @( + Get-MgServicePrincipal ` + -Filter "appId eq '$ClientId'" ` + -Property "id,appId,displayName" +) +if ($clientServicePrincipal.Count -ne 1) { + throw "Expected one enterprise application with client ID '$ClientId'; found $($clientServicePrincipal.Count)." +} +$clientServicePrincipal = $clientServicePrincipal[0] + +$graphServicePrincipal = @( + Get-MgServicePrincipal ` + -Filter "appId eq '$graphAppId'" ` + -Property "id,appId,displayName,appRoles" +) +if ($graphServicePrincipal.Count -ne 1) { + throw "Expected one Microsoft Graph service principal; found $($graphServicePrincipal.Count)." +} +$graphServicePrincipal = $graphServicePrincipal[0] + +$resolvedRoles = foreach ($permission in $requiredPermissions) { + $matches = @( + $graphServicePrincipal.AppRoles | Where-Object { + $_.Value -eq $permission -and + $_.IsEnabled -and + $_.AllowedMemberTypes -contains "Application" + } + ) + + if ($matches.Count -ne 1) { + throw "Could not uniquely resolve the Microsoft Graph application permission '$permission'." + } + + $matches[0] +} + +$requiredResourceAccess = @( + foreach ($resource in $application.RequiredResourceAccess) { + if ($resource.ResourceAppId -ne $graphAppId) { + @{ + ResourceAppId = $resource.ResourceAppId + ResourceAccess = @( + foreach ($access in $resource.ResourceAccess) { + @{ Id = $access.Id; Type = $access.Type } + } + ) + } + } + } +) + +$existingGraphAccess = @( + $application.RequiredResourceAccess | + Where-Object ResourceAppId -eq $graphAppId | + ForEach-Object ResourceAccess +) +$graphAccessByKey = @{} +foreach ($access in $existingGraphAccess) { + $graphAccessByKey["$($access.Id):$($access.Type)"] = @{ + Id = $access.Id + Type = $access.Type + } +} +foreach ($role in $resolvedRoles) { + $graphAccessByKey["$($role.Id):Role"] = @{ + Id = $role.Id + Type = "Role" + } +} + +$requiredResourceAccess += @{ + ResourceAppId = $graphAppId + ResourceAccess = @($graphAccessByKey.Values) +} + +if ($PSCmdlet.ShouldProcess( + "$($application.DisplayName) ($ClientId)", + "Add requested Microsoft Graph application permissions" + )) { + Update-MgApplication ` + -ApplicationId $application.Id ` + -RequiredResourceAccess $requiredResourceAccess +} + +$existingAssignments = @( + Get-MgServicePrincipalAppRoleAssignment ` + -ServicePrincipalId $clientServicePrincipal.Id ` + -All +) +$existingRoleIds = @( + $existingAssignments | + Where-Object ResourceId -eq $graphServicePrincipal.Id | + ForEach-Object { "$($_.AppRoleId)" } +) + +foreach ($role in $resolvedRoles) { + if ("$($role.Id)" -in $existingRoleIds) { + Write-Host "Already granted: $($role.Value)" + continue + } + + if ($PSCmdlet.ShouldProcess( + "$($clientServicePrincipal.DisplayName) ($ClientId)", + "Grant admin consent for Microsoft Graph application permission $($role.Value)" + )) { + $body = @{ + PrincipalId = $clientServicePrincipal.Id + ResourceId = $graphServicePrincipal.Id + AppRoleId = $role.Id + } + + New-MgServicePrincipalAppRoleAssignment ` + -ServicePrincipalId $clientServicePrincipal.Id ` + -BodyParameter $body | Out-Null + Write-Host "Granted: $($role.Value)" + } +} + +$grantedRoleIds = @( + Get-MgServicePrincipalAppRoleAssignment ` + -ServicePrincipalId $clientServicePrincipal.Id ` + -All | + Where-Object ResourceId -eq $graphServicePrincipal.Id | + ForEach-Object { "$($_.AppRoleId)" } +) + +$results = foreach ($role in $resolvedRoles) { + [pscustomobject]@{ + Permission = $role.Value + Required = $true + Consented = ("$($role.Id)" -in $grantedRoleIds) + } +} + +$results | Sort-Object Permission | Format-Table -AutoSize + +if ($results.Consented -contains $false -and -not $WhatIfPreference) { + throw "One or more permissions were not granted." +} From 045ccecdb327ae7159eecdde3bfb1f9847949579 Mon Sep 17 00:00:00 2001 From: Pallavi Vemulapalli Date: Tue, 11 Aug 2026 13:16:49 -0500 Subject: [PATCH 2/3] Adding script to add graph claims to 3P App for Cross Tenant email sync to Dataverse --- .../Grant-SssGraphApplicationPermissions.ps1 | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 index b6bc33fd9..ba84ce9f9 100644 --- a/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 +++ b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 @@ -1,7 +1,7 @@ <# .SYNOPSIS Configures and grants consent for all Microsoft Graph application permissions -required by the cross-tenant SSS workload. +required for synching emails from Exchange online mailboxes into another Tenant's dataverse. .DESCRIPTION Adds the complete Graph application-permission set to an existing Microsoft @@ -13,15 +13,12 @@ Contacts.ReadWrite, MailboxSettings.ReadWrite, Tasks.ReadWrite.All, Chat.Read.All, OnlineMeetings.ReadWrite.All, CallRecords.Read.All, MailboxConfigItem.ReadWrite, and User.Read.All. There are no workload switches. -PREREQUISITES - -Before running the script: +PreRequisites: 1. Install Microsoft Graph PowerShell: Install-Module Microsoft.Graph -Scope CurrentUser -2. Sign in with an activated Privileged Role Administrator or Global +2. Sign in with an activated Global Administrator role. -3. Confirm that both the app registration and its enterprise application - already exist in the target tenant. +3. Confirm that the application is already created in the MSFT tenant. Use -WhatIf first to review the proposed changes. The script prompts for confirmation before changing the app registration and before granting each From 57b9f14559131a25b6597a619405781fe2226a21 Mon Sep 17 00:00:00 2001 From: Pallavi Vemulapalli Date: Tue, 11 Aug 2026 13:38:25 -0500 Subject: [PATCH 3/3] Adding script to add graph claims to 3P App for Cross Tenant email sync to Dataverse --- .../Grant-SssGraphApplicationPermissions.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 index ba84ce9f9..c3ad06ea3 100644 --- a/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 +++ b/powershell/ServerSideSync/Grant-SssGraphApplicationPermissions.ps1 @@ -22,7 +22,8 @@ PreRequisites: Use -WhatIf first to review the proposed changes. The script prompts for confirmation before changing the app registration and before granting each -permission. +permission. Every run writes a timestamped PowerShell transcript under the +same directory as this script. .PARAMETER TenantId The Microsoft Entra tenant ID containing the app registration and Exchange @@ -64,6 +65,17 @@ param( ) $ErrorActionPreference = "Stop" +$transcriptTimestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd-HHmmss") +$transcriptPath = Join-Path $PSScriptRoot ( + "Grant-SssGraphApplicationPermissions-{0}-{1}.log" -f + $transcriptTimestamp, + $PID +) + +Start-Transcript -Path $transcriptPath -IncludeInvocationHeader +Write-Host "Transcript: $transcriptPath" + +try { $graphAppId = "00000003-0000-0000-c000-000000000000" $requiredPermissions = @( "Mail.ReadWrite" @@ -261,3 +273,7 @@ $results | Sort-Object Permission | Format-Table -AutoSize if ($results.Consented -contains $false -and -not $WhatIfPreference) { throw "One or more permissions were not granted." } +} +finally { + Stop-Transcript +}