How to limit application access to emails and calendars with custom application scope for Exchange Online RBAC provider

Why to use the custom application scope

First of all, what is the use case where the custom application scope can be useful?

You have an Entra ID application that accesses users emails or calendars without signed-in user, but you need to limit the access to a subset of users. The subset of users is defined by a filter based on user properties like department, city, country, jobTitle, displayName, givenName, surname, or userPrincipalName.

How to achieve this? The Exchange Online RBAC provider and custom application scope is the answer. Both is accessible and manageable via the Microsoft Graph API.

Exchange Online RBAC provider

The Exchange Online RBAC provider allows you to create a role assignment for a service principal to access users emails or calendars. The scope for the role assignment can be

  • users in the whole tenant
  • users in the group
  • users in the administrative unit
  • specific user
  • users based a filter via the custom application scope

The custom application scope gives you more flexibility to limit the access based on a filter query that defines how you segment your recipients that the app can manage.

In short, you can limit the access to the users based on their properties like job title, department, company, given name, surname, etc.

What roles for the Exchange Online RBAC provider allow an app to access users emails or calendars? Those roles for which the role permissions contain the resource action Mail.Read, Mail.ReadBasic, Mail.ReadWrite, Mail.Send, MailboxSettings.Read, MailboxSettings.ReadWrite, Calendars.Read, or Calendars.ReadWrite.

If you are familiar with the Microsoft Graph API, resource actions may sound familiar. The names are the same as the Microsoft Graph API permissions.

To get the roles that allow an app to access users emails or calendars, you can use the following query:

GET /beta/roleManagement/exchange/roleDefinitions?$filter=rolePermissions/any(r:r/allowedResourceActions/any(a:startswith(a,'Mail') or startswith(a,'Calendars')))

Or the Graph PowerShell SDK cmdlet:

Get-MgBetaRoleManagementExchangeRoleDefinition -Filter "rolePermissions/any(r:r/allowedResourceActions/any(a:startswith(a,'Mail') or startswith(a,'Calendars')))" 

The table below shows the roles returned by the query:

Id Role name Description
1f704712-7d46-481f-b2cd-dbcc978c4f2a Application Mail.Read Allows the app to read mail in all mailboxes without a signed-in user.
3eca55c8-0e73-4c12-81bf-526549f2e5a3 Application Mail.ReadBasic Allows the app to read email except the body, previewBody, attachments, and any extended properties in all mailboxes without a signed-in user
82fd214e-61ca-4dc7-98f6-090700bdb205 Application Mail.ReadWrite Allows the app to create, read, update, and delete email in all mailboxes without a signed-in user. Does not include permission to send mail
8679f4ff-c91d-40d0-809c-c86d114821a5 Application Mail.Send Allows the app to send mail as any user without a signed-in user
c40299e0-2107-455f-85dd-6e8862c3a0cc Application MailboxSettings.Read Allows the app to read user's mailbox settings in all mailboxes without a signed-in user
459cb245-07c5-44f1-8133-3da40b4b6197 Application MailboxSettings.ReadWrite Allows the app to create, read, update, and delete user's mailbox settings in all mailboxes without a signed-in user
a3123d4e-4256-4ad0-bef0-205a00807fae Application Calendars.Read Allows the app to read events of all calendars without a signed-in user
b92761c0-5311-4908-92ca-2c1f8c71aa1c Application Calendars.ReadWrite Allows the app to create, read, update, and delete events of all calendars without a signed-in user
b49ae303-7a8f-4ba1-aa37-27b40461aabb Application Mail Full Access Allows the app to create, read, update, and delete email in all mailboxes as well as send mail as any user without a signed-in user
48d6a78c-0681-4d73-acec-9f9ffad56ddb Application Exchange Full Access Without a signed-in user: Allows the app to create, read, update, and delete email in all mailboxes as well as send mail as any user. Allows the app to create, read, update, and delete user's mailbox settings, events and contacts in all mailboxes

Custom application scope

Let's take a look at how to create a custom application scope. When creating a custom application scope, you need to specify a name, whether the scope is exclusive and a filter for recipients.

