Wednesday, May 20, 2026

Grant Azure App Registration Access to Selected SharePoint Sites Using Sites.Selected

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

  1. Go to Azure PortalMicrosoft Entra IDApp Registrations
  2. Select your existing app or create a new one
  3. Navigate to API PermissionsAdd a permission
  4. Select Microsoft Graph
  5. Choose the permission type based on your scenario (see table below)
  6. Search for Sites.Selected and add it
  7. 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.Selected and not Sites.ReadWrite.All? Sites.ReadWrite.All grants the app access to every SharePoint site in your tenant — including sensitive sites. Sites.Selected scopes 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 ...


No comments:

Post a Comment

Featured Post

Grant Azure App Registration Access to Selected SharePoint Sites Using Sites.Selected

Grant Azure App Registration Access to Selected SharePoint Sites Using Sites.Selected Microsoft 365 | SharePoint Online | PnP PowerShell | ...

Popular posts