-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Expand file tree
/
Copy pathInvoke-CIPPStandardAppDeploy.ps1
More file actions
305 lines (263 loc) · 18 KB
/
Invoke-CIPPStandardAppDeploy.ps1
File metadata and controls
305 lines (263 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
function Invoke-CIPPStandardAppDeploy {
<#
.FUNCTIONALITY
Internal
.COMPONENT
(APIName) AppDeploy
.SYNOPSIS
(Label) Deploy Application
.DESCRIPTION
(Helptext) Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.
(DocsDescription) Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.
.NOTES
CAT
Entra (AAD) Standards
TAG
EXECUTIVETEXT
Automatically deploys approved business applications across all company locations and users, ensuring consistent access to essential tools and maintaining standardized software configurations. This streamlines application management and reduces IT deployment overhead.
ADDEDCOMPONENT
{"type":"select","multiple":false,"creatable":false,"label":"App Approval Mode","name":"standards.AppDeploy.mode","options":[{"label":"Template","value":"template"},{"label":"Copy Permissions","value":"copy"}]}
{"type":"autoComplete","multiple":true,"creatable":false,"label":"Select Applications","name":"standards.AppDeploy.templateIds","api":{"url":"/api/ListAppApprovalTemplates","labelField":"TemplateName","valueField":"TemplateId","queryKey":"StdAppApprovalTemplateList","addedField":{"AppId":"AppId"}},"condition":{"field":"standards.AppDeploy.mode","compareType":"is","compareValue":"template"}}
{"type":"textField","name":"standards.AppDeploy.appids","label":"Application IDs, comma separated","condition":{"field":"standards.AppDeploy.mode","compareType":"isNot","compareValue":"template"}}
IMPACT
Low Impact
ADDEDDATE
2024-07-07
POWERSHELLEQUIVALENT
Portal or Graph API
RECOMMENDEDBY
UPDATECOMMENTBLOCK
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
.LINK
https://docs.cipp.app/user-documentation/tenant/standards/list-standards
#>
param($Tenant, $Settings)
Write-Information "Running AppDeploy standard for tenant $($Tenant)."
$AppsToAdd = $Settings.appids -split ','
$AppExists = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals'
$Mode = $Settings.mode ?? 'copy'
$ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' }
if ($Mode -eq 'template') {
# For template mode, we need to check each template individually
# since Gallery Templates and Enterprise Apps have different deployment methods
$AppsToAdd = @()
$Table = Get-CIPPTable -TableName 'templates'
$AppsToAdd = foreach ($TemplateId in $Settings.templateIds.value) {
$Template = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'AppApprovalTemplate' and RowKey eq '$TemplateId'"
if ($Template) {
$TemplateData = $Template.JSON | ConvertFrom-Json
# Default to EnterpriseApp for backward compatibility with older templates
$AppType = $TemplateData.AppType
if (-not $AppType) {
$AppType = 'EnterpriseApp'
}
# Return different identifiers based on app type for checking
if ($AppType -eq 'ApplicationManifest') {
# For Application Manifests, use display name for checking
$TemplateData.AppName
} elseif ($AppType -eq 'GalleryTemplate') {
# For Gallery Templates, use gallery template ID
$TemplateData.GalleryTemplateId
} else {
# For Enterprise Apps, use app ID
$TemplateData.AppId
}
}
}
}
# Check for missing apps based on template type
$MissingApps = [System.Collections.Generic.List[string]]::new()
if ($Mode -eq 'template') {
$Table = Get-CIPPTable -TableName 'templates'
foreach ($TemplateId in $Settings.templateIds.value) {
$Template = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'AppApprovalTemplate' and RowKey eq '$TemplateId'"
if ($Template) {
$TemplateData = $Template.JSON | ConvertFrom-Json
$AppType = $TemplateData.AppType ?? 'EnterpriseApp'
$IsAppMissing = $false
if ($AppType -eq 'ApplicationManifest') {
# For Application Manifests, check by display name
$IsAppMissing = $TemplateData.AppName -notin $AppExists.displayName
} elseif ($AppType -eq 'GalleryTemplate') {
# For Gallery Templates, check by application template ID
$IsAppMissing = $TemplateData.GalleryTemplateId -notin $AppExists.applicationTemplateId
} else {
# For Enterprise Apps, check by app ID
$IsAppMissing = $TemplateData.AppId -notin $AppExists.appId
}
if ($IsAppMissing) {
$MissingApps.Add($TemplateData.AppName ?? $TemplateData.AppId ?? $TemplateData.GalleryTemplateId)
}
}
}
} else {
# For copy mode, check by app ID as before
$MissingApps = foreach ($App in $AppsToAdd) {
if ($App -notin $AppExists.appId -and $App -notin $AppExists.applicationTemplateId) {
$App
}
}
}
$CurrentValue = if ($MissingApps.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingApps' = $MissingApps } }
if ($Settings.remediate -eq $true) {
$UpdateDB = $false
if ($Mode -eq 'copy') {
foreach ($App in $AppsToAdd) {
$App = $App.Trim()
if (!$App) {
continue
}
$Application = $AppExists | Where-Object -Property appId -EQ $App
try {
New-CIPPApplicationCopy -App $App -Tenant $Tenant
Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application $($Application.displayName) ($App) to $Tenant and updated it's permissions" -sev Info
$UpdateDB = $true
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to add app $($Application.displayName) ($App). Error: $ErrorMessage" -sev Error
}
}
} elseif ($Mode -eq 'template') {
$TemplateIds = $Settings.templateIds.value
# Get template data to determine deployment type for each template
$Table = Get-CIPPTable -TableName 'templates'
foreach ($TemplateId in $TemplateIds) {
try {
# Get the template data to determine if it's a Gallery Template or Enterprise App
$Template = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'AppApprovalTemplate' and RowKey eq '$TemplateId'"
if (!$Template) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Template $TemplateId not found" -sev Error
continue
}
$TemplateData = $Template.JSON | ConvertFrom-Json
# Default to EnterpriseApp for backward compatibility with older templates
$AppType = $TemplateData.AppType
if (-not $AppType) {
$AppType = 'EnterpriseApp'
}
if ($AppType -eq 'GalleryTemplate') {
# Handle Gallery Template deployment
Write-Information "Deploying Gallery Template $($TemplateData.AppName) to tenant $Tenant."
$GalleryTemplateId = $TemplateData.GalleryTemplateId
if (!$GalleryTemplateId) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Gallery Template ID not found in template data for $($TemplateData.TemplateName)" -sev Error
continue
}
# Check if the app already exists in the tenant
if ($TemplateData.GalleryTemplateId -in $AppExists.applicationTemplateId) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Gallery Template app $($TemplateData.AppName) already exists in tenant $Tenant" -sev Info
continue
}
# Instantiate the gallery template
$InstantiateBody = @{
displayName = $TemplateData.AppName
} | ConvertTo-Json -Depth 10
$InstantiateResult = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applicationTemplates/$GalleryTemplateId/instantiate" -type POST -tenantid $Tenant -body $InstantiateBody
if ($InstantiateResult.application.appId) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully deployed Gallery Template $($TemplateData.AppName) to tenant $Tenant. Application ID: $($InstantiateResult.application.appId)" -sev Info
New-CIPPApplicationCopy -App $InstantiateResult.application.appId -Tenant $Tenant
$UpdateDB = $true
} else {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Gallery Template deployment completed but application ID not returned for $($TemplateData.AppName) in tenant $Tenant" -sev Warning
}
} elseif ($AppType -eq 'ApplicationManifest') {
# Handle Application Manifest deployment
Write-Information "Deploying Application Manifest $($TemplateData.AppName) to tenant $Tenant."
$ApplicationManifest = $TemplateData.ApplicationManifest
if (!$ApplicationManifest) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Application Manifest not found in template data for $($TemplateData.TemplateName)" -sev Error
continue
}
# Check if an application with the same display name already exists
$ExistingApp = $AppExists | Where-Object { $_.displayName -eq $TemplateData.AppName }
if ($ExistingApp) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Application with name '$($TemplateData.AppName)' already exists in tenant $Tenant" -sev Info
# get existing application
$App = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications(appId='$($ExistingApp.appId)')" -tenantid $Tenant
# compare permissions
$ExistingPermissions = $App.requiredResourceAccess | ConvertTo-Json -Depth 10
$NewPermissions = $ApplicationManifest.requiredResourceAccess | ConvertTo-Json -Depth 10
if ($ExistingPermissions -ne $NewPermissions) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Updating permissions for existing application '$($TemplateData.AppName)' in tenant $Tenant" -sev Info
# Update permissions for existing application
$UpdateBody = @{
requiredResourceAccess = $ApplicationManifest.requiredResourceAccess
} | ConvertTo-Json -Depth 10
$null = New-GraphPostRequest -type PATCH -uri "https://graph.microsoft.com/beta/applications(appId='$($ExistingApp.appId)')" -tenantid $Tenant -body $UpdateBody
# consent new permissions
Add-CIPPDelegatedPermission -RequiredResourceAccess $ApplicationManifest.requiredResourceAccess -ApplicationId $ExistingApp.appId -Tenantfilter $Tenant
Add-CIPPApplicationPermission -RequiredResourceAccess $ApplicationManifest.requiredResourceAccess -ApplicationId $ExistingApp.appId -Tenantfilter $Tenant
}
continue
}
$PropertiesToRemove = @('appId', 'id', 'createdDateTime', 'publisherDomain', 'servicePrincipalLockConfiguration', 'identifierUris', 'applicationIdUris')
# Strip tenant-specific data that might cause conflicts
$CleanManifest = $ApplicationManifest | ConvertTo-Json -Depth 10 | ConvertFrom-Json
foreach ($Property in $PropertiesToRemove) {
$CleanManifest.PSObject.Properties.Remove($Property)
}
# Create the application from manifest
try {
$CreateBody = $CleanManifest | ConvertTo-Json -Depth 10
$CreatedApp = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/applications' -type POST -tenantid $Tenant -body $CreateBody
if ($CreatedApp.appId) {
# Create service principal for the application
$ServicePrincipalBody = @{
appId = $CreatedApp.appId
} | ConvertTo-Json
$ServicePrincipal = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body $ServicePrincipalBody
Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully deployed Application Manifest $($TemplateData.AppName) to tenant $Tenant. Application ID: $($CreatedApp.appId)" -sev Info
if ($CreatedApp.requiredResourceAccess) {
Add-CIPPDelegatedPermission -RequiredResourceAccess $CreatedApp.requiredResourceAccess -ApplicationId $CreatedApp.appId -Tenantfilter $Tenant
Add-CIPPApplicationPermission -RequiredResourceAccess $CreatedApp.requiredResourceAccess -ApplicationId $CreatedApp.appId -Tenantfilter $Tenant
}
$UpdateDB = $true
} else {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Application Manifest deployment failed - no application ID returned for $($TemplateData.AppName) in tenant $Tenant" -sev Error
}
} catch {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Error creating application from manifest in tenant $Tenant - $($_.Exception.Message)" -sev Error
}
} else {
# Handle Enterprise App deployment (existing logic)
$AppId = $TemplateData.AppId
if ($AppId -notin $AppExists.appId) {
Write-Information "Adding $AppId to tenant $Tenant."
$PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body "{ `"appId`": `"$AppId`" }"
Write-LogMessage -message "Added $AppId to tenant $Tenant" -tenant $Tenant -API 'Standards' -sev Info
}
# Apply permissions for Enterprise Apps
Add-CIPPApplicationPermission -TemplateId $TemplateId -TenantFilter $Tenant
Add-CIPPDelegatedPermission -TemplateId $TemplateId -TenantFilter $Tenant
Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application $($TemplateData.AppName) from Enterprise App template and updated its permissions" -sev Info
$UpdateDB = $true
}
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to deploy template $TemplateId. Error: $ErrorMessage" -sev Error
}
}
}
# Refresh service principals cache after remediation only if changes were made
if ($UpdateDB) {
try {
Set-CIPPDBCacheServicePrincipals -TenantFilter $Tenant
} catch {
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh service principals cache after remediation: $($_.Exception.Message)" -sev Warning
}
}
}
if ($Settings.alert) {
if ($MissingApps.Count -gt 0) {
Write-StandardsAlert -message "The following applications are not deployed: $($MissingApps -join ', ')" -object (@{ 'Missing Apps' = $MissingApps -join ',' }) -tenant $Tenant -standardName 'AppDeploy' -standardId $Settings.standardId
Write-LogMessage -API 'Standards' -tenant $tenant -message "The following applications are not deployed: $($MissingApps -join ', ')" -sev Info
} else {
Write-LogMessage -API 'Standards' -tenant $tenant -message 'All applications are deployed' -sev Info
}
}
if ($Settings.report -eq $true) {
$StateIsCorrect = $MissingApps.Count -eq 0 ? $true : @{ 'Missing Apps' = $MissingApps -join ',' }
Set-CIPPStandardsCompareField -FieldName 'standards.AppDeploy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant
Add-CIPPBPAField -FieldName 'AppDeploy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
}
}