Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions Config/AdditionalPermissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,5 @@
"type": "Scope"
}
]
},
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "CopilotPolicySettings.ReadWrite",
"type": "Scope"
},
{
"id": "CopilotSettings-LimitedMode.ReadWrite",
"type": "Scope"
},
{
"id": "CopilotPackages.Read.All",
"type": "Scope"
},
{
"id": "CopilotPackages.ReadWrite.All",
"type": "Scope"
}
]
}
]
4,492 changes: 3,141 additions & 1,351 deletions Config/standards.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function Push-ExecOnboardTenantQueue {
param($Item)
try {
$Id = $Item.id
Write-Information "Onboarding: Starting for relationship $Id"
$Start = Get-Date
$Logs = [System.Collections.Generic.List[object]]::new()
$OnboardTable = Get-CIPPTable -TableName 'TenantOnboarding'
Expand Down Expand Up @@ -61,6 +62,7 @@ function Push-ExecOnboardTenantQueue {
$x++
Start-Sleep -Seconds 30
} while ($Relationship.status -ne 'active' -and $x -lt 6)
Write-Information "Onboarding: Step1 poll completed - status=$($Relationship.status) attempts=$x"

if ($Relationship.status -eq 'active') {
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'GDAP Invite Accepted' })
Expand Down Expand Up @@ -118,9 +120,34 @@ function Push-ExecOnboardTenantQueue {
$OnboardingSteps.Step2.Status = 'succeeded'
$OnboardingSteps.Step2.Message = 'Your GDAP relationship has the required roles'
}

# Validate (and correct) that the mapped security groups still exist in the partner tenant before
# Step 3 tries to POST the access assignments - a missing group surfaces as a raw Graph
# "access container does not exist" error otherwise.
if ($OnboardingSteps.Step2.Status -ne 'failed' -and ($Item.Roles | Measure-Object).Count -gt 0) {
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Validating GDAP security group mappings against the partner tenant' })
$GroupCheck = Test-CIPPGDAPGroupMappings -RoleMappings $Item.Roles -CreateMissing:([bool]$Item.AddMissingGroups) -WriteBack
foreach ($GroupResult in $GroupCheck.Results) {
if ($GroupResult.Status -in @('Stale', 'Created', 'Missing')) {
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $GroupResult.Message })
}
}
# Use the corrected mappings for the remainder of the onboarding (group mapping, SAM membership, retries)
$Item.Roles = @($GroupCheck.RoleMappings)

if (-not $GroupCheck.Valid) {
$MissingGroupNames = ($GroupCheck.MissingGroups.Name | Sort-Object -Unique) -join ', '
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Missing GDAP security groups in the partner tenant: $MissingGroupNames" })
$TenantOnboarding.Status = 'failed'
$OnboardingSteps.Step2.Status = 'failed'
$OnboardingSteps.Step2.Message = "The following GDAP security groups are missing in the partner tenant, recreate the GDAP roles and retry: $MissingGroupNames"
}
}

$TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress)
$TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress)
Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop
Write-Information "Onboarding: Step2 completed - status=$($OnboardingSteps.Step2.Status) missingRoles=$($MissingRoles -join ',')"
}

if ($OnboardingSteps.Step2.Status -eq 'succeeded') {
Expand Down Expand Up @@ -304,43 +331,57 @@ function Push-ExecOnboardTenantQueue {
$TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress)
$TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress)
Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop
Write-Information "Onboarding: Step3 completed - status=$($OnboardingSteps.Step3.Status)"
}

