Skip to content

Commit eda364e

Browse files
committed
Allow overwriting app templates; fix scope lookup
Add support for an Overwrite flag and logic to find & reuse existing templates/permission sets when creating app approval templates. Improve delegated permission handling by grouping multi-scope grants, preferring publishedPermissionScopes (with fallback to treat IDs as names), and adding diagnostics. Also adjust servicePrincipal fetch to request publishedPermissionScopes, reuse or generate PermissionSetId when updating, add stronger logging, and use -Force on table writes.
1 parent 24697fa commit eda364e

File tree

1 file changed

+105
-22
lines changed

1 file changed

+105
-22
lines changed

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function Invoke-ExecCreateAppTemplate {
1818
$AppId = $Request.Body.AppId
1919
$DisplayName = $Request.Body.DisplayName
2020
$Type = $Request.Body.Type # 'servicePrincipal' or 'application'
21+
$Overwrite = $Request.Body.Overwrite -eq $true
2122

2223
if ([string]::IsNullOrWhiteSpace($AppId)) {
2324
throw 'AppId is required'
@@ -88,14 +89,21 @@ function Invoke-ExecCreateAppTemplate {
8889
$AppRoleAssignments = ($GrantsResults | Where-Object { $_.id -eq 'assignments' }).body.value
8990

9091
$DelegateResourceAccess = $DelegatePermissionGrants | Group-Object -Property resourceId | ForEach-Object {
92+
$resourceAccessList = [System.Collections.Generic.List[object]]::new()
93+
foreach ($Grant in $_.Group) {
94+
if (-not [string]::IsNullOrWhiteSpace($Grant.scope)) {
95+
$scopeNames = $Grant.scope -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
96+
foreach ($scopeName in $scopeNames) {
97+
$resourceAccessList.Add([pscustomobject]@{
98+
id = $scopeName
99+
type = 'Scope'
100+
})
101+
}
102+
}
103+
}
91104
[pscustomobject]@{
92105
resourceAppId = ($TenantInfo | Where-Object -Property id -EQ $_.Name).appId
93-
resourceAccess = @($_.Group | ForEach-Object {
94-
[pscustomobject]@{
95-
id = $_.scope
96-
type = 'Scope'
97-
}
98-
})
106+
resourceAccess = @($resourceAccessList)
99107
}
100108
}
101109

@@ -229,11 +237,11 @@ function Invoke-ExecCreateAppTemplate {
229237
$RequestId = "sp-$RequestIndex"
230238
$AppIdToRequestId[$ResourceAppId] = $RequestId
231239

232-
# Use object ID to fetch full details with appRoles and oauth2PermissionScopes
240+
# Use object ID to fetch full details with appRoles
233241
$BulkRequests.Add([PSCustomObject]@{
234242
id = $RequestId
235243
method = 'GET'
236-
url = "/servicePrincipals/$($ResourceSPInfo.id)?`$select=id,appId,displayName,appRoles,oauth2PermissionScopes"
244+
url = "/servicePrincipals/$($ResourceSPInfo.id)?`$select=id,appId,displayName,appRoles,publishedPermissionScopes"
237245
})
238246
$RequestIndex++
239247
} else {
@@ -270,6 +278,8 @@ function Invoke-ExecCreateAppTemplate {
270278
continue
271279
}
272280

281+
#Write-Information ($ResourceSP | ConvertTo-Json -Depth 10)
282+
273283
foreach ($Access in $Resource.resourceAccess) {
274284
if ($Access.type -eq 'Role') {
275285
# Look up application permission name from appRoles
@@ -284,16 +294,27 @@ function Invoke-ExecCreateAppTemplate {
284294
Write-LogMessage -headers $Request.headers -API $APINAME -message "Application permission $($Access.id) not found in $ResourceAppId appRoles" -Sev 'Warning'
285295
}
286296
} elseif ($Access.type -eq 'Scope') {
287-
# Look up delegated permission name from oauth2PermissionScopes
288-
$PermissionScope = $ResourceSP.oauth2PermissionScopes | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1
289-
if ($PermissionScope) {
297+
Write-Information "Processing delegated permission with id $($Access.id) for resource appId $ResourceAppId"
298+
# Try to look up the permission by ID in publishedPermissionScopes
299+
$OAuth2Permission = $ResourceSP.publishedPermissionScopes | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1
300+
$OAuth2PermissionValue = $ResourceSP.publishedPermissionScopes | Where-Object { $_.value -eq $Access.id } | Select-Object -First 1
301+
if ($OAuth2Permission) {
302+
Write-Information "Found delegated permission in publishedPermissionScopes with value: $($OAuth2Permission.value)"
303+
# Found the permission - use the value from the lookup
290304
$PermObj = [PSCustomObject]@{
291305
id = $Access.id
292-
value = $PermissionScope.value # Use the claim value name, not the GUID
306+
value = $OAuth2Permission.value
293307
}
294308
[void]$DelegatedPerms.Add($PermObj)
295309
} else {
296-
Write-LogMessage -headers $Request.headers -API $APINAME -message "Delegated permission $($Access.id) not found in $ResourceAppId oauth2PermissionScopes" -Sev 'Warning'
310+
# Not found by ID - assume Access.id is already the permission name
311+
Write-Information "Could not find delegated permission by ID - using provided ID as value: $($Access.id)"
312+
Write-Information "OAuth2PermissionValueLookup: $($OAuth2PermissionValue | ConvertTo-Json -Depth 10)"
313+
$PermObj = [PSCustomObject]@{
314+
id = $OAuth2PermissionValue.id ?? $Access.id
315+
value = $Access.id
316+
}
317+
[void]$DelegatedPerms.Add($PermObj)
297318
}
298319
}
299320
}
@@ -304,10 +325,75 @@ function Invoke-ExecCreateAppTemplate {
304325
}
305326
}
306327

307-
# Create the permission set in AppPermissions table
328+
# Permission set ID will be determined after template lookup
329+
$PermissionSetId = $null
330+
}
331+
332+
# Get permissions table reference (needed later)
333+
$PermissionsTable = Get-CIPPTable -TableName 'AppPermissions'
334+
335+
# Create the template
336+
$Table = Get-CIPPTable -TableName 'templates'
337+
338+
# Check if template already exists
339+
# For servicePrincipal: match by AppId (immutable)
340+
# For application: match by DisplayName (since AppId changes when copied)
341+
$ExistingTemplate = $null
342+
if ($Overwrite) {
343+
try {
344+
$Filter = "PartitionKey eq 'AppApprovalTemplate'"
345+
$AllTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter
346+
$TemplateNameToMatch = "$DisplayName (Auto-created)"
347+
348+
foreach ($Template in $AllTemplates) {
349+
$TemplateData = $Template.JSON | ConvertFrom-Json
350+
$IsMatch = $false
351+
352+
if ($Type -eq 'servicePrincipal') {
353+
# Match by AppId for service principals
354+
$IsMatch = $TemplateData.AppId -eq $AppId
355+
} else {
356+
# Match by TemplateName for app registrations
357+
$IsMatch = $TemplateData.TemplateName -eq $TemplateNameToMatch
358+
}
359+
360+
if ($IsMatch) {
361+
$ExistingTemplate = $Template
362+
# Reuse the existing permission set ID if it exists
363+
if ($TemplateData.PermissionSetId) {
364+
$PermissionSetId = $TemplateData.PermissionSetId
365+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Found existing permission set ID: $PermissionSetId in template" -Sev 'Info'
366+
} else {
367+
Write-LogMessage -headers $Request.headers -API $APINAME -message 'Existing template found but has no PermissionSetId' -Sev 'Warning'
368+
}
369+
break
370+
}
371+
}
372+
} catch {
373+
# Ignore lookup errors
374+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Error during template lookup: $($_.Exception.Message)" -Sev 'Warning'
375+
}
376+
}
377+
378+
if ($ExistingTemplate) {
379+
$TemplateId = $ExistingTemplate.RowKey
380+
$MatchCriteria = if ($Type -eq 'servicePrincipal') { "AppId: $AppId" } else { "DisplayName: $DisplayName" }
381+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Overwriting existing template matched by $MatchCriteria (Template ID: $TemplateId)" -Sev 'Info'
382+
if ($PermissionSetId) {
383+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Reusing permission set ID: $PermissionSetId" -Sev 'Info'
384+
}
385+
} else {
386+
$TemplateId = (New-Guid).Guid
387+
}
388+
389+
# Create new permission set ID if we don't have one yet
390+
if (-not $PermissionSetId) {
308391
$PermissionSetId = (New-Guid).Guid
309-
$PermissionsTable = Get-CIPPTable -TableName 'AppPermissions'
392+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Creating new permission set ID: $PermissionSetId" -Sev 'Info'
393+
}
310394

395+
# Now create/update the permission set entity with the determined ID
396+
if ($Permissions -and $Permissions.Count -gt 0) {
311397
$PermissionEntity = @{
312398
'PartitionKey' = 'Templates'
313399
'RowKey' = [string]$PermissionSetId
@@ -317,13 +403,9 @@ function Invoke-ExecCreateAppTemplate {
317403
}
318404

319405
Add-CIPPAzDataTableEntity @PermissionsTable -Entity $PermissionEntity -Force
320-
Write-LogMessage -headers $Request.headers -API $APINAME -message "Permission set created with ID: $PermissionSetId for $($Permissions.Count) resource(s)" -Sev 'Info'
406+
Write-LogMessage -headers $Request.headers -API $APINAME -message "Permission set saved with ID: $PermissionSetId for $($Permissions.Count) resource(s)" -Sev 'Info'
321407
}
322408

323-
# Create the template
324-
$Table = Get-CIPPTable -TableName 'templates'
325-
$TemplateId = (New-Guid).Guid
326-
327409
$TemplateJson = @{
328410
TemplateName = "$DisplayName (Auto-created)"
329411
AppId = $AppId
@@ -343,7 +425,7 @@ function Invoke-ExecCreateAppTemplate {
343425
PartitionKey = 'AppApprovalTemplate'
344426
}
345427

346-
Add-CIPPAzDataTableEntity @Table -Entity $Entity
428+
Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force
347429

348430
$PermissionCount = 0
349431
if ($CIPPPermissions -and $CIPPPermissions.Count -gt 0) {
@@ -358,7 +440,8 @@ function Invoke-ExecCreateAppTemplate {
358440
}
359441
}
360442

361-
$Message = "Template created: $DisplayName with $PermissionCount permission(s)"
443+
$Action = if ($ExistingTemplate) { 'updated' } else { 'created' }
444+
$Message = "Template $($Action) - $DisplayName with $PermissionCount permission(s)"
362445
Write-LogMessage -headers $Request.headers -API $APINAME -message $Message -Sev 'Info'
363446

364447
$Body = @{

0 commit comments

Comments
 (0)