@@ -12,9 +12,11 @@ function Get-CippSamPermissions {
1212 The effective set returned in .Permissions is therefore always manifest ∪ extras. Each permission
1313 is annotated with a 'required' boolean so the UI can lock the manifest-defined defaults.
1414
15- Unless -NoDiff is used, the function also pulls the live CIPP-SAM application registration from the
16- partner tenant and diffs its requiredResourceAccess against the effective set, surfacing
17- permissions that need to be added to (MissingPermissions) and removed from (PartnerAppDiff) the app.
15+ Unless -NoDiff is used, the function also reads what is actually granted on the CIPP-SAM enterprise
16+ application (service principal) in the partner tenant - appRoleAssignments (application/Role) and
17+ oauth2PermissionGrants (delegated/Scope) - and diffs those grants against the effective set,
18+ surfacing permissions that need to be granted (MissingPermissions) and grants that are present but
19+ not in the effective set (PartnerAppDiff). The app registration's requiredResourceAccess is not used.
1820
1921 . EXAMPLE
2022 Get-CippSamPermissions
@@ -195,38 +197,68 @@ function Get-CippSamPermissions {
195197 }
196198 }
197199
198- # Diff the effective set against the live CIPP-SAM application registration in the partner tenant.
199- # MissingPermissions = effective perms not yet on the app (need to be added).
200- # PartnerAppDiff also surfaces extra perms on the app that are not in the effective set (need to be removed).
200+ # Diff the effective set against what is actually GRANTED on the partner CIPP-SAM enterprise
201+ # application (service principal): appRoleAssignments for application (Role) permissions and
202+ # oauth2PermissionGrants for delegated (Scope) permissions. The app registration's
203+ # requiredResourceAccess is intentionally NOT used - permissions are applied as SP grants, so the
204+ # grants are the real source of truth for what the app can do.
205+ # MissingPermissions = effective perms not yet granted on the SP (need to be added).
206+ # PartnerAppDiff also surfaces extra grants on the SP that are not in the effective set.
201207 $MissingPermissions = @ {}
202208 $PartnerAppDiff = @ {}
203209 if (! $NoDiff.IsPresent ) {
204210 try {
205- $PartnerApp = New-GraphGETRequest - uri " https://graph.microsoft.com/beta/applications(appId='$ ( $env: ApplicationID ) ')?`$ select=requiredResourceAccess" - tenantid $env: TenantID - NoAuthCheck $true
211+ $PartnerSP = New-GraphGETRequest - uri " https://graph.microsoft.com/beta/servicePrincipals(appId='$ ( $env: ApplicationID ) ')?`$ select=id" - tenantid $env: TenantID - NoAuthCheck $true
212+ $AppRoleAssignments = New-GraphGETRequest - uri " https://graph.microsoft.com/beta/servicePrincipals/$ ( $PartnerSP.id ) /appRoleAssignments?`$ top=999" - tenantid $env: TenantID - NoAuthCheck $true
213+ $OAuthGrants = New-GraphGETRequest - uri " https://graph.microsoft.com/beta/servicePrincipals/$ ( $PartnerSP.id ) /oauth2PermissionGrants?`$ top=999" - tenantid $env: TenantID - NoAuthCheck $true
214+
215+ # Grants reference the resource SP's object id; map it back to the resource appId the
216+ # effective set is keyed on. Use $UsedServicePrincipals - it carries both id and appId
217+ # ($ServicePrincipals is selected without id, so its .id is null).
218+ $ResourceIdToAppId = @ {}
219+ foreach ($SP in $UsedServicePrincipals ) { if ($SP.id ) { $ResourceIdToAppId [$SP.id ] = $SP.appId } }
220+
221+ # Granted application roles (GUIDs) per resource appId.
222+ $GrantedRoleIdsByApp = @ {}
223+ foreach ($Assignment in $AppRoleAssignments ) {
224+ $ResAppId = $ResourceIdToAppId [$Assignment.resourceId ]
225+ if (! $ResAppId -or ! $Assignment.appRoleId ) { continue }
226+ if (-not $GrantedRoleIdsByApp.ContainsKey ($ResAppId )) { $GrantedRoleIdsByApp [$ResAppId ] = [System.Collections.Generic.List [string ]]::new() }
227+ $GrantedRoleIdsByApp [$ResAppId ].Add([string ]$Assignment.appRoleId )
228+ }
229+
230+ # Granted delegated scope NAMES per resource appId (oauth2 grants store space-delimited names).
231+ $GrantedScopesByApp = @ {}
232+ foreach ($Grant in $OAuthGrants ) {
233+ $ResAppId = $ResourceIdToAppId [$Grant.resourceId ]
234+ if (! $ResAppId ) { continue }
235+ if (-not $GrantedScopesByApp.ContainsKey ($ResAppId )) { $GrantedScopesByApp [$ResAppId ] = [System.Collections.Generic.List [string ]]::new() }
236+ foreach ($ScopeName in @ (($Grant.scope -split ' ' ) | Where-Object { $_ })) { $GrantedScopesByApp [$ResAppId ].Add($ScopeName ) }
237+ }
238+
206239 foreach ($AppId in $AllAppIds ) {
207240 $ServicePrincipal = $ServicePrincipals | Where-Object - Property appId -EQ $AppId
208- $AppRegResource = $PartnerApp.requiredResourceAccess | Where-Object - Property resourceAppId -EQ $AppId
209- $AppRegRoleIds = @ (($AppRegResource.resourceAccess | Where-Object { $_.type -eq ' Role' }).id)
210- $AppRegScopeIds = @ (($AppRegResource.resourceAccess | Where-Object { $_.type -eq ' Scope' }).id)
241+ $GrantedRoleIds = @ ($GrantedRoleIdsByApp [$AppId ] | Where-Object { $_ })
242+ $GrantedScopeNames = @ ($GrantedScopesByApp [$AppId ] | Where-Object { $_ })
211243
212- # Only GUID-based permissions live in the app registration's requiredResourceAccess.
213- # String-named scopes (e.g. the .Sdp AdditionalPermissions) are applied as direct grants,
214- # so excluding them here avoids permanent false-positive "missing" entries.
244+ # Application (Role) permissions compare by GUID against appRoleAssignments.
215245 $EffApp = @ ($EffectivePermissions .$AppId.applicationPermissions | Where-Object { $_.id -match $GuidRegex })
216- $EffDel = @ ($EffectivePermissions .$AppId.delegatedPermissions | Where-Object { $_.id -match $GuidRegex })
246+ # Delegated (Scope) permissions compare by NAME (value) against oauth2 grant scopes -
247+ # this covers both GUID-resolved scopes and the string-named AdditionalPermissions.
248+ $EffDel = @ ($EffectivePermissions .$AppId.delegatedPermissions )
217249 $EffAppIds = @ ($EffApp.id )
218- $EffDelIds = @ ($EffDel.id )
250+ $EffDelNames = @ ($EffDel.value )
219251
220- $MissingApp = @ (foreach ($Permission in $EffApp ) { if ($AppRegRoleIds -notcontains $Permission.id ) { $Permission } })
221- $MissingDel = @ (foreach ($Permission in $EffDel ) { if ($AppRegScopeIds - notcontains $Permission.id ) { $Permission } })
222- $ExtraApp = @ (foreach ($Id in $AppRegRoleIds ) {
252+ $MissingApp = @ (foreach ($Permission in $EffApp ) { if ($GrantedRoleIds -notcontains $Permission.id ) { $Permission } })
253+ $MissingDel = @ (foreach ($Permission in $EffDel ) { if ($Permission .value -and $GrantedScopeNames - notcontains $Permission.value ) { $Permission } })
254+ $ExtraApp = @ (foreach ($Id in ( $GrantedRoleIds | Sort-Object - Unique) ) {
223255 if ($EffAppIds -notcontains $Id ) {
224256 [PSCustomObject ]@ { id = $Id ; value = (($ServicePrincipal.appRoles | Where-Object - Property id -EQ $Id ).value) ?? $Id }
225257 }
226258 })
227- $ExtraDel = @ (foreach ($Id in $AppRegScopeIds ) {
228- if ($EffDelIds -notcontains $Id ) {
229- [PSCustomObject ]@ { id = $Id ; value = (( $ServicePrincipal .publishedPermissionScopes | Where-Object - Property id -EQ $Id ).value) ?? $Id }
259+ $ExtraDel = @ (foreach ($Name in ( $GrantedScopeNames | Sort-Object - Unique) ) {
260+ if ($EffDelNames -notcontains $Name ) {
261+ [PSCustomObject ]@ { id = $Name ; value = $Name }
230262 }
231263 })
232264
@@ -246,7 +278,7 @@ function Get-CippSamPermissions {
246278 }
247279 }
248280 } catch {
249- Write-Information " Failed to retrieve partner app registration for permission diff: $ ( $_.Exception.Message ) "
281+ Write-Information " Failed to retrieve partner enterprise app grants for permission diff: $ ( $_.Exception.Message ) "
250282 }
251283 }
252284
0 commit comments