DEV Community

Cover image for Use Microsoft Graph to Set Granular Permissions to SharePoint Online Sites for Azure AD Application
SRINIVAS VARUKALA
SRINIVAS VARUKALA

Posted on • Edited on

3 1

Use Microsoft Graph to Set Granular Permissions to SharePoint Online Sites for Azure AD Application

I recently put out a Twitter post about the upcoming capability
that allows you to apply granular permissions to SharePoint Online Sites for Azure AD Applications.

This capability is now live and available in the Microsoft Graph API (both v1.0 and Beta). Find the API reference on Microsoft Graph documentation. I have seen a lot of our customers rely on the legacy 'Access Control Services' (ACS) based apps (apps registered using /_layouts/15/appregnew.aspx page) to meet the site level/granular permissions requirements. With this update you can now stop doing that and embrace the modern way using Microsoft Graph.

In this blog I will show how this is accomplished using PowerShell and Microsoft Graph REST API calls.

The current implementation feels like a MVP('minimal viable product') of a long term solid plan from Microsoft. Therefore, at the moment there is no UI/UX for admins to manage (add/remove permissions at) the individual site level permissions. It's only possible through Microsoft Graph API. You will need two apps to accomplish this.

First is the Azure AD application (let's call it 'client-app') registered that needs to be given app-only permissions to select few sites (as against to all sites in the tenant). First step is to navigate to the "API Permissions" for that app. Select "Application permissions" box. Then select "Sites.Selected" permission scope listed under "Sites" category.
image

You need another Azure AD application (let's call it 'admin-app') that has 'Sites.FullControl.All' app-only permissions. Assume this is created and managed by your IT Admins. IT admin will use this application (as a helper utility) to call Microsoft Graph and assign 'client-app' the permissions (read or write) for each site that it needs access. In this way the 'client-app' will be able to make queries only to those set of sites. Microsoft graph is updated to support this capability. To add/remove permissions at site collection level, you need to use the /sites/{site-id}/permissions REST API.
Here is an example:

https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,ab37ac98-a777-4444-b90f-20e2a8728caf,faaf89c8-4444-4639-978f-39e07847b61a/permissions

Here is a sample PowerShell script to enumerate existing site level permissions using Microsoft Graph

clear
#Provie tenant prefix, Application (client) ID, and client secret of the IT admin app
#IT admin app must have sites.fullcontrol app-only perms
$tenantPrefix = "Contoso";
$clientId = "Client-ID";
$clientSecret = "Client-Secret";
$tenantName = $tenantPrefix +".onmicrosoft.com";
$tenantDomain = $tenantPrefix +".sharepoint.com";
#Provide site url
$sitePath = "https://contoso.sharepoint.com/sites/Web01"
$siteName = $sitePath.Split("/")[4]
$ReqTokenBody = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $clientID
Client_Secret = $clientSecret
}
$TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $tenantDomain +':/sites/'+ $siteName +'?$select=id,displayName'
try {
$spoResult = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get
Write-Host "Site:" $spoResult.displayName
}
catch {
Write-Output "Failed to enumerate the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
Exit
}
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $spoResult.id +'/permissions'
try {
$spoData = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get -ResponseHeadersVariable spoRespHeaders
if ($spoData.value.length -eq 0)
{
Write-Host "No site level permissions found"
}
else {
$spoData.value | %{ $_ | ConvertTo-Json -Depth 10 }
}
}
catch {
Write-Output "Failed to add permissions the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
}

NOTE: When we enumerate the granular permissions, its not showing the role (read/write) details. It's showing the unique perm id, display name and client app id. This might be a temporary gap in the functionality.

Here is a script sample to add granular permissions to a site using Microsoft Graph:

clear
# Provide tenant prefix, Application (client) ID, and Client secret of the admin app
$tenantPrefix = "contoso";
$clientId = "client-id";
$clientSecret = "client-secret";
$tenantName = $tenantPrefix +".onmicrosoft.com";
$tenantDomain = $tenantPrefix +".sharepoint.com";
#Provide the site url
$sitePath = "https://contoso.sharepoint.com/sites/Web01"
$clientAppId = "client-id-with-sites.selected-perms"
$clientAppName = "RestrictedApp"
$roles = @("read") #read, write
$rolesJson = ConvertTo-Json $roles
$siteName = $sitePath.Split("/")[4]
$resource = "https://graph.microsoft.com/"
$ReqTokenBody = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $clientID
Client_Secret = $clientSecret
}
$TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $tenantDomain +':/sites/'+ $siteName +'?$select=id,displayName'
try {
$spoResult = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get
Write-Host "Site: " $spoResult.displayName
}
catch {
Write-Output "Failed to enumerate the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
#Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
Exit
}
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $spoResult.id +'/permissions'
$postBody = @"
{
'roles': $rolesJson,
'grantedToIdentities': [{
'application': {
'id': '$clientAppId',
'displayName': '$clientAppName'
}
}]
}
"@
#Here is an example
<#
$postBody = @'
{
'roles': ['write'],
'grantedToIdentities': [{
'application': {
'id': '3842218f-b211-4444-90f9-ef790e46cf75',
'displayName': 'Client App'
}
}]
}
'@
#>
try {
$spoData = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Post -Body $postBody -ContentType "application/json" #-ResponseHeadersVariable spoRespHeaders
"Successfully added the granular permissions"
$spoData | ConvertTo-Json -Depth 10
}
catch {
Write-Output "Failed to add permissions the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
#Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
}