if ($OnboardingSteps.Step3.Status -eq 'succeeded') {
# Check if the relationship was recently activated — Microsoft propagation may not have settled yet
if ($Relationship.activatedDateTime) {
$MinutesSinceActivation = $null
try {
$ActivatedTimeUtc = ([DateTimeOffset]$Relationship.activatedDateTime).UtcDateTime
$MinutesSinceActivation = ([datetime]::UtcNow - $ActivatedTimeUtc).TotalMinutes
if ($MinutesSinceActivation -lt 15) {
$RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc)
$RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds()
$RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes
$MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation)
$RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes)
$RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
$Logs.Add([PSCustomObject]@{
Date = (Get-Date).ToUniversalTime()
Log = $RetryLogMessage
})
$RetryParams = [PSCustomObject]@{
Item = [PSCustomObject]@{
id = $Item.id
Roles = $Item.Roles
AutoMapRoles = $Item.AutoMapRoles
IgnoreMissingRoles = $Item.IgnoreMissingRoles
StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants
}
}
$RetryTask = [PSCustomObject]@{
Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)"
Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' }
Parameters = $RetryParams
TenantFilter = $env:TenantID
Recurrence = ''
ScheduledTime = $RetryEpoch
} catch {
Write-Warning "Failed to parse activatedDateTime for relationship ${Id}: $($_.Exception.Message)"
}
Write-Information "Onboarding: activatedDateTime=$($Relationship.activatedDateTime) minutesSinceActivation=$MinutesSinceActivation"
if ($null -ne $MinutesSinceActivation -and $MinutesSinceActivation -lt 15) {
$RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc)
$RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds()
$RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes
$MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation)
$RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes)
$RetryParams = [PSCustomObject]@{
Item = [PSCustomObject]@{
id = $Item.id
Roles = $Item.Roles
AutoMapRoles = $Item.AutoMapRoles
IgnoreMissingRoles = $Item.IgnoreMissingRoles
AddMissingGroups = $Item.AddMissingGroups
StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants
}
$null = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch)
}
$RetryTask = [PSCustomObject]@{
Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)"
Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' }
Parameters = $RetryParams
TenantFilter = $env:TenantID
Recurrence = ''
ScheduledTime = $RetryEpoch
}
try {
$ScheduleResult = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch)
} catch {
$ScheduleResult = "Error - $($_.Exception.Message)"
}
Write-Information "Onboarding: Add-CIPPScheduledTask result=$ScheduleResult"
if ($ScheduleResult -match '^Error') {
$FailMessage = "Failed to schedule onboarding retry for $($Relationship.customer.displayName): $ScheduleResult"
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $FailMessage })
Write-LogMessage -API 'Onboarding' -message $FailMessage -Sev 'Error'
} else {
$RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $RetryLogMessage })
$RetryMessage = "Rescheduled: GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Retrying in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
$OnboardingSteps.Step4.Status = 'pending'
$OnboardingSteps.Step4.Message = $RetryMessage
Expand All @@ -351,8 +392,6 @@ function Push-ExecOnboardTenantQueue {
Write-LogMessage -API 'Onboarding' -message $RetryMessage -Sev 'Info'
return
}
} catch {
Write-Warning "Failed to check activatedDateTime for relationship ${Id}: $($_.Exception.Message)"
}
}

Expand Down Expand Up @@ -421,6 +460,7 @@ function Push-ExecOnboardTenantQueue {
}
} while ($Refreshing -and (Get-Date) -lt $Start.AddMinutes(8))

Write-Information "Onboarding: CPV refresh loop completed - success=$CPVSuccess lastError=$LastCPVError"
if ($CPVSuccess) {
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'CPV permissions refreshed' })
$OnboardingSteps.Step4.Status = 'succeeded'
Expand Down Expand Up @@ -534,6 +574,7 @@ function Push-ExecOnboardTenantQueue {
$ApiException = $_
}

