From 1db0ebe10ad1c6669d6ec944d42d3db9413445a0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jan 2024 09:20:06 -0500 Subject: [PATCH 1/8] Tenant Onboarding Include initial CPV push function Improve checks around CPV Add logging --- .../Push-ExecOnboardTenantQueue.ps1 | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 index 687af6d40b87..de79e6070d36 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 @@ -9,6 +9,7 @@ Function Push-ExecOnboardTenantQueue { $DateFormat = '%Y-%m-%d %H:%M:%S' $Id = $QueueItem.id #Write-Host ($QueueItem.Roles | ConvertTo-Json) + $Start = Get-Date $Logs = [System.Collections.Generic.List[object]]::new() $OnboardTable = Get-CIPPTable -TableName 'TenantOnboarding' $TenantOnboarding = Get-CIPPAzDataTableEntity @OnboardTable -Filter "RowKey eq '$Id'" @@ -50,7 +51,7 @@ Function Push-ExecOnboardTenantQueue { $Relationship = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$Id" $x++ Start-Sleep -Seconds 30 - } while ($Relationship.status -ne 'active' -and $x -lt 4) + } while ($Relationship.status -ne 'active' -and $x -lt 6) if ($Relationship.status -eq 'active') { $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'GDAP Invite Accepted' }) @@ -239,22 +240,50 @@ Function Push-ExecOnboardTenantQueue { } if ($OnboardingSteps.Step3.Status -eq 'succeeded') { - $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Refreshing CPV permissions' }) + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Setting up CPV consent' }) $OnboardingSteps.Step4.Status = 'running' - $OnboardingSteps.Step4.Message = 'Refreshing CPV permissions' + $OnboardingSteps.Step4.Message = 'Setting up CPV consent' $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop - try { - Remove-CIPPCache -tenantsOnly $true - } catch {} + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Clearing tenant cache' }) + $y = 0 + do { + try { + Remove-CIPPCache -tenantsOnly $true + } catch {} + + $Tenant = Get-Tenants | Where-Object { $_.customerId -eq $Relationship.customer.tenantId } | Select-Object -First 1 + $y++ + Start-Sleep -Seconds 20 + } while (!$Tenant -and $y -le 4) - $Tenant = Get-Tenants | Where-Object { $_.customerId -eq $Relationship.customer.tenantId } | Select-Object -First 1 if ($Tenant) { - $y = 0 + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Tenant found in customer list' }) + try { + $CPVConsentParams = @{ + TenantFilter = $Tenant.defaultDomainName + } + Set-CIPPCPVConsent @CPVConsentParams + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Added initial CPV consent permissions' }) + } catch { + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'CPV Consent Failed' }) + $TenantOnboarding.Status = 'failed' + $OnboardingSteps.Step4.Status = 'failed' + $OnboardingSteps.Step4.Message = 'CPV Consent failed, check the App Registration in your partner tenant for missing admin consent.' + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) + $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) + Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + return + } $Refreshing = $true $CPVSuccess = $false + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Refreshing CPV permissions' }) + $OnboardingSteps.Step4.Message = 'Refreshing CPV permissions' + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) + $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) + Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop do { try { Add-CIPPApplicationPermission -RequiredResourceAccess 'CippDefaults' -ApplicationId $ENV:ApplicationID -tenantfilter $Tenant.defaultDomainName @@ -262,10 +291,9 @@ Function Push-ExecOnboardTenantQueue { $CPVSuccess = $true $Refreshing = $false } catch { - $y++ Start-Sleep -Seconds 30 } - } while ($Refreshing -and $y -lt 4) + } while ($Refreshing -and (Get-Date) -lt $Start.AddMinutes(8)) if ($CPVSuccess) { $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'CPV permissions refreshed' }) From 344802e26514739d3b97e4eda29e96560f0e052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Bentsen=20Kj=C3=A6rg=C3=A5rd=20=28KBK=29?= Date: Wed, 3 Jan 2024 15:30:29 +0100 Subject: [PATCH 2/8] Possible bugfix of TAP auth method not running --- Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 index e98e7faddd01..ad5ba8681907 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 @@ -6,7 +6,8 @@ function Invoke-CIPPStandardTAP { param($Tenant, $Settings) If ($Settings.remediate) { - Set-CIPPAuthenticationPolicy -Tenant $tenant -APIName 'Standards' -AuthenticationMethodId 'TemporaryAccessPass' -Enabled $true -TAPisUsableOnce $Settings.config + $TAPisUsableOnce = if ($Settings.config -match 'true') { $true } else { $false } + Set-CIPPAuthenticationPolicy -Tenant $tenant -APIName 'Standards' -AuthenticationMethodId 'TemporaryAccessPass' -Enabled $true -TAPisUsableOnce $TAPisUsableOnce } # This is ugly but done to avoid a second call to the Graph API From 5b8af7fd0b77dd3e91aba2beb748932aacd4fa8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 3 Jan 2024 19:20:25 +0100 Subject: [PATCH 3/8] Fix TAPisUsableOnce conversion bug that caused TAP standard to not run --- Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 | 5 +++-- Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index ecdb1bdfc904..b68de07e0301 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -9,7 +9,7 @@ function Set-CIPPAuthenticationPolicy { $TAPMaximumLifetime = 480, #minutes $TAPDefaultLifeTime = 60, #minutes $TAPDefaultLength = 8, #TAP password generated length in chars - [bool]$TAPisUsableOnce = $true, + $TAPisUsableOnce = $true, $APIName = 'Set Authentication Policy', $ExecutingUser ) @@ -62,11 +62,12 @@ function Set-CIPPAuthenticationPolicy { # Temporary Access Pass 'TemporaryAccessPass' { if ($State -eq 'enabled') { - $CurrentInfo.isUsableOnce = $TAPisUsableOnce + $CurrentInfo.isUsableOnce = [System.Convert]::ToBoolean($TAPisUsableOnce) $CurrentInfo.minimumLifetimeInMinutes = $TAPMinimumLifetime $CurrentInfo.maximumLifetimeInMinutes = $TAPMaximumLifetime $CurrentInfo.defaultLifetimeInMinutes = $TAPDefaultLifeTime $CurrentInfo.defaultLength = $TAPDefaultLength + $OptionalLogMessage = "with TAP isUsableOnce set to $TAPisUsableOnce" } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 index ad5ba8681907..e98e7faddd01 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 @@ -6,8 +6,7 @@ function Invoke-CIPPStandardTAP { param($Tenant, $Settings) If ($Settings.remediate) { - $TAPisUsableOnce = if ($Settings.config -match 'true') { $true } else { $false } - Set-CIPPAuthenticationPolicy -Tenant $tenant -APIName 'Standards' -AuthenticationMethodId 'TemporaryAccessPass' -Enabled $true -TAPisUsableOnce $TAPisUsableOnce + Set-CIPPAuthenticationPolicy -Tenant $tenant -APIName 'Standards' -AuthenticationMethodId 'TemporaryAccessPass' -Enabled $true -TAPisUsableOnce $Settings.config } # This is ugly but done to avoid a second call to the Graph API From f4ec253593a5848ac7229c830d66aeeb2e89b149 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jan 2024 07:18:28 -0500 Subject: [PATCH 4/8] Update Push-ExecOnboardTenantQueue.ps1 --- .../Entrypoints/Push-ExecOnboardTenantQueue.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 index de79e6070d36..14efb9887121 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 @@ -175,6 +175,10 @@ Function Push-ExecOnboardTenantQueue { $TenantOnboarding.Status = 'failed' $OnboardingSteps.Step3.Status = 'failed' $OnboardingSteps.Step3.Message = 'No matching roles found, check the relationship and try again.' + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) + $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) + Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + return } } @@ -193,6 +197,10 @@ Function Push-ExecOnboardTenantQueue { $TenantOnboarding.Status = 'failed' $OnboardingSteps.Step3.Status = 'failed' $OnboardingSteps.Step3.Message = 'Group mapping failed, check the log book for details.' + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) + $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) + Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + return } } elseif (!$GroupSuccess) { $TenantOnboarding.Status = 'failed' @@ -213,6 +221,11 @@ Function Push-ExecOnboardTenantQueue { } else { $OnboardingSteps.Step3.Message = 'Group check: Access assignments are still pending, try again later' $OnboardingSteps.Step3.Status = 'failed' + $TenantOnboarding.Status = 'failed' + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) + $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) + Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + return } } if ($QueueItem.AddMissingGroups -eq $true) { From 20ae8d25e56bc86bb9be0f4e371b8e160a9f36ee Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jan 2024 07:21:24 -0500 Subject: [PATCH 5/8] timing --- .../CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 index 14efb9887121..5dc8eee4b97b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 @@ -212,7 +212,7 @@ Function Push-ExecOnboardTenantQueue { do { $x++ $AccessAssignments = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$Id/accessAssignments" - Start-Sleep -Seconds 10 + Start-Sleep -Seconds 15 } while ($AccessAssignments.status -contains 'pending' -and $x -le 12) if ($AccessAssignments.status -notcontains 'pending') { From d1dc412de284df86d62a6a86284f177f17fdca3e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jan 2024 08:09:56 -0500 Subject: [PATCH 6/8] Update Push-ExecOnboardTenantQueue.ps1 --- .../Entrypoints/Push-ExecOnboardTenantQueue.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 index 5dc8eee4b97b..1c507e0ee57f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Push-ExecOnboardTenantQueue.ps1 @@ -18,6 +18,14 @@ Function Push-ExecOnboardTenantQueue { $OnboardingSteps = $TenantOnboarding.OnboardingSteps | ConvertFrom-Json $OnboardingSteps.Step1.Status = 'running' $OnboardingSteps.Step1.Message = 'Checking GDAP invite status' + $OnboardingSteps.Step2.Status = 'pending' + $OnboardingSteps.Step2.Message = 'Waiting for Step 1' + $OnboardingSteps.Step3.Status = 'pending' + $OnboardingSteps.Step3.Message = 'Waiting for Step 2' + $OnboardingSteps.Step4.Status = 'pending' + $OnboardingSteps.Step4.Message = 'Waiting for Step 3' + $OnboardingSteps.Step5.Status = 'pending' + $OnboardingSteps.Step5.Message = 'Waiting for Step 4' $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) $TenantOnboarding.Status = 'running' $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress -AsArray) @@ -278,7 +286,10 @@ Function Push-ExecOnboardTenantQueue { $CPVConsentParams = @{ TenantFilter = $Tenant.defaultDomainName } - Set-CIPPCPVConsent @CPVConsentParams + $Consent = Set-CIPPCPVConsent @CPVConsentParams + if ($Consent -match 'Could not add our Service Principal to the client tenant') { + throw + } $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Added initial CPV consent permissions' }) } catch { $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'CPV Consent Failed' }) From 2573bb932eef9916ae4323dd5554a5058ccfe4bd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jan 2024 08:36:44 -0500 Subject: [PATCH 7/8] Prevent UpdatePermissions from breaking SAM --- UpdatePermissions/run.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UpdatePermissions/run.ps1 b/UpdatePermissions/run.ps1 index 03faa53ab09f..e4e73f4be9ed 100644 --- a/UpdatePermissions/run.ps1 +++ b/UpdatePermissions/run.ps1 @@ -1,7 +1,7 @@ # Input bindings are passed in via param block. param($Timer) -$Tenants = get-tenants -IncludeAll +$Tenants = get-tenants -IncludeAll | Where-Object { $_.customerId -ne $env:TenantId } foreach ($Row in $Tenants) { Push-OutputBinding -Name Msg -Value $row } \ No newline at end of file From c54e3415df2830def15be659bc8ad55e6bebde81 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jan 2024 15:18:35 +0100 Subject: [PATCH 8/8] bug fix --- .../Public/Add-CIPPDelegatedPermission.ps1 | 14 +++++++------- .../Entrypoints/Push-ExecAddMultiTenantApp.ps1 | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 7ff4452bba45..bc0dfe9046bf 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -5,9 +5,10 @@ function Add-CIPPDelegatedPermission { $ApplicationId, $Tenantfilter ) + Write-Host 'Adding Delegated Permissions' Set-Location (Get-Item $PSScriptRoot).FullName - - if ($RequiredResourceAccess -eq "CIPPDefaults") { + Write-Host "RequiredResourceAccess: $($RequiredResourceAccess | ConvertTo-Json -Depth 10)" + if ($RequiredResourceAccess -eq 'CIPPDefaults') { $RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess } $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json @@ -26,17 +27,16 @@ function Add-CIPPDelegatedPermission { if (!$OldScope) { $Createbody = @{ clientId = $ourSVCPrincipal.id - consentType = "AllPrincipals" + consentType = 'AllPrincipals' resourceId = $svcPrincipalId.id scope = $NewScope } | ConvertTo-Json -Compress - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" -tenantid $Tenantfilter -body $Createbody -type POST + $CreateRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants' -tenantid $Tenantfilter -body $Createbody -type POST $Results.add("Successfully added permissions for $($svcPrincipalId.displayName)") | Out-Null - } - else { + } else { $compare = Compare-Object -ReferenceObject $OldScope.scope.Split(' ') -DifferenceObject $NewScope.Split(' ') if (!$compare) { - $Results.add("All delegated permissions exist for $($svcPrincipalId.displayName)") | Out-Null + $Results.add("All delegated permissions exist for $($svcPrincipalId.displayName)") | Out-Null continue } $Patchbody = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-ExecAddMultiTenantApp.ps1 index 4c10f456584e..f567ef62932f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Push-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Push-ExecAddMultiTenantApp.ps1 @@ -1,6 +1,7 @@ function Push-ExecAddMultiTenantApp($QueueItem, $TriggerMetadata) { try { - Write-Host $Queueitem + $Queueitem = $QueueItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json + Write-Host "$($Queueitem | ConvertTo-Json -Depth 10)" $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Queueitem.Tenant if ($Queueitem.AppId -Notin $ServicePrincipalList.appId) { $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $queueitem.tenant -body "{ `"appId`": `"$($Queueitem.appId)`" }" @@ -8,8 +9,8 @@ function Push-ExecAddMultiTenantApp($QueueItem, $TriggerMetadata) { } else { Write-LogMessage -message "This app already exists in tenant $($Queueitem.Tenant). We're adding the required permissions." -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Info } - Add-CIPPApplicationPermission -RequiredResourceAccess [pscustomobject]$queueitem.applicationResourceAccess -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant - Add-CIPPDelegatedPermission -RequiredResourceAccess [pscustomobject]$queueitem.DelegateResourceAccess -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant + Add-CIPPApplicationPermission -RequiredResourceAccess ($queueitem.applicationResourceAccess) -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant + Add-CIPPDelegatedPermission -RequiredResourceAccess ($queueitem.DelegateResourceAccess) -ApplicationId $queueitem.AppId -Tenantfilter $Queueitem.Tenant } catch { Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error }