Public/Grant-NSPGraphSitePermission.ps1

function Grant-NSPGraphSitePermission {
    <#
    .SYNOPSIS
        Gives an app with Sites.Selected access to one SharePoint site. Plan-only by default.
    .DESCRIPTION
        Sites.Selected consents an app to no site at all until a site permission names it. This
        adds that permission for -ClientId on -SiteId with -Role (write by default; read for
        read-only jobs). Without -Execute it reports the plan; with -Execute and ShouldProcess
        approval it creates the permission, or upgrades a weaker existing one. An existing equal
        or stronger role is reported as AlreadyGranted and left alone. Other apps' permissions are
        never touched.

        Uses the active connection. Creating site permissions delegated needs a SharePoint or
        global administrator with Sites.FullControl.All.
    .EXAMPLE
        Grant-NSPGraphSitePermission -SiteId $site.SiteId -ClientId $record.ClientId -DisplayName 'NSP-ScanBridge-Ingest' -Execute
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$SiteId,
        [Parameter(Mandatory)][guid]$ClientId,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$DisplayName,
        [ValidateSet('read', 'write')][string]$Role = 'write',
        [switch]$Execute
    )

    $rank = @{ read = 1; write = 2; manage = 3; fullcontrol = 4 }
    $sitePath = "sites/$([uri]::EscapeDataString($SiteId))/permissions"
    $result = [ordered]@{ Status = $null; SiteId = $SiteId; ClientId = [string]$ClientId; Role = $Role; ExistingRoles = @(); PermissionId = $null; Message = $null }
    $mine = @(Invoke-NSPGraphCollection -Uri $sitePath | Where-Object {
            @(@($_['grantedToIdentitiesV2']) + @($_['grantedToIdentities']) | Where-Object { $_ -and $_['application'] -and $_['application']['id'] -eq [string]$ClientId }).Count
        })
    $existing = @($mine | ForEach-Object { @($_['roles']) } | Where-Object { $_ })
    $result.ExistingRoles = $existing
    $best = @($existing | ForEach-Object { $rank[[string]$_] } | Measure-Object -Maximum).Maximum
    if ($best -ge $rank[$Role]) {
        $result.Status = 'AlreadyGranted'; $result.PermissionId = [string]$mine[0]['id']
        $result.Message = "The app already has $($existing -join ', ') on this site."
        return [pscustomobject]$result
    }
    if (-not $Execute) {
        $result.Status = 'PlanOnly'
        $result.Message = $(if ($mine.Count) { "Run again with -Execute to raise the app's site role to $Role." } else { "Run again with -Execute to grant the app $Role on this site." })
        return [pscustomobject]$result
    }
    if (-not $PSCmdlet.ShouldProcess("site $SiteId", "Grant $Role to app $DisplayName ($ClientId)")) { return }
    if ($mine.Count) {
        Invoke-NSPGraphRequest -Method PATCH -Uri "$sitePath/$($mine[0]['id'])" -Body @{ roles = @($Role) } | Out-Null
        $result.PermissionId = [string]$mine[0]['id']; $result.Status = 'Upgraded'
    } else {
        $created = Invoke-NSPGraphRequest -Method POST -Uri $sitePath -Body @{
            roles = @($Role); grantedToIdentities = @(@{ application = @{ id = [string]$ClientId; displayName = $DisplayName } })
        }
        $result.PermissionId = [string]$created['id']; $result.Status = 'Granted'
    }
    $result.Message = "The app has $Role on this site."
    [pscustomobject]$result
}