Write-Information "Onboarding: Step5 API test completed - userCount=$UserCount apiError=$ApiError"
if ($UserCount -gt 0) {
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'API test successful' })
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Onboarding complete' })
Expand Down
88 changes: 71 additions & 17 deletions Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,103 @@ function Add-CIPPSSOAppSecret {
.SYNOPSIS
Creates a client secret on the CIPP-SSO app registration with retry.
.DESCRIPTION
Adds a new password credential to the given app object via Graph. Retries up to
MaxRetries times with backoff because Entra propagation can take a few seconds
after the app is freshly created or its app-management-policy exemption is set.
Throws on final failure so callers can persist Status=error + LastError.
Adds a new password credential to the given app object via Graph. Before adding the
secret it ensures the app is exempt from the tenant default app-management policy (so a
'passwordAddition' restriction can't block the secret) via Update-AppManagementPolicy,
and honours any 'passwordLifetime' restriction when building the credential body.
Retries up to MaxRetries times with backoff because Entra propagation can take a few
seconds after the app is freshly created or its app-management-policy exemption is set:
replication misses back off 3s, and credential-policy blocks back off min(30, 5*attempt)s
while the exemption propagates. Throws on final failure so callers can persist
Status=error + LastError.
.PARAMETER ObjectId
Graph object ID of the application (NOT the appId/clientId).
.PARAMETER AppId
AppId/clientId of the application, used to target the app-management-policy exemption.
Resolved from ObjectId when not supplied.
.PARAMETER DisplayName
Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'.
.PARAMETER MaxRetries
Number of secret-creation attempts before giving up. Defaults to 5.
Number of secret-creation attempts before giving up. Defaults to 6.
#>
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ObjectId,

[Parameter(Mandatory = $false)]
[string]$AppId,

[Parameter(Mandatory = $false)]
[string]$DisplayName = 'CIPP-SSO-Secret',

[Parameter(Mandatory = $false)]
[int]$MaxRetries = 5
[int]$MaxRetries = 6
)

# Update-AppManagementPolicy targets the app by appId/clientId; resolve it from the object id when not supplied.
if (-not $AppId) {
try {
$SSOApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId`?`$select=id,appId" -NoAuthCheck $true -AsApp $true
$AppId = $SSOApp.appId
} catch {
Write-Warning "[SSO-Secret] Failed to resolve appId for objectId $ObjectId : $($_.Exception.Message)"
}
}

# Ensure the app is exempt from any credential-addition restriction before adding the secret.
if ($AppId) {
try {
$PolicyUpdate = Update-AppManagementPolicy -ApplicationId $AppId
Write-Information "[SSO-Secret] App management policy: $($PolicyUpdate.PolicyAction)"
} catch {
Write-Information "[SSO-Secret] Failed to update app management policy: $($_.Exception.Message)"
}
}

# Honour the tenant password-lifetime restriction (if enforced) when building the credential body.
$AppManagementPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' -AsApp $true -NoAuthCheck $true
$PasswordExpirationPolicy = $AppManagementPolicy.applicationRestrictions.passwordcredentials |
Where-Object { $_.restrictionType -eq 'passwordLifetime' }
if (-not ($PasswordExpirationPolicy.state -eq 'disabled' -or $null -eq $PasswordExpirationPolicy.state)) {
$TimeToExpiration = [System.Xml.XmlConvert]::ToTimeSpan($PasswordExpirationPolicy.maxLifetime)
$ExpirationDate = (Get-Date).AddDays($TimeToExpiration.Days).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
$PasswordBody = "{`"passwordCredential`":{`"displayName`":`"$DisplayName`",`"endDateTime`":`"$ExpirationDate`"}}"
} else {
$PasswordBody = "{`"passwordCredential`":{`"displayName`":`"$DisplayName`"}}"
}

$SecretText = $null
$SecretAttempt = 0
$BackoffSchedule = @(2, 5, 10, 15, 30)
$LastException = $null

while ($SecretAttempt -lt $MaxRetries -and -not $SecretText) {
for ($Attempt = 1; $Attempt -le $MaxRetries; $Attempt++) {
try {
$PasswordBody = @{ passwordCredential = @{ displayName = $DisplayName } } | ConvertTo-Json -Compress
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -AsApp $true -NoAuthCheck $true -type POST -body $PasswordBody -maxRetries 3
$SecretText = $PasswordResult.secretText
Write-Information "[SSO-Secret] Client secret created on objectId $ObjectId"
break
} catch {
$SecretAttempt++
$LastException = $_
Write-Warning "[SSO-Secret] Secret creation attempt $SecretAttempt/$MaxRetries failed: $($_.Exception.Message)"
if ($SecretAttempt -lt $MaxRetries) {
$Delay = $BackoffSchedule[[Math]::Min($SecretAttempt - 1, $BackoffSchedule.Count - 1)]
Start-Sleep -Seconds $Delay
$ExceptionMessage = $_.Exception.Message
$IsNotReplicatedYet = $ExceptionMessage -match "Resource '.*' does not exist or one of its queried reference-property objects are not present"
$IsCredentialPolicyBlocked = $ExceptionMessage -match 'Credential type not allowed as per assigned policy'
Write-Warning "[SSO-Secret] Secret creation attempt $Attempt/$MaxRetries failed: $ExceptionMessage"

if ($IsNotReplicatedYet -and $Attempt -lt $MaxRetries) {
$DelaySeconds = 3
Write-Information "[SSO-Secret] Application object not yet replicated for addPassword (attempt $Attempt of $MaxRetries). Retrying in $DelaySeconds second(s)."
Start-Sleep -Seconds $DelaySeconds
continue
}

if ($IsCredentialPolicyBlocked -and $Attempt -lt $MaxRetries) {
$DelaySeconds = [Math]::Min(30, 5 * $Attempt)
Write-Information "[SSO-Secret] Credential policy still blocks addPassword (attempt $Attempt of $MaxRetries). Waiting for policy propagation and retrying in $DelaySeconds second(s)."
Start-Sleep -Seconds $DelaySeconds
continue
}

throw
}
}

Expand Down
Loading