Skip to content

Commit 7033bae

Browse files
authored
Merge pull request #770 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents a08aff0 + 646ed28 commit 7033bae

File tree

14 files changed

+700
-252
lines changed

14 files changed

+700
-252
lines changed

Modules/CIPPCore/Public/Clear-CIPPImmutableId.ps1

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,69 @@ function Clear-CIPPImmutableId {
33
param (
44
$TenantFilter,
55
$UserID,
6+
$Username, # Optional - used for better logging and scheduling messages
7+
$User, # Optional - if provided, will check sync status and schedule if needed
68
$Headers,
79
$APIName = 'Clear Immutable ID'
810
)
911

1012
try {
13+
# If User object is provided, check if we need to schedule instead of clearing immediately
14+
if ($User) {
15+
# User has ImmutableID but is not synced from on-premises - safe to clear immediately
16+
if ($User.onPremisesSyncEnabled -ne $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) {
17+
$DisplayName = $Username ?? $UserID
18+
Write-LogMessage -Message "User $DisplayName has an ImmutableID set but is not synced from on-premises. Proceeding to clear the ImmutableID." -TenantFilter $TenantFilter -Severity 'Warning' -APIName $APIName -headers $Headers
19+
# Continue to clear below
20+
}
21+
# User is synced from on-premises - must schedule for after deletion
22+
elseif ($User.onPremisesSyncEnabled -eq $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) {
23+
$DisplayName = $Username ?? $UserID
24+
Write-LogMessage -Message "User $DisplayName is synced from on-premises. Scheduling an Immutable ID clear for when the user account has been soft deleted." -TenantFilter $TenantFilter -Severity 'Warning' -APIName $APIName -headers $Headers
25+
26+
$ScheduledTask = @{
27+
TenantFilter = $TenantFilter
28+
Name = "Clear Immutable ID: $DisplayName"
29+
Command = @{ value = 'Clear-CIPPImmutableID' }
30+
Parameters = [pscustomobject]@{
31+
UserID = $UserID
32+
TenantFilter = $TenantFilter
33+
APIName = $APIName
34+
}
35+
Trigger = @{
36+
Type = 'DeltaQuery'
37+
DeltaResource = 'users'
38+
ResourceFilter = @($UserID)
39+
EventType = 'deleted'
40+
UseConditions = $false
41+
ExecutePerResource = $true
42+
ExecutionMode = 'once'
43+
}
44+
ScheduledTime = [int64](([datetime]::UtcNow).AddMinutes(5) - (Get-Date '1/1/1970')).TotalSeconds
45+
Recurrence = '15m'
46+
PostExecution = @{
47+
Webhook = $false
48+
Email = $false
49+
PSA = $false
50+
}
51+
}
52+
Add-CIPPScheduledTask -Task $ScheduledTask -hidden $false -DisallowDuplicateName $true
53+
return 'Scheduled Immutable ID clear task for when the user account is no longer synced in the on-premises directory.'
54+
}
55+
# User has no ImmutableID or is already clear
56+
else {
57+
$DisplayName = $Username ?? $UserID
58+
$Result = "User $DisplayName does not have an ImmutableID set or it is already cleared."
59+
Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Info -tenant $TenantFilter
60+
return $Result
61+
}
62+
}
63+
64+
# Perform the actual clear operation
1165
try {
12-
$User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -ErrorAction SilentlyContinue
66+
$UserObj = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -ErrorAction SilentlyContinue
1367
} catch {
68+
# User might be deleted, try to restore it
1469
$DeletedUser = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directory/deletedItems/$UserID" -tenantid $TenantFilter
1570
if ($DeletedUser.id) {
1671
# Restore deleted user object
@@ -22,12 +77,14 @@ function Clear-CIPPImmutableId {
2277
$Body = [pscustomobject]@{ onPremisesImmutableId = $null }
2378
$Body = ConvertTo-Json -InputObject $Body -Depth 5 -Compress
2479
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -type PATCH -body $Body
25-
$Result = "Successfully cleared immutable ID for user $UserID"
80+
$DisplayName = $Username ?? $UserID
81+
$Result = "Successfully cleared immutable ID for user $DisplayName"
2682
Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Info -tenant $TenantFilter
2783
return $Result
2884
} catch {
2985
$ErrorMessage = Get-CippException -Exception $_
30-
$Result = "Failed to clear immutable ID for $($UserID). Error: $($ErrorMessage.NormalizedError)"
86+
$DisplayName = $Username ?? $UserID
87+
$Result = "Failed to clear immutable ID for $DisplayName. Error: $($ErrorMessage.NormalizedError)"
3188
Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Error -tenant $TenantFilter -LogData $ErrorMessage
3289
throw $Result
3390
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
function Push-CIPPOffboardingComplete {
2+
<#
3+
.SYNOPSIS
4+
Post-execution handler for offboarding orchestration completion
5+
6+
.DESCRIPTION
7+
Updates the scheduled task state when offboarding completes
8+
9+
.FUNCTIONALITY
10+
Entrypoint
11+
#>
12+
[CmdletBinding()]
13+
param($Item)
14+
15+
$TaskInfo = $Item.Parameters.TaskInfo
16+
$TenantFilter = $Item.Parameters.TenantFilter
17+
$Username = $Item.Parameters.Username
18+
$Results = $Item.Results # Results come from orchestrator, not Parameters
19+
20+
try {
21+
Write-Information "Completing offboarding orchestration for $Username in tenant $TenantFilter"
22+
Write-Information "Raw results from orchestrator: $($Results | ConvertTo-Json -Depth 10)"
23+
24+
# Flatten nested arrays from orchestrator results
25+
# Activity functions may return arrays like [result, "status message"]
26+
$FlattenedResults = @(
27+
foreach ($BatchResult in $Results) {
28+
if ($BatchResult -is [array] -and $BatchResult.Count -gt 0) {
29+
Write-Information "Result is array with $($BatchResult.Count) elements, extracting elements"
30+
# Output all elements from the array
31+
foreach ($element in $BatchResult) {
32+
if ($null -ne $element -and $element -ne '') {
33+
$element
34+
}
35+
}
36+
} elseif ($null -ne $BatchResult -and $BatchResult -ne '') {
37+
# Single item - output it
38+
$BatchResult
39+
}
40+
}
41+
)
42+
43+
# Process results in the same way as Push-ExecScheduledCommand
44+
if ($FlattenedResults.Count -eq 0) {
45+
$ProcessedResults = "Offboarding completed successfully for $Username"
46+
} else {
47+
Write-Information "Processing $($FlattenedResults.Count) flattened results: $($FlattenedResults | ConvertTo-Json -Depth 10)"
48+
49+
# Normalize results format
50+
if ($FlattenedResults -is [string]) {
51+
$ProcessedResults = @{ Results = $FlattenedResults }
52+
} elseif ($FlattenedResults -is [array]) {
53+
# Filter and process string or resultText items
54+
$StringResults = $FlattenedResults | Where-Object { $_ -is [string] -or $_.resultText -is [string] }
55+
if ($StringResults) {
56+
$ProcessedResults = $StringResults | ForEach-Object {
57+
$Message = if ($_ -is [string]) { $_ } else { $_.resultText }
58+
@{ Results = $Message }
59+
}
60+
} else {
61+
# Keep structured results as-is
62+
$ProcessedResults = $FlattenedResults
63+
}
64+
} else {
65+
$ProcessedResults = $FlattenedResults
66+
}
67+
}
68+
69+
Write-Information "Results after processing: $($ProcessedResults | ConvertTo-Json -Depth 10)"
70+
71+
# Prepare results for storage
72+
if ($ProcessedResults -is [string]) {
73+
$StoredResults = $ProcessedResults
74+
} else {
75+
$ProcessedResults = $ProcessedResults | Select-Object * -ExcludeProperty RowKey, PartitionKey
76+
$StoredResults = $ProcessedResults | ConvertTo-Json -Compress -Depth 20 | Out-String
77+
}
78+
79+
if ($TaskInfo) {
80+
# Update scheduled task to completed state
81+
$Table = Get-CippTable -tablename 'ScheduledTasks'
82+
$currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
83+
84+
# Check if results are too large and need separate storage
85+
if ($StoredResults.Length -gt 64000) {
86+
Write-Information 'Results exceed 64KB limit. Storing in ScheduledTaskResults table.'
87+
$TaskResultsTable = Get-CippTable -tablename 'ScheduledTaskResults'
88+
$TaskResults = @{
89+
PartitionKey = $TaskInfo.RowKey
90+
RowKey = $TenantFilter
91+
Results = [string](ConvertTo-Json -Compress -Depth 20 $ProcessedResults)
92+
}
93+
$null = Add-CIPPAzDataTableEntity @TaskResultsTable -Entity $TaskResults -Force
94+
$StoredResults = @{ Results = 'Offboarding completed, details are available in the More Info pane' } | ConvertTo-Json -Compress
95+
}
96+
97+
$null = Update-AzDataTableEntity -Force @Table -Entity @{
98+
PartitionKey = $TaskInfo.PartitionKey
99+
RowKey = $TaskInfo.RowKey
100+
Results = "$StoredResults"
101+
ExecutedTime = "$currentUnixTime"
102+
TaskState = 'Completed'
103+
}
104+
105+
Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed successfully for $Username" -sev Info
106+
107+
# Send post-execution alerts if configured
108+
if ($TaskInfo.PostExecution -and $ProcessedResults) {
109+
Send-CIPPScheduledTaskAlert -Results $ProcessedResults -TaskInfo $TaskInfo -TenantFilter $TenantFilter
110+
}
111+
}
112+
113+
return "Offboarding completed for $Username"
114+
115+
} catch {
116+
$ErrorMsg = "Failed to complete offboarding for $Username : $($_.Exception.Message)"
117+
Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message $ErrorMsg -sev Error
118+
throw $ErrorMsg
119+
}
120+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
function Push-CIPPOffboardingTask {
2+
<#
3+
.SYNOPSIS
4+
Generic wrapper to execute individual offboarding task cmdlets
5+
6+
.DESCRIPTION
7+
Executes the specified cmdlet with the provided parameters as part of user offboarding
8+
9+
.FUNCTIONALITY
10+
Entrypoint
11+
#>
12+
[CmdletBinding()]
13+
param($Item)
14+
15+
$Cmdlet = $Item.Cmdlet
16+
$Parameters = $Item.Parameters | ConvertTo-Json -Depth 5 | ConvertFrom-Json -AsHashtable
17+
18+
try {
19+
Write-Information "Executing offboarding cmdlet: $Cmdlet"
20+
21+
# Check if cmdlet exists
22+
$CmdletInfo = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue
23+
if (-not $CmdletInfo) {
24+
throw "Cmdlet $Cmdlet does not exist"
25+
}
26+
27+
# Execute the cmdlet with splatting
28+
$Result = & $Cmdlet @Parameters
29+
30+
Write-Information "Completed $Cmdlet successfully"
31+
return $Result
32+
33+
} catch {
34+
$ErrorMsg = "Failed to execute $Cmdlet : $($_.Exception.Message)"
35+
Write-Information $ErrorMsg
36+
return $ErrorMsg
37+
}
38+
}

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ function Push-ExecScheduledCommand {
77
$item = $Item | ConvertTo-Json -Depth 100 | ConvertFrom-Json
88
Write-Information "We are going to be running a scheduled task: $($Item.TaskInfo | ConvertTo-Json -Depth 10)"
99

10+
# Define orchestrator-based commands that handle their own post-execution and state updates
11+
$OrchestratorBasedCommands = @('Invoke-CIPPOffboardingJob')
12+
1013
# Initialize AsyncLocal storage for thread-safe per-invocation context
1114
if (-not $script:CippScheduledTaskIdStorage) {
1215
$script:CippScheduledTaskIdStorage = [System.Threading.AsyncLocal[string]]::new()
@@ -225,6 +228,12 @@ function Push-ExecScheduledCommand {
225228
try {
226229
if (-not $Trigger.ExecutePerResource) {
227230
try {
231+
# For orchestrator-based commands, add TaskInfo to enable post-execution updates
232+
if ($Item.Command -eq 'Invoke-CIPPOffboardingJob') {
233+
Write-Information 'Adding TaskInfo to command parameters for orchestrator-based offboarding'
234+
$commandParameters['TaskInfo'] = $task
235+
}
236+
228237
Write-Information "Starting task: $($Item.Command) for tenant: $Tenant with parameters: $($commandParameters | ConvertTo-Json)"
229238
$results = & $Item.Command @commandParameters
230239
} catch {
@@ -310,43 +319,24 @@ function Push-ExecScheduledCommand {
310319
}
311320
Write-Information 'Sending task results to target. Updating the task state.'
312321

313-
if ($Results) {
314-
$TableDesign = '<style>table.adaptiveTable{border:1px solid currentColor;background-color:transparent;width:100%;text-align:left;border-collapse:collapse;opacity:0.9}table.adaptiveTable td,table.adaptiveTable th{border:1px solid currentColor;padding:8px 6px;opacity:0.8}table.adaptiveTable tbody td{font-size:13px}table.adaptiveTable tr:nth-child(even){background-color:rgba(128,128,128,0.1)}table.adaptiveTable thead{background-color:rgba(128,128,128,0.2);border-bottom:2px solid currentColor}table.adaptiveTable thead th{font-size:15px;font-weight:700;border-left:1px solid currentColor}table.adaptiveTable thead th:first-child{border-left:none}table.adaptiveTable tfoot{font-size:14px;font-weight:700;background-color:rgba(128,128,128,0.1);border-top:2px solid currentColor}table.adaptiveTable tfoot td{font-size:14px}@media (prefers-color-scheme: dark){table.adaptiveTable{opacity:0.95}table.adaptiveTable tr:nth-child(even){background-color:rgba(255,255,255,0.05)}table.adaptiveTable thead{background-color:rgba(255,255,255,0.1)}table.adaptiveTable tfoot{background-color:rgba(255,255,255,0.05)}}</style>'
315-
$FinalResults = if ($results -is [array] -and $results[0] -is [string]) { $Results | ConvertTo-Html -Fragment -Property @{ l = 'Text'; e = { $_ } } } else { $Results | ConvertTo-Html -Fragment }
316-
$HTML = $FinalResults -replace '<table>', "This alert is for tenant $Tenant. <br /><br /> $TableDesign<table class=adaptiveTable>" | Out-String
317-
318-
# Add alert comment if available
319-
if ($task.AlertComment) {
320-
if ($task.AlertComment -match '%resultcount%') {
321-
$resultCount = if ($Results -is [array]) { $Results.Count } else { 1 }
322-
$task.AlertComment = $task.AlertComment -replace '%resultcount%', "$resultCount"
323-
}
324-
$task.AlertComment = Get-CIPPTextReplacement -Text $task.AlertComment -TenantFilter $Tenant
325-
$HTML += "<div style='background-color: transparent; border-left: 4px solid #007bff; padding: 15px; margin: 15px 0;'><h4 style='margin-top: 0; color: #007bff;'>Alert Information</h4><p style='margin-bottom: 0;'>$($task.AlertComment)</p></div>"
326-
}
327-
328-
$title = "$TaskType - $Tenant - $($task.Name)$(if ($task.Reference) { " - Reference: $($task.Reference)" })"
329-
Write-Information 'Scheduler: Sending the results to the target.'
330-
Write-Information "The content of results is: $Results"
331-
switch -wildcard ($task.PostExecution) {
332-
'*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $Tenant }
333-
'*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $Tenant }
334-
'*webhook*' {
335-
$Webhook = [PSCustomObject]@{
336-
'tenantId' = $TenantInfo.customerId
337-
'Tenant' = $Tenant
338-
'TaskInfo' = $Item.TaskInfo
339-
'Results' = $Results
340-
'AlertComment' = $task.AlertComment
341-
}
342-
Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $Tenant -JSONContent $($Webhook | ConvertTo-Json -Depth 20)
343-
}
344-
}
322+
# For orchestrator-based commands, skip post-execution alerts as they will be handled by the orchestrator's post-execution function
323+
if ($Results -and $Item.Command -notin $OrchestratorBasedCommands) {
324+
Send-CIPPScheduledTaskAlert -Results $Results -TaskInfo $task -TenantFilter $Tenant -TaskType $TaskType
345325
}
346326
Write-Information 'Sent the results to the target. Updating the task state.'
347327

348328
try {
349-
if ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once' -or $Trigger.ExecutionMode -eq 'once') {
329+
# For orchestrator-based commands, skip task state update as it will be handled by post-execution
330+
if ($Item.Command -in $OrchestratorBasedCommands) {
331+
Write-Information "Command $($Item.Command) is orchestrator-based. Skipping task state update - will be handled by post-execution."
332+
# Update task state to 'Running' to indicate orchestration is in progress
333+
Update-AzDataTableEntity -Force @Table -Entity @{
334+
PartitionKey = $task.PartitionKey
335+
RowKey = $task.RowKey
336+
Results = 'Orchestration in progress'
337+
TaskState = 'Processing'
338+
}
339+
} elseif ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once' -or $Trigger.ExecutionMode -eq 'once') {
350340
Write-Information 'Recurrence empty or 0. Task is not recurring. Setting task state to completed.'
351341
Update-AzDataTableEntity -Force @Table -Entity @{
352342
PartitionKey = $task.PartitionKey

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
Function Invoke-AddChocoApp {
1+
function Invoke-AddChocoApp {
22
<#
33
.FUNCTIONALITY
4-
Entrypoint
4+
Entrypoint,AnyTenant
55
.ROLE
66
Endpoint.Application.ReadWrite
77
#>
@@ -30,7 +30,9 @@ Function Invoke-AddChocoApp {
3030
$intuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib"
3131
$intuneBody.detectionRules[0].fileOrFolderName = "$($ChocoApp.PackageName)"
3232

33-
$Tenants = $Request.Body.selectedTenants.defaultDomainName
33+
$AllowedTenants = Test-CIPPAccess -Request $Request -TenantList
34+
$Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName
35+
3436
$Results = foreach ($Tenant in $Tenants) {
3537
try {
3638
# Apply CIPP text replacement for tenant-specific variables

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
function Invoke-AddMSPApp {
22
<#
33
.FUNCTIONALITY
4-
Entrypoint
4+
Entrypoint,AnyTenant
55
.ROLE
66
Endpoint.Application.ReadWrite
77
#>
@@ -17,7 +17,8 @@ function Invoke-AddMSPApp {
1717
$intuneBody = Get-Content "AddMSPApp\$($RMMApp.RMMName.value).app.json" | ConvertFrom-Json
1818
$intuneBody.displayName = $RMMApp.DisplayName
1919

20-
$Tenants = $Request.Body.selectedTenants
20+
$AllowedTenants = Test-CIPPAccess -Request $Request -TenantList
21+
$Tenants = $Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }
2122
$Results = foreach ($Tenant in $Tenants) {
2223
$InstallParams = [PSCustomObject]$RMMApp.params
2324
switch ($RMMApp.RMMName.value) {

0 commit comments

Comments
 (0)