Skip to content

Commit 00e3185

Browse files
feat: Add API endpoints for Intune Application Deployment Templates (KelvinTegelaar#1914)
- Related to KelvinTegelaar/CIPP#5400 - Add endpoints for creating/updating, listing, deleting and executing app templates
2 parents 57359c7 + d094188 commit 00e3185

4 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
function Invoke-AddAppTemplate {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint,AnyTenant
5+
.ROLE
6+
Endpoint.Application.ReadWrite
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$APIName = $Request.Params.CIPPEndpoint
12+
$Headers = $Request.Headers
13+
14+
try {
15+
$Body = $Request.Body
16+
if (!$Body.displayName) { throw 'You must enter a display name' }
17+
18+
$RawApps = $Body.apps
19+
if (!$RawApps -or ($RawApps | Measure-Object).Count -eq 0) {
20+
throw 'You must add at least one application'
21+
}
22+
23+
$AppsList = [System.Collections.Generic.List[hashtable]]::new()
24+
foreach ($App in @($RawApps)) {
25+
$ConfigValue = if ($App.config -is [string]) { $App.config } else { $App.config | ConvertTo-Json -Depth 15 -Compress }
26+
$AppsList.Add(@{
27+
appType = [string]$App.appType
28+
appName = [string]$App.appName
29+
config = [string]$ConfigValue
30+
})
31+
}
32+
33+
$Table = Get-CippTable -tablename 'templates'
34+
35+
# Upsert: if GUID provided, update existing template
36+
if ($Body.GUID) {
37+
$Filter = "PartitionKey eq 'AppTemplate' and RowKey eq '$($Body.GUID)'"
38+
$ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter $Filter
39+
}
40+
41+
if ($ExistingEntity) {
42+
$GUID = $ExistingEntity.RowKey
43+
} else {
44+
$GUID = (New-Guid).GUID
45+
}
46+
47+
$AppsJSON = ConvertTo-Json -InputObject @($AppsList.ToArray()) -Depth 15 -Compress -AsArray
48+
$TemplateJSON = ConvertTo-Json -InputObject @{
49+
Displayname = $Body.displayName
50+
Description = $Body.description ?? ''
51+
GUID = $GUID
52+
} -Depth 15 -Compress
53+
$TemplateJSON = $TemplateJSON.TrimEnd('}') + ',"Apps":' + $AppsJSON + '}'
54+
55+
$Table.Force = $true
56+
Add-CIPPAzDataTableEntity @Table -Entity @{
57+
JSON = [string]$TemplateJSON
58+
RowKey = [string]$GUID
59+
PartitionKey = 'AppTemplate'
60+
}
61+
62+
$AppCount = $AppsList.Count
63+
Write-LogMessage -headers $Headers -API $APIName -message "Saved app template '$($Body.displayName)' with $AppCount app(s)" -Sev 'Info'
64+
$Result = "Successfully saved app template '$($Body.displayName)' with $AppCount app(s)"
65+
$StatusCode = [HttpStatusCode]::OK
66+
} catch {
67+
$ErrorMessage = Get-CippException -Exception $_
68+
$Result = "Failed to add app template: $($ErrorMessage.NormalizedMessage)"
69+
Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage
70+
$StatusCode = [HttpStatusCode]::InternalServerError
71+
}
72+
73+
return ([HttpResponseContext]@{
74+
StatusCode = $StatusCode
75+
Body = @{ Results = $Result }
76+
})
77+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
function Invoke-ExecDeployAppTemplate {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint,AnyTenant
5+
.ROLE
6+
Endpoint.Application.ReadWrite
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$APIName = $Request.Params.CIPPEndpoint
12+
$Headers = $Request.Headers
13+
14+
try {
15+
$TemplateId = $Request.Body.templateId
16+
if (!$TemplateId) { throw 'No template ID provided' }
17+
18+
$Table = Get-CippTable -tablename 'templates'
19+
$Filter = "PartitionKey eq 'AppTemplate' and RowKey eq '$TemplateId'"
20+
$TemplateEntity = Get-CIPPAzDataTableEntity @Table -Filter $Filter
21+
if (!$TemplateEntity) { throw 'Template not found' }
22+
23+
$TemplateData = $TemplateEntity.JSON | ConvertFrom-Json -Depth 100
24+
$AppsRaw = $TemplateData.Apps
25+
26+
$Apps = [System.Collections.Generic.List[PSCustomObject]]::new()
27+
28+
$AppTypes = @($AppsRaw.appType)
29+
$AppNames = @($AppsRaw.appName)
30+
$AppConfigs = @($AppsRaw.config)
31+
32+
for ($i = 0; $i -lt $AppTypes.Count; $i++) {
33+
$Apps.Add([PSCustomObject]@{
34+
appType = [string]$AppTypes[$i]
35+
appName = [string]$AppNames[$i]
36+
config = [string]$AppConfigs[$i]
37+
})
38+
}
39+
40+
$SelectedTenants = @($Request.Body.selectedTenants | ForEach-Object {
41+
[PSCustomObject]@{
42+
defaultDomainName = $_.defaultDomainName
43+
customerId = $_.customerId
44+
}
45+
})
46+
47+
$OverrideAssignTo = $Request.Body.AssignTo
48+
$OverrideCustomGroup = $Request.Body.customGroup
49+
50+
$Results = foreach ($App in $Apps) {
51+
try {
52+
$Config = $App.config
53+
if ($Config -is [string]) {
54+
$Config = $Config | ConvertFrom-Json -Depth 100
55+
}
56+
57+
$AppType = "$($App.appType ?? $App.AppType)"
58+
59+
$RequestBody = $Config | ConvertTo-Json -Depth 100 | ConvertFrom-Json -Depth 100
60+
$RequestBody | Add-Member -NotePropertyName 'selectedTenants' -NotePropertyValue $SelectedTenants -Force
61+
$RequestBody | Add-Member -NotePropertyName 'tenantFilter' -NotePropertyValue 'allTenants' -Force
62+
63+
if ($OverrideAssignTo) {
64+
$RequestBody | Add-Member -NotePropertyName 'AssignTo' -NotePropertyValue $OverrideAssignTo -Force
65+
if ($OverrideAssignTo -eq 'customGroup' -and $OverrideCustomGroup) {
66+
$RequestBody | Add-Member -NotePropertyName 'CustomGroup' -NotePropertyValue $OverrideCustomGroup -Force
67+
}
68+
}
69+
70+
$MockRequest = [PSCustomObject]@{
71+
Body = $RequestBody
72+
Headers = $Headers
73+
Params = @{ CIPPEndpoint = $APIName }
74+
Query = @{}
75+
}
76+
77+
$HandlerResult = switch ($AppType) {
78+
'StoreApp' { Invoke-AddStoreApp -Request $MockRequest -TriggerMetadata $null }
79+
'chocolateyApp' { Invoke-AddChocoApp -Request $MockRequest -TriggerMetadata $null }
80+
'officeApp' { Invoke-AddOfficeApp -Request $MockRequest -TriggerMetadata $null }
81+
'win32ScriptApp' { Invoke-AddWin32ScriptApp -Request $MockRequest -TriggerMetadata $null }
82+
'mspApp' { Invoke-AddMSPApp -Request $MockRequest -TriggerMetadata $null }
83+
default { throw "Unknown app type: $AppType" }
84+
}
85+
86+
if ($HandlerResult.Body.Results) {
87+
$HandlerResult.Body.Results
88+
} elseif ($HandlerResult.Body) {
89+
$HandlerResult.Body
90+
} else {
91+
"Queued '$($App.appName)'"
92+
}
93+
} catch {
94+
$ErrorMessage = Get-CippException -Exception $_
95+
"Failed '$($App.appName)': $($ErrorMessage.NormalizedMessage)"
96+
Write-LogMessage -headers $Headers -API $APIName -message "Failed to deploy app '$($App.appName)' from template: $($ErrorMessage.NormalizedMessage)" -Sev 'Error' -LogData $ErrorMessage
97+
}
98+
}
99+
100+
$StatusCode = [HttpStatusCode]::OK
101+
} catch {
102+
$ErrorMessage = Get-CippException -Exception $_
103+
$Results = "Failed to deploy app template: $($ErrorMessage.NormalizedMessage)"
104+
Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage
105+
$StatusCode = [HttpStatusCode]::InternalServerError
106+
}
107+
108+
return ([HttpResponseContext]@{
109+
StatusCode = $StatusCode
110+
Body = @{ Results = @($Results) }
111+
})
112+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
function Invoke-ListAppTemplates {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint,AnyTenant
5+
.ROLE
6+
Endpoint.Application.Read
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$Table = Get-CippTable -tablename 'templates'
12+
$Filter = "PartitionKey eq 'AppTemplate'"
13+
$RawTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter
14+
15+
$Templates = foreach ($Template in $RawTemplates) {
16+
try {
17+
$JSONData = $Template.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue
18+
$Apps = if ($JSONData.Apps) { @($JSONData.Apps) } else { @() }
19+
[PSCustomObject]@{
20+
displayName = $JSONData.Displayname
21+
description = $JSONData.Description
22+
appCount = $Apps.Count
23+
appTypes = @($Apps | ForEach-Object { $_.appType } | Sort-Object -Unique)
24+
appNames = @($Apps | ForEach-Object { $_.appName })
25+
Apps = $Apps
26+
GUID = $Template.RowKey
27+
}
28+
} catch {}
29+
}
30+
31+
$Templates = @($Templates | Sort-Object -Property displayName)
32+
33+
if ($Request.Query.ID) {
34+
$Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID
35+
}
36+
37+
return ([HttpResponseContext]@{
38+
StatusCode = [HttpStatusCode]::OK
39+
Body = ConvertTo-Json -Depth 100 -InputObject @($Templates)
40+
})
41+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
function Invoke-RemoveAppTemplate {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint,AnyTenant
5+
.ROLE
6+
Endpoint.Application.ReadWrite
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$APIName = $Request.Params.CIPPEndpoint
12+
$Headers = $Request.Headers
13+
14+
try {
15+
$ID = $Request.Body.ID ?? $Request.Query.ID
16+
if (!$ID) { throw 'No template ID provided' }
17+
18+
$Table = Get-CippTable -tablename 'templates'
19+
$Filter = "PartitionKey eq 'AppTemplate' and RowKey eq '$ID'"
20+
$Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter
21+
if ($Entity) {
22+
Remove-AzDataTableEntity @Table -Entity $Entity
23+
$Result = 'Successfully removed app template'
24+
Write-LogMessage -headers $Headers -API $APIName -message "Removed app template $ID" -Sev 'Info'
25+
} else {
26+
$Result = 'Template not found'
27+
}
28+
$StatusCode = [HttpStatusCode]::OK
29+
} catch {
30+
$ErrorMessage = Get-CippException -Exception $_
31+
$Result = "Failed to remove app template: $($ErrorMessage.NormalizedMessage)"
32+
Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage
33+
$StatusCode = [HttpStatusCode]::InternalServerError
34+
}
35+
36+
return ([HttpResponseContext]@{
37+
StatusCode = $StatusCode
38+
Body = @{ Results = $Result }
39+
})
40+
}

0 commit comments

Comments
 (0)