diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecDeployAppTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecDeployAppTemplate.ps1 index 55ea43ce06514..b09f5f3e07d78 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecDeployAppTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecDeployAppTemplate.ps1 @@ -51,7 +51,12 @@ function Invoke-ExecDeployAppTemplate { try { $Config = $App.config if ($Config -is [string]) { - $Config = $Config | ConvertFrom-Json -Depth 100 + # Parse case-sensitive to survive templates carrying both 'applicationName' + # and 'ApplicationName', then collapse them via a case-insensitive dictionary. + $Parsed = $Config | ConvertFrom-Json -Depth 100 -AsHashtable + $Config = [ordered]@{} + foreach ($Key in $Parsed.Keys) { $Config[$Key] = $Parsed[$Key] } + $Config = [PSCustomObject]$Config } $AppType = "$($App.appType ?? $App.AppType)" diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index 75cd4fc383475..3a8fada8861a2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -123,13 +123,15 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $BlockedDomainsMatches = $true } 'AllowSpecificExternal' { - $AllowedDomainsMatches = -not (Compare-Object -ReferenceObject $AllowedDomainsAsAList -DifferenceObject $CurrentAllowedDomains) + # Both lists are already Sort-Object'd; compare as joined strings. Avoids Compare-Object, + # whose parameter binder coerces an empty array @() to $null and then throws. + $AllowedDomainsMatches = (@($AllowedDomainsAsAList) -join ',') -eq (@($CurrentAllowedDomains) -join ',') $BlockedDomainsMatches = (!$CurrentBlockedDomains -or @($CurrentBlockedDomains).Count -eq 0) } 'BlockSpecificExternal' { # Allowed should be AllowAllKnownDomains, blocked domains already parsed above $AllowedDomainsMatches = $IsCurrentAllowAllKnownDomains - $BlockedDomainsMatches = -not (Compare-Object -ReferenceObject $BlockedDomains -DifferenceObject $CurrentBlockedDomains) + $BlockedDomainsMatches = (@($BlockedDomains) -join ',') -eq (@($CurrentBlockedDomains) -join ',') } } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 index 95e854007533b..941ee634dc825 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 @@ -18,7 +18,7 @@ function Invoke-NinjaOneDocumentTemplate { } else { $DocumentTemplate = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/document-templates/$($ID)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 } - + $MatchedCount = ($DocumentTemplate | Measure-Object).count if ($MatchedCount -eq 1) { # Matched a single document template @@ -26,13 +26,12 @@ function Invoke-NinjaOneDocumentTemplate { } elseif ($MatchedCount -eq 0) { # Create a new Document Template $Body = $Template | ConvertTo-Json -Depth 100 - Write-Host "Ninja Body: $body" $NinjaDocumentTemplate = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/document-templates/" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -Body $Body).content | ConvertFrom-Json -Depth 100 } else { - # Matched multiple templates. Should be impossible but lets check anyway :D - Throw 'Multiple Documents Matched the Provided Criteria' + $NinjaDocumentTemplate = $DocumentTemplate | Sort-Object { [int64]$_.id } | Select-Object -First 1 + Write-Warning "Multiple NinjaOne document templates named '$($Template.name)' found ($MatchedCount). Using the oldest (id $($NinjaDocumentTemplate.id)). Remove the duplicate template(s) in NinjaOne." } return $NinjaDocumentTemplate -} \ No newline at end of file +} diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 0a79c0e349a2d..ae74102230165 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -746,14 +746,16 @@ function Invoke-NinjaOneTenantSync { $UsersFilter = "PartitionKey eq '$($Customer.CustomerId)'" - [System.Collections.Generic.List[PSCustomObject]]$ParsedUsers = Get-CIPPAzDataTableEntity @UsersTable -Filter $UsersFilter - if (($ParsedUsers | Measure-Object).count -eq 0) { - [System.Collections.Generic.List[PSCustomObject]]$ParsedUsers = @() + + [System.Collections.Generic.List[PSCustomObject]]$StaleParsedUsers = Get-CIPPAzDataTableEntity @UsersTable -Filter $UsersFilter + if (($StaleParsedUsers | Measure-Object).count -gt 0) { + Remove-AzDataTableEntity -Force @UsersTable -Entity ($StaleParsedUsers | Select-Object PartitionKey, RowKey) } + [System.Collections.Generic.List[PSCustomObject]]$ParsedUsers = @() - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCache = Get-CIPPAzDataTableEntity @UsersUpdateTable -Filter $UsersFilter - if (($NinjaUserCache | Measure-Object).count -eq 0) { - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCache = @() + [System.Collections.Generic.List[PSCustomObject]]$StaleUserUpdates = Get-CIPPAzDataTableEntity @UsersUpdateTable -Filter $UsersFilter + if (($StaleUserUpdates | Measure-Object).count -gt 0) { + Remove-AzDataTableEntity -Force @UsersUpdateTable -Entity ($StaleUserUpdates | Select-Object PartitionKey, RowKey) } [System.Collections.Generic.List[PSCustomObject]]$UsersMap = Get-CIPPAzDataTableEntity @UsersMapTable -Filter $UsersFilter @@ -761,15 +763,8 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$UsersMap = @() } - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserUpdates = $NinjaUserCache | Where-Object { $_.action -eq 'Update' } - if (($NinjaUserUpdates | Measure-Object).count -eq 0) { - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserUpdates = @() - } - - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCreation = $NinjaUserCache | Where-Object { $_.action -eq 'Create' } - if (($NinjaUserCreation | Measure-Object).count -eq 0) { - [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCreation = @() - } + [System.Collections.Generic.List[PSCustomObject]]$NinjaUserUpdates = @() + [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCreation = @() foreach ($user in $SyncUsers | Where-Object { $_.id -notin $ParsedUsers.RowKey }) { @@ -1219,7 +1214,8 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$NinjaUserCreation = @() } } catch { - Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne user document creation failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaUserCreation | Measure-Object).count) user(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } try { @@ -1231,7 +1227,8 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$NinjaUserUpdates = @() } } catch { - Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne user document update failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaUserUpdates | Measure-Object).count) user(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } @@ -1294,7 +1291,8 @@ function Invoke-NinjaOneTenantSync { } } catch { - Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne user document creation failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaUserCreation | Measure-Object).count) user(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } try { @@ -1305,7 +1303,8 @@ function Invoke-NinjaOneTenantSync { Remove-AzDataTableEntity -Force @UsersUpdateTable -Entity $NinjaUserUpdates } } catch { - Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne user document update failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaUserUpdates | Measure-Object).count) user(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } ### Relationship Mapping @@ -1483,7 +1482,8 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$CreatedLicenses = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaLicenseCreation | ConvertTo-Json -Depth 100 -AsArray) -EA Stop).content | ConvertFrom-Json -Depth 100 } } catch { - Write-Information "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne license document creation failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaLicenseCreation | Measure-Object).count) license(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } try { @@ -1494,7 +1494,8 @@ function Invoke-NinjaOneTenantSync { Write-Information 'Completed Update' } } catch { - Write-Information "Bulk Update Errored, but may have been successful as only 1 record with an issue could have been the cause: $_" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -tenant $Customer.defaultDomainName -API 'NinjaOneSync' -message "NinjaOne license document update failed for $($Customer.displayName). NinjaOne rejects the whole batch if any single document is invalid, so all $(($NinjaLicenseUpdates | Measure-Object).count) license(s) in this batch were not written: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } [System.Collections.Generic.List[PSCustomObject]]$LicenseDocs = $CreatedLicenses + $UpdatedLicenses