<# .SYNOPSIS Grants, revokes or lists Cloudinary MediaFlows' read access to a single SharePoint site. .DESCRIPTION MediaFlows holds one Microsoft Graph application permission, Sites.Selected, which by itself grants access to no site at all. This script adds an explicit read grant for one site collection. Every other site in the tenant stays inaccessible to MediaFlows. Uses the first-party Microsoft.Graph PowerShell SDK. Nothing is registered in your tenant to run it, and it creates no application of its own. .PARAMETER SiteUrl Full URL of the site, e.g. https://contoso.sharepoint.com/sites/marketing .PARAMETER ApplicationId The MediaFlows application ID, shown read-only on the MediaFlows connection form. .PARAMETER Revoke Remove MediaFlows' access to this site instead of granting it. .PARAMETER List Show which applications currently have access to this site, and change nothing. .EXAMPLE .\Grant-MediaFlowsSiteAccess.ps1 -SiteUrl https://contoso.sharepoint.com/sites/marketing -ApplicationId .EXAMPLE .\Grant-MediaFlowsSiteAccess.ps1 -SiteUrl https://contoso.sharepoint.com/sites/marketing -ApplicationId -Revoke .NOTES Requires SharePoint Administrator or Global Administrator. The Sites.FullControl.All scope requested at sign-in is DELEGATED: it acts only as you, only for the lifetime of this session, and grants MediaFlows nothing. Microsoft requires that scope to manage another application's site permissions; there is no narrower one. Prefer consenting for yourself rather than on behalf of the organisation — see ONBOARDING.md. #> [CmdletBinding(DefaultParameterSetName = 'Grant')] param( [Parameter(Mandatory, Position = 0)] [string]$SiteUrl, [Parameter(Mandatory)] [string]$ApplicationId, [Parameter(ParameterSetName = 'Revoke')] [switch]$Revoke, [Parameter(ParameterSetName = 'List')] [switch]$List, [string]$ApplicationName = 'Cloudinary MediaFlows' ) $ErrorActionPreference = 'Stop' function Resolve-GraphSiteId { param([string]$Url) $uri = [System.Uri]$Url $path = $uri.AbsolutePath.TrimEnd('/') if ([string]::IsNullOrWhiteSpace($path)) { return (Get-MgSite -SiteId "$($uri.Host):/").Id } return (Get-MgSite -SiteId "$($uri.Host):$($path)").Id } if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Sites)) { Write-Host 'Installing Microsoft.Graph.Sites (current user only)...' -ForegroundColor Cyan Install-Module Microsoft.Graph.Sites -Scope CurrentUser -Force -AllowClobber } Import-Module Microsoft.Graph.Sites Write-Host 'Signing you in...' -ForegroundColor Cyan Connect-MgGraph -Scopes 'Sites.FullControl.All' -NoWelcome try { $siteId = Resolve-GraphSiteId -Url $SiteUrl Write-Host "Site: $SiteUrl" -ForegroundColor Gray Write-Host " id: $siteId" -ForegroundColor DarkGray # Graph offers no tenant-wide "which sites is this app granted" query for Sites.Selected, so this # is per-site by necessity, not by choice. Auditing every grant means running it per site. if ($List) { # -All, not the first page: an audit that silently stops at the page boundary reports a site # as having fewer applications on it than it does. $perms = Get-MgSitePermission -SiteId $siteId -All if (-not $perms) { Write-Host 'No applications have been granted access to this site.' -ForegroundColor Green } else { Write-Host "`nApplications with access to this site:" -ForegroundColor Yellow foreach ($p in $perms) { $app = $p.GrantedToIdentitiesV2.Application '{0,-40} {1,-10} (permission id {2})' -f $app.DisplayName, ($p.Roles -join ','), $p.Id } } return } # -All is load-bearing on both paths below. Without it the search stops at the first page, so a # grant sitting on a later one reads as absent: -Revoke then reports "nothing to revoke" while # leaving MediaFlows able to read the site, and the grant path adds a second permission entry # beside the one it could not see. $existing = Get-MgSitePermission -SiteId $siteId -All | Where-Object { $_.GrantedToIdentitiesV2.Application.Id -eq $ApplicationId } if ($Revoke) { if (-not $existing) { Write-Host "$ApplicationName has no access to this site. Nothing to revoke." -ForegroundColor Green return } foreach ($p in $existing) { Remove-MgSitePermission -SiteId $siteId -PermissionId $p.Id Write-Host "Revoked $($p.Roles -join ',') from $ApplicationName." -ForegroundColor Green } Write-Host 'MediaFlows can no longer read this site.' -ForegroundColor Green return } if ($existing) { $roles = @($existing.Roles | ForEach-Object { $_ }) # MediaFlows only ever needs read, and that is what the documentation promises. A grant that # carries more than that is what an administrator runs this script to find, so saying # "no change made" and exiting clean would answer the opposite of the question being asked. $broader = @($roles | Where-Object { $_ -ne 'read' }) if ($broader.Count -gt 0) { Write-Host "$ApplicationName holds '$($roles -join ',')' on this site, which is broader than the read access MediaFlows needs." -ForegroundColor Red Write-Host "To narrow it: rerun with -Revoke, then rerun without it to grant read only." -ForegroundColor Red return } Write-Host "$ApplicationName already has '$($roles -join ',')' on this site. No change made." -ForegroundColor Yellow return } $body = @{ roles = @('read') grantedToIdentities = @( @{ application = @{ id = $ApplicationId; displayName = $ApplicationName } } ) } $result = New-MgSitePermission -SiteId $siteId -BodyParameter $body Write-Host '' Write-Host 'Granted.' -ForegroundColor Green Write-Host " application : $ApplicationName ($ApplicationId)" Write-Host " site : $SiteUrl" Write-Host " access : $($result.Roles -join ',') (read-only)" Write-Host " permission : $($result.Id)" Write-Host '' Write-Host 'MediaFlows can now read this site, and no other site in your tenant.' -ForegroundColor Gray Write-Host 'To undo: re-run with -Revoke' -ForegroundColor Gray } finally { # Runs on a failure too, so a resolve error does not leave a Graph session open in the shell. Disconnect-MgGraph | Out-Null }