If the scope is exclusive, users who match the filter query of the exclusive scope cannot be managed by other applications that have a role assignment with non-exclusive scope.

Exclusive scope is not currently supported for role assignments by the Graph API.

POST /beta/roleManagement/exchange/customAppScopes
{
  "type": "RecipientScope",
  "displayName": "Users from Marketing Department",
  "customAttributes": {
    "Exclusive": false,
    "RecipientFilter": "Department -eq 'Marketing'"
  }
}

Or when using the Graph PowerShell SDK (Microsoft.Graph.Beta module):

$params = @{
  type = "RecipientScope"
  displayName = "Users from Marketing Department"
  customAttributes = @{
    Exclusive = $false
    RecipientFilter = "Department -eq 'Marketing'"
  }
}

New-MgBetaRoleManagementExchangeCustomAppScope -BodyParameter $params

Example

Let's say you want to create two Entra ID applications that will have access to users emails.

  • The first application should have access to the emails of all users from the marketing department
  • The second application should have access to the emails of all managers

The steps are:

  • Register Entra ID applications with the application permission User.Read.All (needed to query users from the marketing department and all managers) and create a client secret
  • Create custom application scopes for users from the marketing department and managers
  • Create role assignments for each Entra ID application with the custom application scope

We can create PowerShell functions to register an application and service principal, create custom application scope and role assignment.

Create application and service principal

# Create application and service principal
function Add-EntraApplicationAndServicePrincipal {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$DisplayName,
        [Parameter(Mandatory)]
        [string]$ResourceId,
        [Parameter(Mandatory)]
        [string]$AppRoleId
    )

    $paramsApp = @{
        displayName = $DisplayName
        passwordCredentials = @(
            @{
                displayName = "default"
            }
        )
    }

    # create application with client secret
    $app = New-MgApplication -BodyParameter $paramsApp
    # save client secret to the file
    $app.PasswordCredentials[0].SecretText | Out-File -FilePath "$($DisplayName)_secret.txt"

    # create service principal
    $paramsSp = @{
        appId = $app.AppId
    }

    $servicePrincipal = New-MgServicePrincipal -BodyParameter $paramsSp

    # wait until the service principal is provisioned
    Start-Sleep -Seconds 30

    # add application permission
    $params = @{
        principalId = $servicePrincipal.Id
        resourceId = $ResourceId
        appRoleId = $AppRoleId
    }

    $appRoleAssignment = New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ResourceId -BodyParameter $params

    return $servicePrincipal
}

Get Exchange role definition

function Get-ExchangeRoleDefinition {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$RoleDisplayName
    )

   return Get-MgBetaRoleManagementExchangeRoleDefinition -Filter "displayName eq '$RoleDisplayName'"
}

Create custom application scope

function Add-ExchangeCustomApplicationScope {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ScopeDisplayName,
        [Parameter(Mandatory)]
        [bool]$Exclusive,
        [Parameter(Mandatory)]
        [string]$RecipientFilter
    )

    $params = @{
        type = "RecipientScope"
        displayName = $ScopeDisplayName
        customAttributes = @{
            Exclusive = $Exclusive
            RecipientFilter = $RecipientFilter
        }
    }

   return New-MgBetaRoleManagementExchangeCustomAppScope -BodyParameter $params
}

Create role assignment with custom application scope

function Add-ExchangeRoleAssignmentWithCustomAppScope {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ServicePrincipalId,
        [Parameter(Mandatory)]
        [string]$RoleDefinitionId,
        [Parameter(Mandatory)]
        [string]$AppScopeId
    )

    $params = @{
        principalId = "/ServicePrincipals/$ServicePrincipalId"
        roleDefinitionId = $RoleDefinitionId
        directoryScopeId = $null
        appScopeId = $AppScopeId
    }

    New-MgBetaRoleManagementExchangeRoleAssignment -BodyParameter $params
}

Now, the main part:

# Connect as admin
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Exchange", "Application.ReadWrite.All"

Import-Module Microsoft.Graph.Applications
Import-Module Microsoft.Graph.Identity.Governance

