From 8ae4a14c1496806f9dcb7b81a2f1d32c4a25d3f2 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:18:36 +0800 Subject: [PATCH] Enhance mailbox permission modification and bulk request tracking Refactored Invoke-ExecModifyMBPerms.ps1 to support multiple mailbox request formats, improved error handling, and implemented precise result mapping using operation GUIDs for bulk permission changes. Updated New-ExoBulkRequest.ps1 to propagate and track operation GUIDs through batch requests, enabling accurate correlation of results and errors to specific permission operations. --- .../Invoke-ExecModifyMBPerms.ps1 | 358 ++++++++++++++---- .../Public/GraphHelper/New-ExoBulkRequest.ps1 | 85 ++++- 2 files changed, 355 insertions(+), 88 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 index cddc705a3556..ff03133f3675 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 @@ -11,188 +11,382 @@ Function Invoke-ExecModifyMBPerms { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' - $Username = $request.body.userID - $Tenantfilter = $request.body.tenantfilter - $Permissions = $request.body.permissions - - if ($username -eq $null) { exit } - - $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id + # Extract mailbox requests - handle all three formats + $MailboxRequests = $null $Results = [System.Collections.ArrayList]::new() - # Convert permissions to array format if it's an object with numeric keys - if ($Permissions -is [PSCustomObject]) { - if ($Permissions.PSObject.Properties.Name -match '^\d+$') { - $Permissions = $Permissions.PSObject.Properties.Value - } - else { - $Permissions = @($Permissions) - } + # Direct array format + if ($request.body -is [array]) { + $MailboxRequests = $request.body + } + # Bulk format with mailboxRequests property + elseif ($request.body.mailboxRequests) { + $MailboxRequests = $request.body.mailboxRequests + } + # Legacy single mailbox format + elseif ($request.body.userID -and $request.body.permissions) { + $MailboxRequests = @([PSCustomObject]@{ + userID = $request.body.userID + tenantFilter = $request.body.tenantFilter + permissions = $request.body.permissions + }) + } + + if (-not $MailboxRequests -or $MailboxRequests.Count -eq 0) { + Write-LogMessage -headers $Request.Headers -API $APINAME -message 'No mailbox requests provided' -Sev 'Error' + $body = [pscustomobject]@{'Results' = @("No mailbox requests provided") } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Body + }) + return } - foreach ($Permission in $Permissions) { - $PermissionLevels = $Permission.PermissionLevel - $Modification = $Permission.Modification - $AutoMap = if ($Permission.PSObject.Properties.Name -contains 'AutoMap') { $Permission.AutoMap } else { $true } + $TenantFilter = $Request.body.tenantFilter + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Processing permission changes for $($MailboxRequests.Count) mailboxes" -Sev 'Info' -tenant $TenantFilter - # Handle multiple permission levels separated by commas - if ($PermissionLevels -like "*,*") { - $PermissionLevelArray = $PermissionLevels -split ',' | ForEach-Object { $_.Trim() } - } - else { - $PermissionLevelArray = @($PermissionLevels.Trim()) + # Build cmdlet array for processing + $CmdletArray = [System.Collections.ArrayList]::new() + $CmdletMetadataArray = [System.Collections.ArrayList]::new() + $GuidToMetadataMap = @{} # Map GUIDs to our metadata + $UserLookupCache = @{} + + foreach ($MailboxRequest in $MailboxRequests) { + $Username = $MailboxRequest.userID + $Permissions = $MailboxRequest.permissions + + if ([string]::IsNullOrEmpty($Username)) { + $null = $Results.Add("Skipped mailbox with missing userID") + continue } - # Handle UserID as array of objects or single value - $TargetUsers = if ($Permission.UserID -is [array]) { - $Permission.UserID | ForEach-Object { $_.value } + # User lookup with caching for bulk operations + if (-not $UserLookupCache.ContainsKey($Username)) { + try { + $UserObject = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter + $UserLookupCache[$Username] = $UserObject.userPrincipalName + } + catch { + try { + $UserObject = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userPrincipalName eq '$Username'" -tenantid $TenantFilter + if ($UserObject.value -and $UserObject.value.Count -gt 0) { + $UserLookupCache[$Username] = $UserObject.value[0].userPrincipalName + } else { + throw "User not found" + } + } + catch { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Could not find user $($Username)" -Sev 'Error' -tenant $TenantFilter + $null = $Results.Add("Could not find user $($Username)") + continue + } + } } - else { - @($Permission.UserID) + $UserId = $UserLookupCache[$Username] + + # Convert permissions to array if needed + if ($Permissions -is [PSCustomObject]) { + if ($Permissions.PSObject.Properties.Name -match '^\d+$') { + $Permissions = $Permissions.PSObject.Properties.Value + } else { + $Permissions = @($Permissions) + } } - foreach ($TargetUser in $TargetUsers) { - foreach ($PermissionLevel in $PermissionLevelArray) { - try { + foreach ($Permission in $Permissions) { + $PermissionLevels = $Permission.PermissionLevel + $Modification = $Permission.Modification + $AutoMap = if ($Permission.PSObject.Properties.Name -contains 'AutoMap') { $Permission.AutoMap } else { $true } + + # Handle multiple permission levels + $PermissionLevelArray = if ($PermissionLevels -like "*,*") { + $PermissionLevels -split ',' | ForEach-Object { $_.Trim() } + } else { + @($PermissionLevels.Trim()) + } + + # Extract target users from UserID (handle array of objects or single values) + $TargetUsers = if ($Permission.UserID -is [array]) { + $Permission.UserID | ForEach-Object { + if ($_ -is [PSCustomObject] -and $_.value) { + $_.value + } else { + $_.ToString() + } + } + } else { + if ($Permission.UserID -is [PSCustomObject] -and $Permission.UserID.value) { + @($Permission.UserID.value) + } else { + @($Permission.UserID.ToString()) + } + } + + foreach ($TargetUser in $TargetUsers) { + foreach ($PermissionLevel in $PermissionLevelArray) { + + # Create cmdlet parameters based on permission type and action + $CmdletParams = @{} + $CmdletName = "" + $ExpectedResult = "" + switch ($PermissionLevel) { 'FullAccess' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('FullAccess') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Shared Mailbox permissions (FullAccess)") - } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{ - Identity = $userid + $ExpectedResult = "Removed $($TargetUser) from $($Username) FullAccess permissions" + } else { + $CmdletName = 'Add-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('FullAccess') automapping = $AutoMap Confirm = $false } - $null = $results.Add("Granted $($TargetUser) access to $($username) Mailbox (FullAccess) with automapping set to $($AutoMap)") + $ExpectedResult = "Granted $($TargetUser) FullAccess to $($Username) with automapping $($AutoMap)" } } 'SendAs' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-RecipientPermission' + $CmdletParams = @{ + Identity = $UserId Trustee = $TargetUser accessRights = @('SendAs') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) with Send As permissions") - } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{ - Identity = $userid + $ExpectedResult = "Removed $($TargetUser) SendAs permissions from $($Username)" + } else { + $CmdletName = 'Add-RecipientPermission' + $CmdletParams = @{ + Identity = $UserId Trustee = $TargetUser accessRights = @('SendAs') Confirm = $false } - $null = $results.Add("Granted $($TargetUser) access to $($username) with Send As permissions") + $ExpectedResult = "Granted $($TargetUser) SendAs permissions to $($Username)" } } 'SendOnBehalf' { + $CmdletName = 'Set-Mailbox' if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ - Identity = $userid + $CmdletParams = @{ + Identity = $UserId GrantSendonBehalfTo = @{ '@odata.type' = '#Exchange.GenericHashTable' remove = $TargetUser } Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Send on Behalf Permissions") - } - else { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{ - Identity = $userid + $ExpectedResult = "Removed $($TargetUser) SendOnBehalf permissions from $($Username)" + } else { + $CmdletParams = @{ + Identity = $UserId GrantSendonBehalfTo = @{ '@odata.type' = '#Exchange.GenericHashTable' add = $TargetUser } Confirm = $false } - $null = $results.Add("Granted $($TargetUser) access to $($username) with Send On Behalf Permissions") + $ExpectedResult = "Granted $($TargetUser) SendOnBehalf permissions to $($Username)" } } 'ReadPermission' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('ReadPermission') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + $ExpectedResult = "Removed $($TargetUser) ReadPermission from $($Username)" } } 'ExternalAccount' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('ExternalAccount') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + $ExpectedResult = "Removed $($TargetUser) ExternalAccount permissions from $($Username)" } } 'DeleteItem' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('DeleteItem') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + $ExpectedResult = "Removed $($TargetUser) DeleteItem permissions from $($Username)" } } 'ChangePermission' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('ChangePermission') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + $ExpectedResult = "Removed $($TargetUser) ChangePermission from $($Username)" } } 'ChangeOwner' { if ($Modification -eq 'Remove') { - $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{ - Identity = $userid + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId user = $TargetUser accessRights = @('ChangeOwner') Confirm = $false } - $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions") + $ExpectedResult = "Removed $($TargetUser) ChangeOwner permissions from $($Username)" } } } - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter - } - catch { - Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Error' -tenant $TenantFilter - $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)") + + if ($CmdletName) { + # Generate unique GUID for this operation + $OperationGuid = [Guid]::NewGuid().ToString() + + $CmdletObj = @{ + CmdletInput = @{ + CmdletName = $CmdletName + Parameters = $CmdletParams + } + OperationGuid = $OperationGuid # Add GUID to cmdlet object + } + + $CmdletMetadata = [PSCustomObject]@{ + ExpectedResult = $ExpectedResult + Mailbox = $Username + TargetUser = $TargetUser + Permission = $PermissionLevel + Action = $Modification + OperationGuid = $OperationGuid + } + + $null = $CmdletArray.Add($CmdletObj) + $null = $CmdletMetadataArray.Add($CmdletMetadata) + + # Map GUID to metadata for precise result mapping + $GuidToMetadataMap[$OperationGuid] = $CmdletMetadata + } } } } } - $body = [pscustomobject]@{'Results' = @($results) } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + if ($CmdletArray.Count -eq 0) { + Write-LogMessage -headers $Request.Headers -API $APINAME -message 'No valid cmdlets to process' -Sev 'Warning' -tenant $TenantFilter + $body = [pscustomobject]@{'Results' = @("No valid permission changes to process") } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $Body }) + return + } + + # Execute requests - use enhanced bulk processing with GUID mapping + if ($CmdletArray.Count -gt 1) { + # Use bulk processing with GUID tracking + try { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Executing bulk request with $($CmdletArray.Count) cmdlets" -Sev 'Info' -tenant $TenantFilter + $BulkResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($CmdletArray) -ReturnWithCommand $true + + # Process bulk results using GUID mapping + if ($BulkResults -is [hashtable] -and $BulkResults.Keys.Count -gt 0) { + foreach ($cmdletName in $BulkResults.Keys) { + foreach ($result in $BulkResults[$cmdletName]) { + $operationGuid = $result.OperationGuid + + if ($operationGuid -and $GuidToMetadataMap.ContainsKey($operationGuid)) { + $metadata = $GuidToMetadataMap[$operationGuid] + + if ($result.error) { + $ErrorMessage = try { (Get-CippException -Exception $result.error).NormalizedError } catch { $result.error } + $null = $Results.Add("Error processing $($metadata.Permission) for $($metadata.TargetUser) on $($metadata.Mailbox): $ErrorMessage") + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Error for operation $operationGuid`: $ErrorMessage" -Sev 'Error' -tenant $TenantFilter + } else { + $null = $Results.Add($metadata.ExpectedResult) + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Success for operation $operationGuid`: $($metadata.ExpectedResult)" -Sev 'Info' -tenant $TenantFilter + } + } else { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Could not map result to operation. GUID: $operationGuid, Available GUIDs: $($GuidToMetadataMap.Keys -join ', ')" -Sev 'Warning' -tenant $TenantFilter + + # Fallback for unmapped results + if ($result.error) { + $ErrorMessage = try { (Get-CippException -Exception $result.error).NormalizedError } catch { $result.error } + $null = $Results.Add("Error in $cmdletName`: $ErrorMessage") + } else { + $null = $Results.Add("Completed $cmdletName operation") + } + } + } + } + } else { + # If no results returned but no error thrown, assume all succeeded + foreach ($CmdletMetadata in $CmdletMetadataArray) { + if ($CmdletMetadata.ExpectedResult) { + $null = $Results.Add($CmdletMetadata.ExpectedResult) + } + } + } + + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Bulk request completed successfully" -Sev 'Info' -tenant $TenantFilter + } + catch { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Bulk request failed, using fallback: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter + + # Fallback to individual processing + for ($i = 0; $i -lt $CmdletArray.Count; $i++) { + $CmdletObj = $CmdletArray[$i] + $CmdletMetadata = $CmdletMetadataArray[$i] + try { + $null = New-ExoRequest -Anchor $CmdletMetadata.Mailbox -tenantid $TenantFilter -cmdlet $CmdletObj.CmdletInput.CmdletName -cmdParams $CmdletObj.CmdletInput.Parameters + $null = $Results.Add($CmdletMetadata.ExpectedResult) + } + catch { + $null = $Results.Add("Error processing $($CmdletMetadata.Permission) for $($CmdletMetadata.TargetUser) on $($CmdletMetadata.Mailbox): $($_.Exception.Message)") + } + } + } + } + else { + # Use individual processing for single operation + $CmdletObj = $CmdletArray[0] + $CmdletMetadata = $CmdletMetadataArray[0] + try { + $null = New-ExoRequest -Anchor $CmdletMetadata.Mailbox -tenantid $TenantFilter -cmdlet $CmdletObj.CmdletInput.CmdletName -cmdParams $CmdletObj.CmdletInput.Parameters + $null = $Results.Add($CmdletMetadata.ExpectedResult) + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Executed $($CmdletMetadata.Permission) permission modification" -Sev 'Info' -tenant $TenantFilter + } + catch { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Permission modification failed: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter + $null = $Results.Add("Error processing $($CmdletMetadata.Permission) for $($CmdletMetadata.TargetUser) on $($CmdletMetadata.Mailbox): $($_.Exception.Message)") + } + } + + $body = [pscustomobject]@{'Results' = @($Results) } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) } diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 index 38275011282b..553331e6a328 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 @@ -43,6 +43,7 @@ function New-ExoBulkRequest { # Initialize the ID to Cmdlet Name mapping $IdToCmdletName = @{} + $IdToOperationGuid = @{} # Track operation GUIDs when provided # Split the cmdletArray into batches of 10 $batches = [System.Collections.Generic.List[object]]::new() @@ -69,19 +70,32 @@ function New-ExoBulkRequest { $Headers['Accept'] = 'application/json; odata.metadata=minimal' $Headers['Accept-Encoding'] = 'gzip' - # Generate a unique ID for each request - $RequestId = [Guid]::NewGuid().ToString() + # Use provided OperationGuid if available, otherwise generate one + $RequestId = if ($cmd.OperationGuid) { + $cmd.OperationGuid + } else { + [Guid]::NewGuid().ToString() + } + + # Create clean cmdlet object for API (without OperationGuid) + $CleanCmd = @{ + CmdletInput = $cmd.CmdletInput + } + $BatchRequest = @{ url = $URL method = 'POST' - body = $cmd + body = $CleanCmd headers = $Headers.Clone() id = $RequestId } $BatchBodyObj['requests'] = $BatchBodyObj['requests'] + $BatchRequest - # Map the Request ID to the Cmdlet Name + # Map the Request ID to the Cmdlet Name and Operation GUID (if provided) $IdToCmdletName[$RequestId] = $cmd.CmdletInput.CmdletName + if ($cmd.OperationGuid) { + $IdToOperationGuid[$RequestId] = $cmd.OperationGuid + } } $BatchBodyJson = ConvertTo-Json -InputObject $BatchBodyObj -Depth 10 $BatchBodyJson = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $BatchBodyJson @@ -104,6 +118,7 @@ function New-ExoBulkRequest { foreach ($item in $ReturnedData) { $itemId = $item.id $CmdletName = $IdToCmdletName[$itemId] + $OperationGuid = $IdToOperationGuid[$itemId] # Will be $null if not provided $body = $item.body.PSObject.Copy() if ($body.'@adminapi.warnings') { @@ -115,20 +130,50 @@ function New-ExoBulkRequest { } else { $msg = [pscustomobject]@{ error = $body.error.message; target = $body.error.details.target } } + + # Add OperationGuid to error if it was provided + if ($OperationGuid) { + $msg | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + $body | Add-Member -MemberType NoteProperty -Name 'value' -Value $msg -Force + } else { + # Handle successful operations - add OperationGuid if provided + if ($body.value) { + # Add GUID to existing results if provided + if ($OperationGuid) { + if ($body.value -is [array]) { + foreach ($val in $body.value) { + $val | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + } else { + $body.value | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + } + } else { + # Create success indicators when GUID was provided (caller wants tracking) + if ($OperationGuid) { + $body | Add-Member -MemberType NoteProperty -Name 'value' -Value ([pscustomobject]@{ + Success = $true + OperationGuid = $OperationGuid + }) -Force + } + } } + $resultValues = $body.value foreach ($resultValue in $resultValues) { if (-not $FinalData.ContainsKey($CmdletName)) { $FinalData[$CmdletName] = [System.Collections.Generic.List[object]]::new() - $FinalData.$CmdletName.Add($resultValue) + $FinalData[$CmdletName].Add($resultValue) } else { - $FinalData.$CmdletName.Add($resultValue) + $FinalData[$CmdletName].Add($resultValue) } } } } else { $FinalData = foreach ($item in $ReturnedData) { + $OperationGuid = $IdToOperationGuid[$item.id] # Will be $null if not provided $body = $item.body.PSObject.Copy() if ($body.'@adminapi.warnings') { @@ -140,7 +185,35 @@ function New-ExoBulkRequest { } else { $msg = [pscustomobject]@{ error = $body.error.message; target = $body.error.details.target } } + + # Add OperationGuid to error if it was provided + if ($OperationGuid) { + $msg | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + $body | Add-Member -MemberType NoteProperty -Name 'value' -Value $msg -Force + } else { + # Handle successful operations + if ($body.value) { + # Add GUID to existing results if provided + if ($OperationGuid) { + if ($body.value -is [array]) { + foreach ($val in $body.value) { + $val | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + } else { + $body.value | Add-Member -MemberType NoteProperty -Name 'OperationGuid' -Value $OperationGuid -Force + } + } + } else { + # Create success indicators when GUID was provided (caller wants tracking) + if ($OperationGuid) { + $body | Add-Member -MemberType NoteProperty -Name 'value' -Value ([pscustomobject]@{ + Success = $true + OperationGuid = $OperationGuid + }) -Force + } + } } $body.value }