ScriptBlocks/Grant-Access-SitePermission.ps1
|
param( [Parameter(Mandatory=$true)] [ValidateSet('grant','read','delete')] [string]$Mode, [Parameter(Mandatory=$true)] [string]$ClientId, [Parameter(Mandatory=$true)] [string]$TenantId, [Parameter(Mandatory=$true)] [string]$Thumbprint, [Parameter(Mandatory=$true)] [string]$TenantHost, [Parameter(Mandatory=$true)] [string]$SitePath, [Parameter(Mandatory=$true)] [string]$ContainerName, [Parameter(Mandatory=$true)] [string]$FolderPath, [Parameter(Mandatory=$false)] [AllowEmptyString()] [string]$FilePath = '', [Parameter(Mandatory=$true)] [string]$TargetAppId, [ValidateSet('read','write')] [string]$Role = 'read', [switch]$ForceUpgrade, [switch]$CleanDuplicates, [switch]$AllowRoot ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue' $WarningPreference = 'SilentlyContinue' $InformationPreference = 'SilentlyContinue' function _RoleRank([string[]]$roles) { $r = @($roles) if ($r -contains 'owner') { return 3 } if ($r -contains 'write') { return 2 } if ($r -contains 'read') { return 1 } return 0 } function _Satisfies([string[]]$existingRoles, [string]$requestedRole) { $r = @($existingRoles) if ($requestedRole -eq 'read') { return ($r -contains 'read' -or $r -contains 'write' -or $r -contains 'owner') } if ($requestedRole -eq 'write') { return ($r -contains 'write' -or $r -contains 'owner') } return $false } function _GetAppPermissionMatches($permissions, [string]$appId) { $pmatches = @() foreach ($p in @($permissions)) { if ($p.grantedToV2 -and $p.grantedToV2.application -and $p.grantedToV2.application.id -eq $appId) { $pmatches += $p continue } if ($p.grantedToIdentitiesV2) { foreach ($idset in @($p.grantedToIdentitiesV2)) { if ($idset.application -and $idset.application.id -eq $appId) { $pmatches += $p break } } } if ($p.grantedToIdentities) { foreach ($idset in @($p.grantedToIdentities)) { if ($idset.application -and $idset.application.id -eq $appId) { $pmatches += $p break } } } } return $pmatches } function Test-WorkerAppHasGraphAppRoles { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$WorkerAppId, [Parameter(Mandatory)] [string[]]$RequiredRoles ) try { $graphSp = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'&`$select=id,appRoles").value | Select-Object -First 1 if (-not $graphSp) { throw "Microsoft Graph servicePrincipal not found." } $workerSp = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '$WorkerAppId'&`$select=id,appId").value | Select-Object -First 1 if (-not $workerSp) { throw "Worker servicePrincipal not found for appId '$WorkerAppId'." } $assignments = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($workerSp.id)/appRoleAssignments").value $roleIdToValue = @{} foreach ($r in $graphSp.appRoles) { $roleIdToValue[$r.id] = $r.value } $granted = @() foreach ($a in $assignments) { if ($a.resourceId -eq $graphSp.id -and $roleIdToValue.ContainsKey($a.appRoleId)) { $granted += $roleIdToValue[$a.appRoleId] } } $missing = @($RequiredRoles | Where-Object { $_ -notin $granted }) return [pscustomobject]@{ Status = if ($missing.Count -eq 0) { 'Pass' } else { 'Fail' } RequiredRoles = $RequiredRoles GrantedRoles = ($granted | Sort-Object -Unique) MissingRoles = $missing Note = '' } } catch { return [pscustomobject]@{ Status = 'Skipped' RequiredRoles = $RequiredRoles GrantedRoles = @() MissingRoles = @() Note = "Skipped directory-based validation (likely missing Application.Read.All/Directory.Read.All on provisioning identity). Error: $($_.Exception.Message)" } } } function Test-WorkerAppPermissionsExtended { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$WorkerAppId, [Parameter(Mandatory)] [string[]]$RequiredGraphRoles ) try { $graphSp = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals(appId='00000003-0000-0000-c000-000000000000')?`$select=id,appRoles" if (-not $graphSp) { throw "Graph SP not found" } $workerSp = (Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '$WorkerAppId'&`$select=id,appId").value | Select-Object -First 1 if (-not $workerSp) { throw "Worker SP not found for appId '$WorkerAppId'" } $spoSp = (Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=startswith(displayName,'SharePoint')&`$select=id,displayName").value | Select-Object -First 1 if (-not $spoSp) { throw "SharePoint SP not found" } $graphRolesMap = @{} foreach ($r in $graphSp.appRoles) { $graphRolesMap[$r.id] = $r.value } $spoSpFull = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($spoSp.id)?`$select=appRoles" $spoRolesMap = @{} foreach ($r in $spoSpFull.appRoles) { $spoRolesMap[$r.id] = $r.value } $assignments = (Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($workerSp.id)/appRoleAssignments").value $graphGranted = @() $spoGranted = @() foreach ($a in $assignments) { if ($a.resourceId -eq $graphSp.id -and $graphRolesMap.ContainsKey($a.appRoleId)) { $graphGranted += $graphRolesMap[$a.appRoleId] } if ($a.resourceId -eq $spoSp.id -and $spoRolesMap.ContainsKey($a.appRoleId)) { $spoGranted += $spoRolesMap[$a.appRoleId] } } $graphGranted = $graphGranted | Sort-Object -Unique $spoGranted = $spoGranted | Sort-Object -Unique $missingGraph = @( $RequiredGraphRoles | Where-Object { $_ -notin $graphGranted } ) return [pscustomobject]@{ Graph = [pscustomobject]@{ GrantedRoles = $graphGranted MissingRoles = $missingGraph } SharePoint = [pscustomobject]@{ GrantedRoles = $spoGranted MissingRoles = @() } Summary = [pscustomobject]@{ HasGraphSitesSelected = ($graphGranted -contains "Sites.Selected") HasSharePointSitesSelected = ($spoGranted -contains "Sites.Selected") EffectiveScopes = @( $graphGranted $spoGranted ) | Sort-Object -Unique } Status = if ($missingGraph.Count -eq 0) { "Pass" } else { "Fail" } } } catch { return [pscustomobject]@{ Status = "Skipped" Error = $_.Exception.Message } } } function Test-WorkerAppPermissions_ExtendedWithDelegated { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkerAppId, [Parameter()] [string[]]$RequiredGraphAppRoles = @(), [Parameter()] [string[]]$RequiredGraphDelegatedScopes = @(), [Parameter()] [string[]]$RequiredSharePointAppRoles = @(), [Parameter()] [string[]]$RequiredSharePointDelegatedScopes = @() ) $MicrosoftGraphAppId = '00000003-0000-0000-c000-000000000000' $SharePointOnlineAppId = '00000003-0000-0ff1-ce00-000000000000' $SupportedGraphSelectedPermissions = @( 'Sites.Selected' 'Lists.SelectedOperations.Selected' 'ListItems.SelectedOperations.Selected' 'Files.SelectedOperations.Selected' ) $SupportedSharePointRestPermissions = @( 'Sites.Selected' ) function Test-LocalBlank { [CmdletBinding()] param( [Parameter()] [AllowNull()] [object]$Value ) if ($null -eq $Value) { return $true } return ([string]$Value).Trim().Length -eq 0 } function ConvertTo-NormalizedStringArray { [CmdletBinding()] param( [Parameter()] [AllowNull()] [object[]]$Value ) return @( $Value | Where-Object { $null -ne $_ -and -not (Test-LocalBlank $_) } | ForEach-Object { ([string]$_).Trim() } | Sort-Object -Unique ) } function Get-MissingValues { [CmdletBinding()] param( [Parameter()] [AllowNull()] [object[]]$Granted, [Parameter()] [AllowNull()] [object[]]$Required ) $normalizedGranted = @( ConvertTo-NormalizedStringArray -Value $Granted ) $normalizedRequired = @( ConvertTo-NormalizedStringArray -Value $Required ) return @( $normalizedRequired | Where-Object { $_ -notin $normalizedGranted } ) } function Get-GraphCollection { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Uri ) $results = New-Object System.Collections.Generic.List[object] $nextUri = $Uri while (-not (Test-LocalBlank $nextUri)) { $response = Invoke-MgGraphRequest ` -Method GET ` -Uri $nextUri ` -ErrorAction Stop if ($null -ne $response.value) { foreach ($item in @($response.value)) { [void]$results.Add($item) } } elseif ($null -ne $response) { [void]$results.Add($response) } $nextUri = $null if ( $null -ne $response -and $response.PSObject.Properties.Name -contains '@odata.nextLink' ) { $nextUri = [string]$response.'@odata.nextLink' } } return @($results.ToArray()) } function Get-SingleServicePrincipalByAppId { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AppId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Description, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SelectProperties = 'id,appId,displayName' ) $escapedAppId = $AppId.Replace("'", "''") $uri = 'https://graph.microsoft.com/v1.0/servicePrincipals' + "?`$filter=appId eq '$escapedAppId'" + "&`$select=$SelectProperties" $matches = @( Get-GraphCollection -Uri $uri ) if ($matches.Count -eq 0) { throw ( "$Description service principal was not found " + "for AppId '$AppId'." ) } if ($matches.Count -gt 1) { throw ( "Multiple service principals were found for " + "$Description AppId '$AppId'." ) } return $matches[0] } function New-FailedValidationResult { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ErrorMessage ) return [pscustomobject]@{ Status = 'Skipped' Error = $ErrorMessage Worker = [pscustomobject]@{ ServicePrincipalId = '' AppId = $WorkerAppId DisplayName = '' } Graph = [pscustomobject]@{ ServicePrincipalId = '' ServicePrincipalAppId = $MicrosoftGraphAppId ServicePrincipalDisplayName = 'Microsoft Graph' Application = [pscustomobject]@{ GrantedRoles = @() RequiredRoles = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphAppRoles ) MissingRoles = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphAppRoles ) } Delegated = [pscustomobject]@{ GrantedScopes = @() RequiredScopes = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphDelegatedScopes ) MissingScopes = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphDelegatedScopes ) DetectionStatus = 'Skipped' Note = $ErrorMessage } SupportedSelectedPermissions = @( $SupportedGraphSelectedPermissions ) SelectedApplicationRolesGranted = @() HasSitesSelectedApplicationRole = $false RequirementsSpecified = $false RequirementsSatisfied = $false IsUsableForSelectedApplicationAccess = $false } SharePoint = [pscustomobject]@{ ServicePrincipalId = '' ServicePrincipalAppId = $SharePointOnlineAppId ServicePrincipalDisplayName = 'Office 365 SharePoint Online' Application = [pscustomobject]@{ GrantedRoles = @() RequiredRoles = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointAppRoles ) MissingRoles = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointAppRoles ) } Delegated = [pscustomobject]@{ GrantedScopes = @() RequiredScopes = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointDelegatedScopes ) MissingScopes = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointDelegatedScopes ) DetectionStatus = 'Skipped' Note = $ErrorMessage } SupportedRestPermissions = @( $SupportedSharePointRestPermissions ) RestApplicationRolesGranted = @() HasSitesSelectedApplicationRole = $false RequirementsSpecified = $false RequirementsSatisfied = $false IsUsableForSelectedApplicationAccess = $false } Summary = [pscustomobject]@{ WorkerAppId = $WorkerAppId EvaluationMode = 'Failed' GraphRequirementsSpecified = $false SharePointRequirementsSpecified = $false AnyRequirementsSpecified = $false EffectiveApplicationRoles = @() EffectiveDelegatedScopes = @() GraphApiUsable = $false SharePointRestApiUsable = $false AnyApplicationApiUsable = $false HasGraphSitesSelectedAppRole = $false HasSharePointSitesSelectedAppRole = $false BothApisMissingSitesSelected = $true RequestedPermissionsSatisfied = $false } } } try { $requiredGraphApp = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphAppRoles ) $requiredGraphDelegated = @( ConvertTo-NormalizedStringArray ` -Value $RequiredGraphDelegatedScopes ) $requiredSharePointApp = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointAppRoles ) $requiredSharePointDelegated = @( ConvertTo-NormalizedStringArray ` -Value $RequiredSharePointDelegatedScopes ) $graphRequirementsSpecified = ( $requiredGraphApp.Count -gt 0 -or $requiredGraphDelegated.Count -gt 0 ) $sharePointRequirementsSpecified = ( $requiredSharePointApp.Count -gt 0 -or $requiredSharePointDelegated.Count -gt 0 ) $anyRequirementsSpecified = ( $graphRequirementsSpecified -or $sharePointRequirementsSpecified ) $evaluationMode = if ($anyRequirementsSpecified) { 'RequestedPermissions' } else { 'Discovery' } $graphSp = Get-SingleServicePrincipalByAppId ` -AppId $MicrosoftGraphAppId ` -Description 'Microsoft Graph' ` -SelectProperties 'id,appId,displayName,appRoles' $sharePointSp = Get-SingleServicePrincipalByAppId ` -AppId $SharePointOnlineAppId ` -Description 'Office 365 SharePoint Online' ` -SelectProperties 'id,appId,displayName,appRoles' $workerSp = Get-SingleServicePrincipalByAppId ` -AppId $WorkerAppId ` -Description 'Worker application' ` -SelectProperties 'id,appId,displayName' $graphRoleIdToValue = @{} foreach ($role in @($graphSp.appRoles)) { $roleId = [string]$role.id $roleValue = [string]$role.value if ( -not (Test-LocalBlank $roleId) -and -not (Test-LocalBlank $roleValue) ) { $graphRoleIdToValue[$roleId] = $roleValue } } $sharePointRoleIdToValue = @{} foreach ($role in @($sharePointSp.appRoles)) { $roleId = [string]$role.id $roleValue = [string]$role.value if ( -not (Test-LocalBlank $roleId) -and -not (Test-LocalBlank $roleValue) ) { $sharePointRoleIdToValue[$roleId] = $roleValue } } $assignmentUri = 'https://graph.microsoft.com/v1.0/servicePrincipals/' + $workerSp.id + '/appRoleAssignments' + '?$select=id,appRoleId,resourceId,resourceDisplayName' $assignments = @( Get-GraphCollection -Uri $assignmentUri ) $graphAppRoleList = New-Object System.Collections.Generic.List[string] $sharePointAppRoleList = New-Object System.Collections.Generic.List[string] foreach ($assignment in $assignments) { $resourceId = [string]$assignment.resourceId $appRoleId = [string]$assignment.appRoleId if ( $resourceId -eq [string]$graphSp.id -and $graphRoleIdToValue.ContainsKey($appRoleId) ) { [void]$graphAppRoleList.Add( $graphRoleIdToValue[$appRoleId] ) } if ( $resourceId -eq [string]$sharePointSp.id -and $sharePointRoleIdToValue.ContainsKey($appRoleId) ) { [void]$sharePointAppRoleList.Add( $sharePointRoleIdToValue[$appRoleId] ) } } $graphAppGranted = @( ConvertTo-NormalizedStringArray ` -Value $graphAppRoleList.ToArray() ) $sharePointAppGranted = @( ConvertTo-NormalizedStringArray ` -Value $sharePointAppRoleList.ToArray() ) $graphDelegatedGranted = @() $sharePointDelegatedGranted = @() $delegatedDetectionStatus = 'Pass' $delegatedDetectionNote = '' try { $delegatedUri = 'https://graph.microsoft.com/v1.0/servicePrincipals/' + $workerSp.id + '/oauth2PermissionGrants' + '?$select=id,clientId,consentType,principalId,resourceId,scope' $delegatedGrants = @( Get-GraphCollection -Uri $delegatedUri ) $graphDelegatedList = New-Object System.Collections.Generic.List[string] $sharePointDelegatedList = New-Object System.Collections.Generic.List[string] foreach ($grant in $delegatedGrants) { if (Test-LocalBlank $grant.scope) { continue } $scopes = @( ([string]$grant.scope) -split '\s+' | Where-Object { -not (Test-LocalBlank $_) } ) if ([string]$grant.resourceId -eq [string]$graphSp.id) { foreach ($scope in $scopes) { [void]$graphDelegatedList.Add($scope) } } elseif ( [string]$grant.resourceId -eq [string]$sharePointSp.id ) { foreach ($scope in $scopes) { [void]$sharePointDelegatedList.Add($scope) } } } $graphDelegatedGranted = @( ConvertTo-NormalizedStringArray ` -Value $graphDelegatedList.ToArray() ) $sharePointDelegatedGranted = @( ConvertTo-NormalizedStringArray ` -Value $sharePointDelegatedList.ToArray() ) } catch { $delegatedDetectionStatus = 'Skipped' $delegatedDetectionNote = ( 'Delegated permission detection was skipped. Error: ' + $_.Exception.Message ) } $graphAppMissing = @( Get-MissingValues ` -Granted $graphAppGranted ` -Required $requiredGraphApp ) $graphDelegatedMissing = @( Get-MissingValues ` -Granted $graphDelegatedGranted ` -Required $requiredGraphDelegated ) $sharePointAppMissing = @( Get-MissingValues ` -Granted $sharePointAppGranted ` -Required $requiredSharePointApp ) $sharePointDelegatedMissing = @( Get-MissingValues ` -Granted $sharePointDelegatedGranted ` -Required $requiredSharePointDelegated ) $graphSelectedAppGranted = @( $graphAppGranted | Where-Object { $_ -in $SupportedGraphSelectedPermissions } | Sort-Object -Unique ) $sharePointRestAppGranted = @( $sharePointAppGranted | Where-Object { $_ -in $SupportedSharePointRestPermissions } | Sort-Object -Unique ) $hasGraphSitesSelected = ( $graphAppGranted -contains 'Sites.Selected' ) $hasSharePointSitesSelected = ( $sharePointAppGranted -contains 'Sites.Selected' ) $graphApplicationRequirementsSatisfied = ( $graphAppMissing.Count -eq 0 ) $graphDelegatedRequirementsSatisfied = ( $graphDelegatedMissing.Count -eq 0 -and ( $requiredGraphDelegated.Count -eq 0 -or $delegatedDetectionStatus -eq 'Pass' ) ) $graphRequirementsSatisfied = ( $graphApplicationRequirementsSatisfied -and $graphDelegatedRequirementsSatisfied ) $sharePointApplicationRequirementsSatisfied = ( $sharePointAppMissing.Count -eq 0 ) $sharePointDelegatedRequirementsSatisfied = ( $sharePointDelegatedMissing.Count -eq 0 -and ( $requiredSharePointDelegated.Count -eq 0 -or $delegatedDetectionStatus -eq 'Pass' ) ) $sharePointRequirementsSatisfied = ( $sharePointApplicationRequirementsSatisfied -and $sharePointDelegatedRequirementsSatisfied ) if ($anyRequirementsSpecified) { $graphApiUsable = ( $graphRequirementsSpecified -and $graphRequirementsSatisfied ) $sharePointRestApiUsable = ( $sharePointRequirementsSpecified -and $sharePointRequirementsSatisfied ) } else { $graphApiUsable = ( $graphSelectedAppGranted.Count -gt 0 ) $sharePointRestApiUsable = ( $sharePointRestAppGranted.Count -gt 0 ) } $anyApiUsable = ( $graphApiUsable -or $sharePointRestApiUsable ) if ($anyRequirementsSpecified) { $requestedPermissionsSatisfied = $anyApiUsable } else { $requestedPermissionsSatisfied = $true } $bothApisMissingSitesSelected = ( -not $hasGraphSitesSelected -and -not $hasSharePointSitesSelected ) $status = if ($anyApiUsable) { 'Pass' } else { 'Fail' } return [pscustomobject]@{ Status = $status Error = '' Worker = [pscustomobject]@{ ServicePrincipalId = [string]$workerSp.id AppId = [string]$workerSp.appId DisplayName = [string]$workerSp.displayName } Graph = [pscustomobject]@{ ServicePrincipalId = [string]$graphSp.id ServicePrincipalAppId = [string]$graphSp.appId ServicePrincipalDisplayName = [string]$graphSp.displayName Application = [pscustomobject]@{ GrantedRoles = $graphAppGranted RequiredRoles = $requiredGraphApp MissingRoles = $graphAppMissing } Delegated = [pscustomobject]@{ GrantedScopes = $graphDelegatedGranted RequiredScopes = $requiredGraphDelegated MissingScopes = $graphDelegatedMissing DetectionStatus = $delegatedDetectionStatus Note = $delegatedDetectionNote } SupportedSelectedPermissions = @( $SupportedGraphSelectedPermissions ) SelectedApplicationRolesGranted = $graphSelectedAppGranted HasSitesSelectedApplicationRole = $hasGraphSitesSelected RequirementsSpecified = $graphRequirementsSpecified RequirementsSatisfied = $graphRequirementsSatisfied IsUsableForSelectedApplicationAccess = $graphApiUsable } SharePoint = [pscustomobject]@{ ServicePrincipalId = [string]$sharePointSp.id ServicePrincipalAppId = [string]$sharePointSp.appId ServicePrincipalDisplayName = ( [string]$sharePointSp.displayName ) Application = [pscustomobject]@{ GrantedRoles = $sharePointAppGranted RequiredRoles = $requiredSharePointApp MissingRoles = $sharePointAppMissing } Delegated = [pscustomobject]@{ GrantedScopes = $sharePointDelegatedGranted RequiredScopes = $requiredSharePointDelegated MissingScopes = $sharePointDelegatedMissing DetectionStatus = $delegatedDetectionStatus Note = $delegatedDetectionNote } SupportedRestPermissions = @( $SupportedSharePointRestPermissions ) RestApplicationRolesGranted = $sharePointRestAppGranted HasSitesSelectedApplicationRole = $hasSharePointSitesSelected RequirementsSpecified = $sharePointRequirementsSpecified RequirementsSatisfied = $sharePointRequirementsSatisfied IsUsableForSelectedApplicationAccess = ( $sharePointRestApiUsable ) } Summary = [pscustomobject]@{ WorkerAppId = $WorkerAppId EvaluationMode = $evaluationMode GraphRequirementsSpecified = $graphRequirementsSpecified SharePointRequirementsSpecified = ( $sharePointRequirementsSpecified ) AnyRequirementsSpecified = $anyRequirementsSpecified EffectiveApplicationRoles = @( @($graphAppGranted) + @($sharePointAppGranted) | Sort-Object -Unique ) EffectiveDelegatedScopes = @( @($graphDelegatedGranted) + @($sharePointDelegatedGranted) | Sort-Object -Unique ) GraphApiUsable = $graphApiUsable SharePointRestApiUsable = $sharePointRestApiUsable AnyApplicationApiUsable = $anyApiUsable HasGraphSitesSelectedAppRole = $hasGraphSitesSelected HasSharePointSitesSelectedAppRole = ( $hasSharePointSitesSelected ) BothApisMissingSitesSelected = ( $bothApisMissingSitesSelected ) RequestedPermissionsSatisfied = ( $requestedPermissionsSatisfied ) } } } catch { return New-FailedValidationResult ` -ErrorMessage $_.Exception.ToString() } } try { Import-Module Microsoft.Graph.Sites -RequiredVersion 2.33.0 -ErrorAction Stop | Out-Null Connect-MgGraph -ClientId $ClientId -TenantId $TenantId -CertificateThumbprint $Thumbprint -NoWelcome | Out-Null if (-not $SitePath.StartsWith('/')) { $SitePath = '/' + $SitePath } $siteUri = 'https://graph.microsoft.com/v1.0/sites/{0}:{1}:?$select=id,webUrl' -f $TenantHost, $SitePath $site = Invoke-MgGraphRequest -Method GET -Uri $siteUri $siteId = $site.id $siteWebUrl = $site.webUrl $permsUri = 'https://graph.microsoft.com/v1.0/sites/{0}/permissions' -f $siteId $canListPermissions = $true $perms = @() $listError = '' try { $permResp = Invoke-MgGraphRequest -Method GET -Uri $permsUri $perms = @($permResp.value) } catch { $canListPermissions = $false $listError = $_.Exception.ToString() $perms = @() } $appId = $TargetAppId.ToString() $pmatches = if ($canListPermissions) { _GetAppPermissionMatches -permissions $perms -appId $appId } else { @() } $best = $null if ($pmatches.Count -gt 0) { $best = $pmatches | Sort-Object { _RoleRank $_.roles } -Descending | Select-Object -First 1 } $Changed = $false $Deleted = $false $Granted = $false $ActualRole = '' $permissionIds = @() if ($best) { $Granted = $true $ActualRole = (@($best.roles) | Select-Object -First 1) $permissionIds = $pmatches | ForEach-Object { $_.id } } switch ($Mode) { 'read' { $requiredScopes = @('Sites.Selected') $validation = Test-WorkerAppPermissions_ExtendedWithDelegated -WorkerAppId $TargetAppId $listError = $validation | ConvertTo-Json -Compress -Depth 10 } 'grant' { if ($CleanDuplicates -and $canListPermissions -and $pmatches.Count -gt 1 -and $best) { $toRemove = $pmatches | Where-Object { $_.id -ne $best.id } foreach ($p in $toRemove) { Invoke-MgGraphRequest -Method DELETE -Uri ('{0}/{1}' -f $permsUri, $p.id) | Out-Null $Deleted = $true } $permResp = Invoke-MgGraphRequest -Method GET -Uri $permsUri $perms = @($permResp.value) $pmatches = _GetAppPermissionMatches -permissions $perms -appId $appId $best = $null if ($pmatches.Count -gt 0) { $best = $pmatches | Sort-Object { _RoleRank $_.roles } -Descending | Select-Object -First 1 } $Granted = ($null -ne $best) $ActualRole = if ($best) { (@($best.roles) | Select-Object -First 1) } else { '' } $permissionIds = if ($pmatches) { $pmatches | ForEach-Object { $_.id } } else { @() } } $alreadySatisfies = $false if ($best -and (_Satisfies -existingRoles $best.roles -requestedRole $Role)) { $alreadySatisfies = $true } if ($alreadySatisfies -and (-not $ForceUpgrade)) { } else { if ($ForceUpgrade -and $canListPermissions -and $pmatches.Count -gt 0) { foreach ($p in $pmatches) { Invoke-MgGraphRequest -Method DELETE -Uri ('{0}/{1}' -f $permsUri, $p.id) | Out-Null $Deleted = $true } } $bodyObj = @{ roles = @($Role) grantedToIdentities = @( @{ application = @{ id = $TargetAppId displayName = "TargetApp" } } ) } try { Invoke-MgGraphRequest -Method POST -Uri $permsUri -Body $bodyObj -ContentType 'application/json' | Out-Null $Changed = $true } catch { $details = $null if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $details = $_.ErrorDetails.Message } throw ("POST failed. permsUri={0}`nErrorDetails={1}`nException={2}" -f $permsUri, $details, $_.Exception.ToString()) } if ($canListPermissions) { $permResp = Invoke-MgGraphRequest -Method GET -Uri $permsUri $perms = @($permResp.value) $pmatches = _GetAppPermissionMatches -permissions $perms -appId $appId $best = $pmatches | Sort-Object { _RoleRank $_.roles } -Descending | Select-Object -First 1 $Granted = ($null -ne $best) $ActualRole = if ($best) { (@($best.roles) | Select-Object -First 1) } else { '' } $permissionIds = if ($pmatches) { $pmatches | ForEach-Object { $_.id } } else { @() } } else { $Granted = $true $ActualRole = $Role } } } 'delete' { if (-not $canListPermissions) { throw "Cannot delete without listing existing permission IDs. GET /sites/{siteId}/permissions failed: $listError" } if ($pmatches.Count -gt 0) { foreach ($p in $pmatches) { Invoke-MgGraphRequest -Method DELETE -Uri ('{0}/{1}' -f $permsUri, $p.id) | Out-Null $Deleted = $true } $Changed = $Deleted $permResp = Invoke-MgGraphRequest -Method GET -Uri $permsUri $perms = @($permResp.value) $pmatches = _GetAppPermissionMatches -permissions $perms -appId $appId $best = $null if ($pmatches.Count -gt 0) { $best = $pmatches | Sort-Object { _RoleRank $_.roles } -Descending | Select-Object -First 1 } $Granted = ($null -ne $best) $ActualRole = if ($best) { (@($best.roles) | Select-Object -First 1) } else { '' } $permissionIds = if ($pmatches) { $pmatches | ForEach-Object { $_.id } } else { @() } } } } $result = @{ Success = $true Data = @{ Mode = $Mode TenantHost = $TenantHost SitePath = $SitePath ContainerName = $ContainerName FolderPath = $FolderPath SiteId = $siteId SiteWebUrl = $siteWebUrl TargetAppId = $appId Granted = $Granted Role = $ActualRole PermissionIds = ($permissionIds -join ',') RequestedRole = $Role Changed = $Changed Deleted = $Deleted CanListPermissions = $canListPermissions ListError = $listError } } } catch { $result = @{ Success = $false Error = $_.Exception.ToString() } } Write-Output ('__JSON__' + ($result | ConvertTo-Json -Compress -Depth 10)) |