Grant Azure App Registration Access to Selected SharePoint Sites Using Sites.Selected
Microsoft 365 | SharePoint Online | PnP PowerShell | Microsoft Graph
Introduction
By default, when you grant an Azure App Registration access to SharePoint via Microsoft Graph, it gets access to every site in your tenant. That is a significant over-privilege for most automation scenarios where you only need to interact with one or a few specific sites.
Microsoft introduced the Sites.Selected permission model to solve this exact problem. It is a two-step approach: you grant the app a limited placeholder permission in Entra ID, then you explicitly assign it access to only the sites you choose. The app gets zero access until you complete both steps.
This article covers three approaches to implement Sites.Selected:
| Approach | Script | Best For |
|---|---|---|
| Option 1 | PnP PowerShell | Easiest — dedicated cmdlets, recommended |
| Option 2 | Microsoft Graph REST API via PowerShell | No module dependency — pure REST calls |
| Option 3 | Microsoft Graph PowerShell SDK | Official Microsoft SDK approach |
How Sites.Selected Works
Step 1 — Azure Portal
App Registration → API Permissions → Sites.Selected → Admin Consent
(App has NO site access yet)
Step 2 — PowerShell (choose one option below)
Explicitly assign the app to specific sites with a role
(App can now only access those assigned sites)
| Permission | Scope |
|---|---|
Sites.ReadWrite.All |
❌ All sites in the tenant |
Sites.Selected |
✅ Only explicitly assigned sites |
Step 1 — Azure App Registration Setup
1.1 Create or open your App Registration
- Go to Azure Portal → Microsoft Entra ID → App Registrations
- Select your existing app or create a new one
- Navigate to API Permissions → Add a permission
- Select Microsoft Graph
- Choose the permission type based on your scenario (see table below)
- Search for
Sites.Selectedand add it - Click Grant admin consent for your tenant
1.2 Delegated vs Application — Which One to Select?
Sites.Selected exists under both permission types in the portal. Pick based on your scenario:
Microsoft Graph
├── Delegated permissions → Sites.Selected ✅
└── Application permissions → Sites.Selected ✅
| Scenario | Permission Type to Select |
|---|---|
| Background automation, daemon, scheduled job, no user login | Application permissions |
| Power Automate unattended / service account flow | Application permissions |
| App acting on behalf of a signed-in user | Delegated permissions |
| Interactive app / user-context required | Delegated permissions |
Key difference in Delegated mode: Access is the intersection of the app's permission AND the signed-in user's own SharePoint permissions. The app can never exceed what the signed-in user can already access. In Application mode, there is no user — the app accesses the site purely based on the explicit role assigned in Step 2.
Most automation scenarios (scripts, Power Automate, background services) → use Application permissions.
Why
Sites.Selectedand notSites.ReadWrite.All?Sites.ReadWrite.Allgrants the app access to every SharePoint site in your tenant — including sensitive sites.Sites.Selectedscopes access to only the sites you explicitly configure, following the principle of least privilege.
1.3 Note down these values
After app registration, note the following — you will need them in all three script options:
- Application (client) ID
- Directory (tenant) ID
- Client Secret or Certificate (for app-only auth)
Option 1 — PnP PowerShell (Recommended)
# =============================================================================
# SITES.SELECTED — Option 1: PnP PowerShell Script
# Purpose : Grant / View / Update / Revoke app permissions on selected sites
# Module : PnP.PowerShell
# Requires : SharePoint Admin or Global Admin account
# Ref : https://pnp.github.io/powershell/cmdlets/Grant-PnPEntraIDAppSitePermission.html
# =============================================================================
# ── CONFIGURATION — update these values before running ───────────────────────
$adminUrl = "https://<your-tenant>-admin.sharepoint.com" # SharePoint Admin Center URL
$appClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App Registration Client ID
$appDisplayName = "MyAppName" # Display label (any name)
$permissionLevel = "Write" # Read | Write | Manage | FullControl
# Single site — used for grant / view / update / revoke operations
$singleSiteUrl = "https://<tenant>.sharepoint.com/sites/SiteName"
# Bulk grant — list of sites to grant access to
$bulkSites = @(
"https://<tenant>.sharepoint.com/sites/Site1",
"https://<tenant>.sharepoint.com/sites/Site2",
"https://<tenant>.sharepoint.com/sites/Site3"
)
# =============================================================================
# SECTION 1 — MODULE SETUP
# Pattern : Install if not present → always Import-Module to load into session
# =============================================================================
$moduleName = "PnP.PowerShell"
Write-Host "`n[Module] $moduleName" -ForegroundColor Cyan
# Step 1: Install if not already present
$installed = Get-Module -Name $moduleName -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $installed) {
Write-Host " Install : Not found — installing from PSGallery..." -ForegroundColor Yellow
try {
Install-Module -Name $moduleName `
-Scope CurrentUser `
-Force `
-AllowClobber `
-ErrorAction Stop
$installed = Get-Module -Name $moduleName -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
Write-Host " Install : Done — v$($installed.Version)" -ForegroundColor Green
}
catch {
Write-Host " Install : FAILED — $_" -ForegroundColor Red
exit 1
}
}
else {
Write-Host " Install : Already present — v$($installed.Version)" -ForegroundColor Green
}
# Step 2: Always import into the current session
# Import-Module is idempotent — safe to call even if already loaded
try {
Import-Module -Name $moduleName -Force -ErrorAction Stop
Write-Host " Import : Loaded into session" -ForegroundColor Green
}
catch {
Write-Host " Import : FAILED — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 2 — CONNECT
# =============================================================================
Write-Host "`n[Connect] Connecting to SharePoint Admin Center..." -ForegroundColor Cyan
try {
# Interactive login — supports MFA
# For app-only auth replace with: -ClientId <id> -CertificateThumbprint <thumb> -Tenant <tenant>
Connect-PnPOnline -Url $adminUrl -Interactive -ErrorAction Stop
Write-Host " Connected to: $adminUrl" -ForegroundColor Green
}
catch {
Write-Host " Connection FAILED — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 3 — GRANT: Single Site
# =============================================================================
Write-Host "`n[Grant] Single site permission..." -ForegroundColor Cyan
try {
# Grant permission to the target site
Grant-PnPEntraIDAppSitePermission `
-AppId $appClientId ` # App Client ID from Entra ID
-DisplayName $appDisplayName ` # Label only — does not need to match Entra app name
-Site $singleSiteUrl ` # Target site URL
-Permissions $permissionLevel # Read | Write | Manage | FullControl
Write-Host " Granted [$permissionLevel] on: $singleSiteUrl" -ForegroundColor Green
}
catch {
Write-Host " FAILED on: $singleSiteUrl — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 4 — GRANT: Bulk Sites
# =============================================================================
Write-Host "`n[Grant] Bulk site permissions..." -ForegroundColor Cyan
foreach ($siteUrl in $bulkSites) {
try {
Grant-PnPEntraIDAppSitePermission `
-AppId $appClientId `
-DisplayName $appDisplayName `
-Site $siteUrl `
-Permissions $permissionLevel
Write-Host " Granted [$permissionLevel] → $siteUrl" -ForegroundColor Green
}
catch {
Write-Host " FAILED → $siteUrl — $_" -ForegroundColor Red
}
}
# =============================================================================
# SECTION 5 — VIEW: List permissions on a site
# =============================================================================
Write-Host "`n[View] Listing permissions on: $singleSiteUrl" -ForegroundColor Cyan
try {
$perms = Get-PnPEntraIDAppSitePermission -Site $singleSiteUrl -ErrorAction Stop
$perms | Select-Object Id, Roles, GrantedToIdentities | Format-Table -AutoSize
}
catch {
Write-Host " FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 6 — UPDATE: Change permission level on an existing assignment
# =============================================================================
Write-Host "`n[Update] Updating permission level..." -ForegroundColor Cyan
# Auto-lookup PermissionId for this app on the target site
$targetPerm = Get-PnPEntraIDAppSitePermission -Site $singleSiteUrl |
Where-Object { $_.GrantedToIdentities.Application.Id -eq $appClientId } |
Select-Object -First 1
if ($targetPerm) {
try {
Set-PnPEntraIDAppSitePermission `
-Site $singleSiteUrl `
-PermissionId $targetPerm.Id ` # PermissionId retrieved above
-Permissions FullControl # New permission level
Write-Host " Updated to [FullControl] on: $singleSiteUrl" -ForegroundColor Green
}
catch {
Write-Host " Update FAILED — $_" -ForegroundColor Red
}
}
else {
Write-Host " No matching permission found for AppId: $appClientId" -ForegroundColor Yellow
}
# =============================================================================
# SECTION 7 — REVOKE: Remove app permission from a site
# =============================================================================
Write-Host "`n[Revoke] Revoking permission..." -ForegroundColor Cyan
# Auto-lookup PermissionId for this app on the target site
$revokePerm = Get-PnPEntraIDAppSitePermission -Site $singleSiteUrl |
Where-Object { $_.GrantedToIdentities.Application.Id -eq $appClientId } |
Select-Object -First 1
if ($revokePerm) {
try {
Revoke-PnPEntraIDAppSitePermission `
-Site $singleSiteUrl `
-PermissionId $revokePerm.Id ` # PermissionId to remove
-Force # Skip confirmation prompt
Write-Host " Revoked permission on: $singleSiteUrl" -ForegroundColor Green
}
catch {
Write-Host " Revoke FAILED — $_" -ForegroundColor Red
}
}
else {
Write-Host " No matching permission found to revoke for AppId: $appClientId" -ForegroundColor Yellow
}
# =============================================================================
Write-Host "`n[Done] PnP Sites.Selected script completed." -ForegroundColor Cyan
# =============================================================================
Option 2 — Microsoft Graph REST API via PowerShell
No module dependency. Uses Invoke-RestMethod directly against the Graph API.
Requires an access token — obtain it using client credentials (app secret or certificate).
# =============================================================================
# SITES.SELECTED — Option 2: Microsoft Graph REST API via PowerShell
# Purpose : Grant / View / Update / Revoke app permissions using raw Graph API
# Module : None — uses Invoke-RestMethod only
# Requires : App with Sites.FullControl.All (to manage permissions)
# Target app with Sites.Selected (the app being granted access)
# Ref : https://learn.microsoft.com/en-us/graph/api/site-post-permissions
# =============================================================================
# ── CONFIGURATION — update these values before running ───────────────────────
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Your Entra ID Tenant ID
$operatorAppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App used to call Graph (needs Sites.FullControl.All)
$operatorSecret = "your-client-secret-here" # Secret for the operator app above
$targetAppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App being granted Sites.Selected access
$targetAppName = "MyAppName" # Display label for the target app
$siteUrl = "https://<tenant>.sharepoint.com/sites/SiteName" # Target site URL
$permissionRole = "write" # read | write | manage | fullcontrol
# Bulk sites — used in Section 5
$bulkSites = @(
"https://<tenant>.sharepoint.com/sites/Site1",
"https://<tenant>.sharepoint.com/sites/Site2",
"https://<tenant>.sharepoint.com/sites/Site3"
)
# =============================================================================
# SECTION 1 — GET ACCESS TOKEN (Client Credentials flow)
# =============================================================================
Write-Host "`n[Token] Acquiring access token..." -ForegroundColor Cyan
try {
$tokenBody = @{
grant_type = "client_credentials"
client_id = $operatorAppId
client_secret = $operatorSecret
scope = "https://graph.microsoft.com/.default"
}
$tokenResponse = Invoke-RestMethod `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST `
-Body $tokenBody `
-ErrorAction Stop
$accessToken = $tokenResponse.access_token
$headers = @{
Authorization = "Bearer $accessToken"
"Content-Type" = "application/json"
}
Write-Host " Token acquired successfully" -ForegroundColor Green
}
catch {
Write-Host " Token request FAILED — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 2 — GET SITE ID from site URL
# =============================================================================
Write-Host "`n[Site] Resolving Site ID for: $siteUrl" -ForegroundColor Cyan
try {
# Parse hostname and path from the site URL
$uri = [System.Uri]$siteUrl
$hostname = $uri.Host # e.g. tenant.sharepoint.com
$sitePath = $uri.AbsolutePath # e.g. /sites/SiteName
$siteResponse = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/${hostname}:${sitePath}" `
-Headers $headers `
-Method GET `
-ErrorAction Stop
$siteId = $siteResponse.id
Write-Host " Site ID: $siteId" -ForegroundColor Green
}
catch {
Write-Host " Failed to resolve Site ID — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 3 — GRANT: Single Site
# =============================================================================
Write-Host "`n[Grant] Granting permission on: $siteUrl" -ForegroundColor Cyan
try {
$grantBody = @{
roles = @($permissionRole) # read | write | manage | fullcontrol
grantedToIdentities = @(
@{
application = @{
id = $targetAppId # App being granted access
displayName = $targetAppName # Label only
}
}
)
} | ConvertTo-Json -Depth 5
$grantResponse = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions" `
-Headers $headers `
-Method POST `
-Body $grantBody `
-ErrorAction Stop
Write-Host " Granted [$permissionRole] — Permission ID: $($grantResponse.id)" -ForegroundColor Green
}
catch {
Write-Host " Grant FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 4 — VIEW: List all permissions on a site
# =============================================================================
Write-Host "`n[View] Listing permissions on: $siteUrl" -ForegroundColor Cyan
try {
$listResponse = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions" `
-Headers $headers `
-Method GET `
-ErrorAction Stop
$listResponse.value | ForEach-Object {
Write-Host " ID : $($_.id)" -ForegroundColor White
Write-Host " Roles: $($_.roles -join ', ')" -ForegroundColor DarkCyan
Write-Host " App : $($_.grantedToIdentitiesV2.application.displayName)" -ForegroundColor DarkGray
Write-Host ""
}
}
catch {
Write-Host " View FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 5 — GRANT: Bulk Sites
# =============================================================================
Write-Host "`n[Grant] Bulk site permissions..." -ForegroundColor Cyan
foreach ($url in $bulkSites) {
try {
# Resolve Site ID for each site
$u = [System.Uri]$url
$host_ = $u.Host
$path_ = $u.AbsolutePath
$s = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/${host_}:${path_}" `
-Headers $headers `
-Method GET `
-ErrorAction Stop
$sid = $s.id
# Grant permission
$body_ = @{
roles = @($permissionRole)
grantedToIdentities = @(@{ application = @{ id = $targetAppId; displayName = $targetAppName } })
} | ConvertTo-Json -Depth 5
Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$sid/permissions" `
-Headers $headers `
-Method POST `
-Body $body_ `
-ErrorAction Stop | Out-Null
Write-Host " Granted [$permissionRole] → $url" -ForegroundColor Green
}
catch {
Write-Host " FAILED → $url — $_" -ForegroundColor Red
}
}
# =============================================================================
# SECTION 6 — UPDATE: Change permission level (requires Permission ID)
# =============================================================================
Write-Host "`n[Update] Updating permission..." -ForegroundColor Cyan
try {
# First, find the Permission ID for the target app
$existing = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions" `
-Headers $headers `
-Method GET `
-ErrorAction Stop
$permId = ($existing.value |
Where-Object { $_.grantedToIdentitiesV2.application.id -eq $targetAppId } |
Select-Object -First 1).id
if ($permId) {
$updateBody = @{ roles = @("fullcontrol") } | ConvertTo-Json # New permission level
Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions/$permId" `
-Headers $headers `
-Method PATCH `
-Body $updateBody `
-ErrorAction Stop | Out-Null
Write-Host " Updated to [fullcontrol] — Permission ID: $permId" -ForegroundColor Green
}
else {
Write-Host " No matching permission found for AppId: $targetAppId" -ForegroundColor Yellow
}
}
catch {
Write-Host " Update FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 7 — REVOKE: Delete a permission (requires Permission ID)
# =============================================================================
Write-Host "`n[Revoke] Revoking permission..." -ForegroundColor Cyan
try {
# Find the Permission ID for the target app
$existing = Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions" `
-Headers $headers `
-Method GET `
-ErrorAction Stop
$permId = ($existing.value |
Where-Object { $_.grantedToIdentitiesV2.application.id -eq $targetAppId } |
Select-Object -First 1).id
if ($permId) {
Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/sites/$siteId/permissions/$permId" `
-Headers $headers `
-Method DELETE `
-ErrorAction Stop | Out-Null
Write-Host " Revoked — Permission ID: $permId" -ForegroundColor Green
}
else {
Write-Host " No matching permission found for AppId: $targetAppId" -ForegroundColor Yellow
}
}
catch {
Write-Host " Revoke FAILED — $_" -ForegroundColor Red
}
# =============================================================================
Write-Host "`n[Done] Graph REST API Sites.Selected script completed." -ForegroundColor Cyan
# =============================================================================
Option 3 — Microsoft Graph PowerShell SDK
Official Microsoft SDK approach. Uses Microsoft.Graph module cmdlets instead of raw REST calls.
# =============================================================================
# SITES.SELECTED — Option 3: Microsoft Graph PowerShell SDK
# Purpose : Grant / View / Update / Revoke app permissions using Graph SDK cmdlets
# Module : Microsoft.Graph
# Requires : Sites.FullControl.All (to manage permissions)
# Target app with Sites.Selected (the app being granted access)
# Ref : https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.sites
# =============================================================================
# ── CONFIGURATION — update these values before running ───────────────────────
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Your Entra ID Tenant ID
$operatorAppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App used to connect (needs Sites.FullControl.All)
$certThumbprint = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # Certificate thumbprint for app-only auth
$targetAppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # App being granted Sites.Selected access
$targetAppName = "MyAppName" # Display label for the target app
$siteUrl = "https://<tenant>.sharepoint.com/sites/SiteName" # Target site URL
$permissionRole = "write" # read | write | manage | fullcontrol
# Bulk sites — used in Section 5
$bulkSites = @(
"https://<tenant>.sharepoint.com/sites/Site1",
"https://<tenant>.sharepoint.com/sites/Site2",
"https://<tenant>.sharepoint.com/sites/Site3"
)
# =============================================================================
# SECTION 1 — MODULE SETUP
# Pattern : Install if not present → always Import-Module to load into session
# =============================================================================
$moduleName = "Microsoft.Graph"
Write-Host "`n[Module] $moduleName" -ForegroundColor Cyan
# Step 1: Install if not already present
$installed = Get-Module -Name $moduleName -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $installed) {
Write-Host " Install : Not found — installing from PSGallery..." -ForegroundColor Yellow
try {
Install-Module -Name $moduleName `
-Scope CurrentUser `
-Force `
-AllowClobber `
-ErrorAction Stop
$installed = Get-Module -Name $moduleName -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1
Write-Host " Install : Done — v$($installed.Version)" -ForegroundColor Green
}
catch {
Write-Host " Install : FAILED — $_" -ForegroundColor Red
exit 1
}
}
else {
Write-Host " Install : Already present — v$($installed.Version)" -ForegroundColor Green
}
# Step 2: Always import into the current session
try {
Import-Module -Name $moduleName -Force -ErrorAction Stop
Write-Host " Import : Loaded into session" -ForegroundColor Green
}
catch {
Write-Host " Import : FAILED — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 2 — CONNECT (App-only with Certificate)
# =============================================================================
Write-Host "`n[Connect] Connecting to Microsoft Graph..." -ForegroundColor Cyan
try {
Connect-MgGraph `
-TenantId $tenantId `
-ClientId $operatorAppId `
-CertificateThumbprint $certThumbprint `
-NoWelcome `
-ErrorAction Stop
Write-Host " Connected to Microsoft Graph" -ForegroundColor Green
}
catch {
Write-Host " Connection FAILED — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 3 — GET SITE ID from site URL
# =============================================================================
Write-Host "`n[Site] Resolving Site ID for: $siteUrl" -ForegroundColor Cyan
try {
$uri = [System.Uri]$siteUrl
$hostname = $uri.Host
$sitePath = $uri.AbsolutePath
# Get-MgSite resolves site by hostname:path format
$site = Get-MgSite -SiteId "${hostname}:${sitePath}" -ErrorAction Stop
$siteId = $site.Id
Write-Host " Site ID: $siteId" -ForegroundColor Green
}
catch {
Write-Host " Failed to resolve Site ID — $_" -ForegroundColor Red
exit 1
}
# =============================================================================
# SECTION 4 — GRANT: Single Site
# =============================================================================
Write-Host "`n[Grant] Granting permission on: $siteUrl" -ForegroundColor Cyan
try {
$grantedIdentity = @{
Application = @{
Id = $targetAppId # App being granted access
DisplayName = $targetAppName # Label only
}
}
$newPerm = New-MgSitePermission `
-SiteId $siteId `
-Roles @($permissionRole) ` # read | write | manage | fullcontrol
-GrantedToIdentities @($grantedIdentity) `
-ErrorAction Stop
Write-Host " Granted [$permissionRole] — Permission ID: $($newPerm.Id)" -ForegroundColor Green
}
catch {
Write-Host " Grant FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 5 — GRANT: Bulk Sites
# =============================================================================
Write-Host "`n[Grant] Bulk site permissions..." -ForegroundColor Cyan
foreach ($url in $bulkSites) {
try {
$u = [System.Uri]$url
$s = Get-MgSite -SiteId "$($u.Host):$($u.AbsolutePath)" -ErrorAction Stop
$sid = $s.Id
New-MgSitePermission `
-SiteId $sid `
-Roles @($permissionRole) `
-GrantedToIdentities @(@{ Application = @{ Id = $targetAppId; DisplayName = $targetAppName } }) `
-ErrorAction Stop | Out-Null
Write-Host " Granted [$permissionRole] → $url" -ForegroundColor Green
}
catch {
Write-Host " FAILED → $url — $_" -ForegroundColor Red
}
}
# =============================================================================
# SECTION 6 — VIEW: List all permissions on a site
# =============================================================================
Write-Host "`n[View] Listing permissions on: $siteUrl" -ForegroundColor Cyan
try {
$perms = Get-MgSitePermission -SiteId $siteId -ErrorAction Stop
$perms | ForEach-Object {
Write-Host " ID : $($_.Id)" -ForegroundColor White
Write-Host " Roles: $($_.Roles -join ', ')" -ForegroundColor DarkCyan
Write-Host " App : $($_.GrantedToIdentitiesV2.Application.DisplayName)" -ForegroundColor DarkGray
Write-Host ""
}
}
catch {
Write-Host " View FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 7 — UPDATE: Change permission level on an existing assignment
# =============================================================================
Write-Host "`n[Update] Updating permission..." -ForegroundColor Cyan
try {
# Find the Permission ID for the target app
$perms = Get-MgSitePermission -SiteId $siteId -ErrorAction Stop
$permId = ($perms |
Where-Object { $_.GrantedToIdentitiesV2.Application.Id -eq $targetAppId } |
Select-Object -First 1).Id
if ($permId) {
Update-MgSitePermission `
-SiteId $siteId `
-PermissionId $permId `
-Roles @("fullcontrol") ` # New permission level
-ErrorAction Stop
Write-Host " Updated to [fullcontrol] — Permission ID: $permId" -ForegroundColor Green
}
else {
Write-Host " No matching permission found for AppId: $targetAppId" -ForegroundColor Yellow
}
}
catch {
Write-Host " Update FAILED — $_" -ForegroundColor Red
}
# =============================================================================
# SECTION 8 — REVOKE: Remove a permission
# =============================================================================
Write-Host "`n[Revoke] Revoking permission..." -ForegroundColor Cyan
try {
# Find the Permission ID for the target app
$perms = Get-MgSitePermission -SiteId $siteId -ErrorAction Stop
$permId = ($perms |
Where-Object { $_.GrantedToIdentitiesV2.Application.Id -eq $targetAppId } |
Select-Object -First 1).Id
if ($permId) {
Remove-MgSitePermission `
-SiteId $siteId `
-PermissionId $permId `
-ErrorAction Stop
Write-Host " Revoked — Permission ID: $permId" -ForegroundColor Green
}
else {
Write-Host " No matching permission found for AppId: $targetAppId" -ForegroundColor Yellow
}
}
catch {
Write-Host " Revoke FAILED — $_" -ForegroundColor Red
}
# =============================================================================
Write-Host "`n[Done] Graph SDK Sites.Selected script completed." -ForegroundColor Cyan
# =============================================================================
Approach Comparison
| Option 1 — PnP PowerShell | Option 2 — Graph REST API | Option 3 — Graph SDK | |
|---|---|---|---|
| Module required | PnP.PowerShell |
None | Microsoft.Graph |
| Cmdlet style | Dedicated SPO cmdlets | Raw Invoke-RestMethod |
Graph SDK cmdlets |
| Auth support | Interactive + App-only | App-only (token) | Interactive + App-only |
| Complexity | Low | Medium | Medium |
| Best for | Day-to-day admin tasks | No-module / CI-CD pipelines | Microsoft-native SDK preference |
Required Admin Roles
| Action | Minimum Role Required |
|---|---|
| Add API permission in Entra ID | Global Administrator |
| Grant admin consent | Global Administrator |
Run Grant-PnPEntraIDAppSitePermission (Option 1) |
SharePoint Administrator or Global Administrator |
Call Graph /sites/{id}/permissions (Option 2 & 3) |
App or delegated token with Sites.FullControl.All |
Quick Summary
Step 1 — Azure Portal (all options)
App Registration → API Permissions
→ Microsoft Graph → Sites.Selected → Admin Consent
Step 2 — Choose one option:
Option 1 — PnP PowerShell (Recommended)
Module : PnP.PowerShell
Grant : Grant-PnPEntraIDAppSitePermission -AppId ... -Site ... -Permissions Write
View : Get-PnPEntraIDAppSitePermission -Site ...
Update : Set-PnPEntraIDAppSitePermission -Site ... -PermissionId ... -Permissions FullControl
Revoke : Revoke-PnPEntraIDAppSitePermission -Site ... -PermissionId ...
Option 2 — Graph REST API (No module)
Grant : POST /v1.0/sites/{id}/permissions
View : GET /v1.0/sites/{id}/permissions
Update : PATCH /v1.0/sites/{id}/permissions/{permissionId}
Revoke : DELETE /v1.0/sites/{id}/permissions/{permissionId}
Option 3 — Graph SDK
Module : Microsoft.Graph
Grant : New-MgSitePermission -SiteId ... -Roles ... -GrantedToIdentities ...
View : Get-MgSitePermission -SiteId ...
Update : Update-MgSitePermission -SiteId ... -PermissionId ... -Roles ...
Revoke : Remove-MgSitePermission -SiteId ... -PermissionId ...