Here is a script to remove all granular permissions or selected granular permissions based on the app id:

clear
#Provie tenant prefix, Application (client) ID, and client secret of the IT admin app
#IT admin app must have sites.fullcontrol app-only perms
$tenantPrefix = "Contoso";
$clientId = "Client-Id";
$clientSecret = "Client-Secret";
$tenantName = $tenantPrefix +".onmicrosoft.com";
$tenantDomain = $tenantPrefix +".sharepoint.com";
#Site url
$sitePath = "https://contoso.sharepoint.com/sites/Web01"
#Leave this empty to delete all granular perms or provide specific app id
$clientAppId = "" #Example: "986f9573-cfcc-4444-b86a-99f9997c3edc"
$siteName = $sitePath.Split("/")[4]
$resource = "https://graph.microsoft.com/"
$ReqTokenBody = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $clientID
Client_Secret = $clientSecret
}
$TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $tenantDomain +':/sites/'+ $siteName +'?$select=id,displayName'
try {
$spoResult = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get
Write-Host "Site:" $spoResult.displayName
}
catch {
Write-Output "Failed to enumerate the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
Exit
}
$baseApiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $spoResult.id +'/permissions/'
try {
$spoData = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $baseApiUrl -Method Get
if ($spoData.value.length -eq 0)
{
Write-Host "No site level permissions found"
}
else {
$spoData.value | %{
if (($clientAppId.Trim().Length -ne 0) -and ($clientAppId -eq $_.grantedToIdentities.application.id))
{
#Delete only the requested app perm
$apiUrl = $baseApiUrl + $_.id
Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Delete
Write-Host "Deleted permission id: " $_.id
}
elseif($clientAppId.Trim().Length -eq 0)
{
#Delete all perms
$apiUrl = $baseApiUrl + $_.id
Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Delete
Write-Host "Deleted permission id: " $_.id
}
}
}
}
catch {
Write-Output "Failed to add permissions the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
}

I created a sample script to finally test this out. This sample script enumerates all the lists in a given SPO site. Below image shows the script failing before giving the granular permissions:
image

Now I ran the Add-SPOSiteGranularPermission script giving my client-app 'read' permissions to the site. Here is the output from the script:
image

Below image shows the same script now successful after giving the granular permissions on the site:
image

Here is the sample script that I used for my sample run:

clear
# Application (client) ID, secret, tenant name and site
$tenantPrefix = "CONTOSO"; #Pass 'Contoso' for contoso.onmicrosoft.com
$clientId = "CLIENT ID"; #Pass the azure ad app id here
$clientSecret = "CLIENT SECRET"; #Pass the azure ad app client secret
$tenantName = $tenantPrefix +".onmicrosoft.com";
$tenantDomain = $tenantPrefix +".sharepoint.com";
$sitePath = "https://contoso.sharepoint.com/sites/Web01"
$siteName = $sitePath.Split("/")[4]
$resource = "https://graph.microsoft.com/"
$ReqTokenBody = @{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $clientID
Client_Secret = $clientSecret
}
$TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody
$AccessToken = $TokenResponse.access_token
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $tenantDomain +':/sites/'+ $siteName +'?$select=id,displayName'
try {
$spoResult = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get
Write-Host "Site: " $spoResult.displayName
}
catch {
Write-Output "Failed to enumerate the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
#Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
Exit
}
$apiUrl = 'https://graph.microsoft.com/v1.0/sites/'+ $spoResult.id +'/lists?$select=displayName'
try {
$spoData = Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Method Get -ContentType "text/plain" -ResponseHeadersVariable spoRespHeaders
$spoData.Value | FT
}
catch {
Write-Output "Failed to add permissions the site"
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
}

Some of the limitations I see in the current state:

  1. Thie granular permissions is limited to application permissions (app-only) for an Azure AD app.
  2. There is no UI/UX to manage the granular permissions
  3. There is currently no endpoint to list out sites that has granular permissions enabled for a given AAD app
  4. Site collection level seems to be the most granular it can go.

Jeremy Kelley (Microsoft PM) presented on this topic in February's Microsoft Graph community call. He answered a bunch of questions and also hinted that this capability is just beginning and it has a long way to go. You can access the recording here.

Finally I am working on a full blown script to manage the site level permissions in bulk. You can retrieve, add, remove permissions by supplying a CSV file as input. I will update the link here once that is ready.

Please leave any comments, questions or feedback.

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (2)

Collapse
 
kkazala profile image
Kinga

Hi,
Is $roles = @("sp.full control") working for you? I'm getting
"Failed to add permissions the site
StatusCode: 400"

thx for great article!

Collapse
 
svarukala profile image
SRINIVAS VARUKALA

Sorry for late response. I guess I should register for alerts on comments. I am also getting similar error.
Based on what I gather setting sp.full controll is still not supported. PG will eventually get there. Not sure about the timeline.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay