Skip to content

Commit 786f247

Browse files
authored
Merge pull request #1031 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents ede6760 + 5e63541 commit 786f247

7 files changed

Lines changed: 278 additions & 119 deletions

File tree

Config/standards.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4930,7 +4930,7 @@
49304930
"name": "standards.SPFileRequests",
49314931
"cat": "SharePoint Standards",
49324932
"tag": [],
4933-
"helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.",
4933+
"helpText": "*Requires 'Sharing Level for OneDrive and SharePoint' to be set to Anyone* Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.",
49344934
"docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.",
49354935
"executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.",
49364936
"addedComponent": [

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function Push-ExecOnboardTenantQueue {
77
param($Item)
88
try {
99
$Id = $Item.id
10+
Write-Information "Onboarding: Starting for relationship $Id"
1011
$Start = Get-Date
1112
$Logs = [System.Collections.Generic.List[object]]::new()
1213
$OnboardTable = Get-CIPPTable -TableName 'TenantOnboarding'
@@ -61,6 +62,7 @@ function Push-ExecOnboardTenantQueue {
6162
$x++
6263
Start-Sleep -Seconds 30
6364
} while ($Relationship.status -ne 'active' -and $x -lt 6)
65+
Write-Information "Onboarding: Step1 poll completed - status=$($Relationship.status) attempts=$x"
6466

6567
if ($Relationship.status -eq 'active') {
6668
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'GDAP Invite Accepted' })
@@ -145,6 +147,7 @@ function Push-ExecOnboardTenantQueue {
145147
$TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress)
146148
$TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress)
147149
Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop
150+
Write-Information "Onboarding: Step2 completed - status=$($OnboardingSteps.Step2.Status) missingRoles=$($MissingRoles -join ',')"
148151
}
149152

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

333337
if ($OnboardingSteps.Step3.Status -eq 'succeeded') {
334338
# Check if the relationship was recently activated — Microsoft propagation may not have settled yet
335339
if ($Relationship.activatedDateTime) {
340+
$MinutesSinceActivation = $null
336341
try {
337342
$ActivatedTimeUtc = ([DateTimeOffset]$Relationship.activatedDateTime).UtcDateTime
338343
$MinutesSinceActivation = ([datetime]::UtcNow - $ActivatedTimeUtc).TotalMinutes
339-
if ($MinutesSinceActivation -lt 15) {
340-
$RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc)
341-
$RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds()
342-
$RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes
343-
$MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation)
344-
$RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes)
345-
$RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
346-
$Logs.Add([PSCustomObject]@{
347-
Date = (Get-Date).ToUniversalTime()
348-
Log = $RetryLogMessage
349-
})
350-
$RetryParams = [PSCustomObject]@{
351-
Item = [PSCustomObject]@{
352-
id = $Item.id
353-
Roles = $Item.Roles
354-
AutoMapRoles = $Item.AutoMapRoles
355-
IgnoreMissingRoles = $Item.IgnoreMissingRoles
356-
StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants
357-
}
358-
}
359-
$RetryTask = [PSCustomObject]@{
360-
Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)"
361-
Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' }
362-
Parameters = $RetryParams
363-
TenantFilter = $env:TenantID
364-
Recurrence = ''
365-
ScheduledTime = $RetryEpoch
344+
} catch {
345+
Write-Warning "Failed to parse activatedDateTime for relationship ${Id}: $($_.Exception.Message)"
346+
}
347+
Write-Information "Onboarding: activatedDateTime=$($Relationship.activatedDateTime) minutesSinceActivation=$MinutesSinceActivation"
348+
if ($null -ne $MinutesSinceActivation -and $MinutesSinceActivation -lt 15) {
349+
$RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc)
350+
$RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds()
351+
$RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes
352+
$MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation)
353+
$RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes)
354+
$RetryParams = [PSCustomObject]@{
355+
Item = [PSCustomObject]@{
356+
id = $Item.id
357+
Roles = $Item.Roles
358+
AutoMapRoles = $Item.AutoMapRoles
359+
IgnoreMissingRoles = $Item.IgnoreMissingRoles
360+
AddMissingGroups = $Item.AddMissingGroups
361+
StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants
366362
}
367-
$null = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch)
363+
}
364+
$RetryTask = [PSCustomObject]@{
365+
Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)"
366+
Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' }
367+
Parameters = $RetryParams
368+
TenantFilter = $env:TenantID
369+
Recurrence = ''
370+
ScheduledTime = $RetryEpoch
371+
}
372+
try {
373+
$ScheduleResult = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch)
374+
} catch {
375+
$ScheduleResult = "Error - $($_.Exception.Message)"
376+
}
377+
Write-Information "Onboarding: Add-CIPPScheduledTask result=$ScheduleResult"
378+
if ($ScheduleResult -match '^Error') {
379+
$FailMessage = "Failed to schedule onboarding retry for $($Relationship.customer.displayName): $ScheduleResult"
380+
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $FailMessage })
381+
Write-LogMessage -API 'Onboarding' -message $FailMessage -Sev 'Error'
382+
} else {
383+
$RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
384+
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $RetryLogMessage })
368385
$RetryMessage = "Rescheduled: GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Retrying in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle."
369386
$OnboardingSteps.Step4.Status = 'pending'
370387
$OnboardingSteps.Step4.Message = $RetryMessage
@@ -375,8 +392,6 @@ function Push-ExecOnboardTenantQueue {
375392
Write-LogMessage -API 'Onboarding' -message $RetryMessage -Sev 'Info'
376393
return
377394
}
378-
} catch {
379-
Write-Warning "Failed to check activatedDateTime for relationship ${Id}: $($_.Exception.Message)"
380395
}
381396
}
382397

@@ -445,6 +460,7 @@ function Push-ExecOnboardTenantQueue {
445460
}
446461
} while ($Refreshing -and (Get-Date) -lt $Start.AddMinutes(8))
447462

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

577+
Write-Information "Onboarding: Step5 API test completed - userCount=$UserCount apiError=$ApiError"
561578
if ($UserCount -gt 0) {
562579
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'API test successful' })
563580
$Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Onboarding complete' })

Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,66 @@ function Initialize-CIPPAuth {
159159
Write-Information "[Auth-Init] EasyAuth policy reconcile failed (non-fatal): $_"
160160
}
161161
}
162+
163+
# 3d. Reconcile API clients — ensure the EasyAuth config matches what the
164+
# "Save to Azure" action (Set-CippApiAuth) would produce for the currently
165+
# enabled API clients. That means BOTH lists must be checked, not just apps:
166+
# allowedApplications = SSO app + every enabled client
167+
# allowedAudiences = api://<id> for each of the above, plus the MCP host
168+
# URIs and bare client IDs for MCP-enabled clients
169+
# Config drifts when a client is enabled but "Save to Azure" was never run (or a
170+
# prior save partially applied — e.g. apps set but audiences missing), which
171+
# silently breaks API authentication for that client.
172+
if ($AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) {
173+
try {
174+
$ApiClientsTable = Get-CippTable -tablename 'ApiClients'
175+
$EnabledClients = @(Get-CIPPAzDataTableEntity @ApiClientsTable -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) })
176+
177+
if ($EnabledClients.Count -gt 0) {
178+
$EnabledClientIds = @($EnabledClients.RowKey)
179+
# MCPAllowed can round-trip as a bool or string; compare on string form (matches SaveToAzure)
180+
$McpClientIds = @($EnabledClients | Where-Object { "$($_.MCPAllowed)" -eq 'True' } | ForEach-Object { $_.RowKey })
181+
182+
$ApiAuthConfig = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction Stop
183+
$AADConfig = $ApiAuthConfig.identityProviders.azureActiveDirectory
184+
185+
# Desired state — keep in sync with Set-CippApiAuth's CIPPNG branch.
186+
$DesiredApps = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
187+
if ($AADConfig.registration.clientId) { [void]$DesiredApps.Add($AADConfig.registration.clientId) }
188+
foreach ($Id in $EnabledClientIds) { if (-not [string]::IsNullOrEmpty($Id)) { [void]$DesiredApps.Add($Id) } }
189+
190+
$DesiredAudiences = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
191+
foreach ($Id in $DesiredApps) { [void]$DesiredAudiences.Add("api://$Id") }
192+
if ($McpClientIds.Count -gt 0 -and $env:WEBSITE_HOSTNAME) {
193+
[void]$DesiredAudiences.Add("https://$($env:WEBSITE_HOSTNAME)")
194+
[void]$DesiredAudiences.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp")
195+
foreach ($McpId in $McpClientIds) { if (-not [string]::IsNullOrEmpty($McpId)) { [void]$DesiredAudiences.Add($McpId) } }
196+
}
197+
198+
# Current state from the platform-injected config
199+
$CurrentApps = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
200+
foreach ($App in @($AADConfig.validation.defaultAuthorizationPolicy.allowedApplications)) { if ($App) { [void]$CurrentApps.Add($App) } }
201+
$CurrentAudiences = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
202+
foreach ($Aud in @($AADConfig.validation.allowedAudiences)) { if ($Aud) { [void]$CurrentAudiences.Add($Aud) } }
203+
204+
# Drift when anything the endpoint would set is missing from the live config
205+
$AppsOk = $DesiredApps.IsSubsetOf($CurrentApps)
206+
$AudiencesOk = $DesiredAudiences.IsSubsetOf($CurrentAudiences)
207+
208+
if (-not $AppsOk -or -not $AudiencesOk) {
209+
$MissingApps = @($DesiredApps | Where-Object { -not $CurrentApps.Contains($_) })
210+
$MissingAudiences = @($DesiredAudiences | Where-Object { -not $CurrentAudiences.Contains($_) })
211+
Write-Information "[Auth-Init] API client drift detected — missing apps: [$($MissingApps -join ', ')]; missing audiences: [$($MissingAudiences -join ', ')] — reconciling EasyAuth"
212+
Set-CippApiAuth -TenantId $env:TenantID -ClientIds $EnabledClientIds -McpClientIds $McpClientIds
213+
Write-Information '[Auth-Init] EasyAuth allowedApplications + allowedAudiences reconciled with enabled API clients'
214+
} else {
215+
Write-Information "[Auth-Init] EasyAuth already matches $($EnabledClients.Count) enabled API client(s) — no update needed"
216+
}
217+
}
218+
} catch {
219+
Write-Information "[Auth-Init] API client reconcile failed (non-fatal): $_"
220+
}
221+
}
162222
} elseif ($AuthState.HasSAMCredentials) {
163223
# EasyAuth NOT configured but we DO have SAM credentials — try to auto-configure
164224
Write-Information '[Auth-Init] EasyAuth not configured but SAM credentials available — attempting auto-configuration'

0 commit comments

Comments
 (0)