$resource = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -Property "id,appRoles" 
$appRole = $resource.AppRoles | Where-Object { $_.Value -eq 'User.Read.All' } | Select-Object -First 1

# Get role definition for reading emails
$roleDefinition = Get-ExchangeRoleDefinition -RoleDisplayName 'Application Mail.Read'
$roleDefinitionId = $roleDefinition.Id

# First app, scope limited to users from the marketing department
$servicePrincipal = Add-EntraApplicationAndServicePrincipal -DisplayName 'MailsReaderApp1' -ResourceId $resource.Id -AppRoleId $appRole.Id
$servicePrincipalId = $servicePrincipal.Id
$appScope = Add-ExchangeCustomApplicationScope -ScopeDisplayName 'Users from Marketing Department' -Exclusive $false -RecipientFilter "Department -eq 'Marketing'"
Add-ExchangeRoleAssignmentWithCustomAppScope -ServicePrincipalId $servicePrincipalId -RoleDefinitionId "$($roleDefinition.Id)" -AppScopeId "$($appScope.Id)"

# Second app, scope limited to managers
$servicePrincipal = Add-EntraApplicationAndServicePrincipal -DisplayName 'MailsReaderApp2' -ResourceId $resource.Id -AppRoleId $appRole.Id
$servicePrincipalId = $servicePrincipal.Id
$appScope = Add-ExchangeCustomApplicationScope -ScopeDisplayName 'Managers' -Exclusive $false -RecipientFilter "Title -like '*Manager*'"
Add-ExchangeRoleAssignmentWithCustomAppScope -ServicePrincipalId $servicePrincipalId -RoleDefinitionId "$($roleDefinition.Id)" -AppScopeId "$($appScope.Id)"

Disconnect-MgGraph

Let's test the reading emails for user from the marketing department. Connect with the client secret, filter users from the department and read their mails.

$clientId = '<app1_client_id>'
$clientSecret='<app1_client_secret>'
$clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientId, (ConvertTo-SecureString -String $clientSecret -AsPlainText -Force)
$tenantId = '<tenant_id>'

# connect to Microsoft Graph API
Connect-MgGraph -ClientSecretCredential $clientSecretCredential -TenantId $tenantId

$users = Get-MgUser -Filter "Department eq 'Marketing'" -Property 'id,displayName,department'

$users | ForEach-Object {
    Write-Host "Messages for user $($_.DisplayName) from $($_.Department)"
    $messages = Get-MgUserMessage -UserId $_.Id -Property "id,subject"
    $messages | ForEach-Object {
        Write-Host "- $($_.Subject)"
    }
}

Disconnect-MgGraph

The result:

If you try to call Get-MgUserMessage for a user who is not from the marketing department, you will receive an error Access is denied. Check credentials and try again..

Be aware that this is the beta version of the Graph API, so it may sometimes fail unexpectedly.

The script for reading managers emails is very similar:

$clientId = '<app2_client_id>'
$clientSecret='<app2_client_secret>'
$clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientId, (ConvertTo-SecureString -String $clientSecret -AsPlainText -Force)
$tenantId = '<tenant_id>'

# connect to Microsoft Graph API
Connect-MgGraph -ClientSecretCredential $clientSecretCredential -TenantId $tenantId

$users = Get-MgUser -Property 'id,displayName,jobTitle' | Where-Object { $_.JobTitle -like '*manager*' }

$users | ForEach-Object {
    Write-Host "Messages for user $($_.DisplayName) with job title $($_.JobTitle)"
    $messages = Get-MgUserMessage -UserId $_.Id -Property "id,subject"
    $messages | ForEach-Object {
        Write-Host "- $($_.Subject)"
    }
}

Disconnect-MgGraph

The result:

The /users endpoint doesn't support the contains operator for filtering users by job title, so you need to get all users and filter them in the script.

Conclusion

The Exchange Online RBAC provider and custom application scope give you more flexibility to limit the access to users emails or calendars based on the user properties. The custom application scope allows you to define a filter query that defines how you segment your recipients that the app can manage.

1
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x