From 784fd992401a25a51c71961c4fecfce4b4e920b3 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 31 Jul 2026 21:15:50 +0200 Subject: [PATCH] fix(web): consent verify rides AAD propagation; funnel banner actions + dark mode - Post-consent verification now POLLS the access-check (up to 5 attempts, 15s apart) instead of a single shot: right after a successful admin consent the app-only token often mints with an EMPTY roles claim for 30-90s (observed live: tenant 5ca2b350, GrantedRoleCount=0 at T+43s), which the single-shot probe misreported as "permission not granted" while the homing flip and consent were actually fine. The access-check invalidates and re-mints fresh per call, so the poll converges as soon as Microsoft has propagated the role; the consent spinner stays up meanwhile. Final "absent" message now names propagation as the likely cause instead of sounding fatal. - Funnel banner gains always-visible actions ("Grant consent for the new app" + "Detect existing access" on the autopilot gate, idempotent when already enabled): tenants whose validations are already ON had no consent button to click, and the per-toggle detect link under Corporate Identifier would enable that validation as a side effect. - Dark mode: funnel/flipped/legacy banners and the admins-only notice were light-palette-only and unreadable on dark. tsc clean, vitest 631/631, ESLint ratchet holds (115). Co-Authored-By: Claude Fable 5 --- .../app/settings/TenantConfigContext.tsx | 27 +++++++++--- .../sections/SectionAutopilotValidation.tsx | 43 ++++++++++++++----- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx index 314a8b39..3ac1bb27 100644 --- a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx @@ -1037,19 +1037,36 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode } const reconcileTrigger: ValidationTrigger = trigger === "corporate" ? "corporate" : trigger === "device-preparation" ? "device-preparation" : "autopilot"; - const probe = await probeAccessCheck(); + // AAD consent-propagation awareness: right after a successful admin consent the app-only + // token often mints with an EMPTY roles claim for 30-90s (observed live 2026-07-31, + // tenant 5ca2b350: token 43s post-consent with GrantedRoleCount=0) — a single-shot probe + // misclassified that window as "permission not granted" and showed a hard error while + // everything was actually fine. The access-check invalidates + re-mints fresh on every + // call, so a bounded poll converges as soon as Microsoft has propagated the role. The + // consent-in-progress spinner stays up for the duration (finally-block clears it). + let probe: AccessCheckOutcome = "absent"; + let attempts = 0; + for (attempts = 1; attempts <= 5; attempts++) { + if (attempts > 1) await new Promise((resolve) => setTimeout(resolve, 15000)); + probe = await probeAccessCheck(); + if (probe === "reconciled") break; + } + if (probe === "reconciled") { + if (attempts > 1) trackEvent("consent_verify_propagated", { trigger, attempts: String(attempts) }); const saved = await persistValidation(reconcileTrigger); if (saved) { setSuccessMessage(`${validationLabel(reconcileTrigger)}${validationEnabledSuffix(reconcileTrigger)}`); } } else if (probe === "transient") { - trackEvent("consent_verify_failed", { trigger, stage: "role-propagating" }); + trackEvent("consent_verify_failed", { trigger, stage: "role-propagating", attempts: String(attempts - 1) }); setError("Admin consent succeeded, but the required permission could not be confirmed yet (access is still propagating). Please retry in a moment — toggle the option again or use 'Detect existing access'."); } else { - // "absent" — consent went through but the role is genuinely not on the app in this tenant. - trackEvent("consent_verify_failed", { trigger, stage: "role-missing" }); - setError("Admin consent succeeded, but the required permission (DeviceManagementServiceConfig.Read.All) is not granted to the app in this tenant. Ensure it is included when granting consent, then try again."); + // "absent" after ~a minute of polling — either the role really is missing from the + // consent, or Microsoft's propagation is unusually slow. Keep the message actionable + // for both without sounding fatal. + trackEvent("consent_verify_failed", { trigger, stage: "role-missing", attempts: String(attempts - 1) }); + setError("Admin consent succeeded, but the permission (DeviceManagementServiceConfig.Read.All) has not shown up on the app in this tenant yet. This can simply be Microsoft still propagating the consent — wait a minute and use 'Detect existing access'. If it persists, re-run the consent and ensure the permission is included."); } router.replace("/settings/tenant/autopilot"); } catch (err) { diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionAutopilotValidation.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionAutopilotValidation.tsx index 68a60f82..5dd4a391 100644 --- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionAutopilotValidation.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionAutopilotValidation.tsx @@ -27,7 +27,7 @@ export function SectionAutopilotValidation() { // Operators do not see this section at all. if (!canEditConfig) { return ( -
+
This page is available to tenant administrators only.
); @@ -49,7 +49,7 @@ export function SectionAutopilotValidation() { <> {homingFlipped ? ( -
+

Your tenant now runs on the new Autopilot Monitor app registration. Current sessions keep working; every next sign-in uses the new app automatically. @@ -62,44 +62,67 @@ export function SectionAutopilotValidation() {

) : appHomingFunnelActive ? ( -
+
- + -
+

Please switch to the new Autopilot Monitor app registration

-

+

Your tenant still runs on the previous app registration. Granting consent below (or running "Detect existing access") approves the new app and switches your tenant over automatically — current sessions keep working, and new sign-ins use the new app. It's a one-time step and takes about a minute.

-

+

Note: granting admin consent requires an Entra ID account that can consent tenant-wide (e.g. Global Administrator or{" "} Privileged Role Administrator). The consented permission stays the same read-only Graph permission as before - (DeviceManagementServiceConfig.Read.All). + (DeviceManagementServiceConfig.Read.All).

See the documentation for details on the required permissions + {/* Always-visible funnel actions: a tenant whose validations are ALREADY enabled has + no consent button to click (the toggles are on) and the per-toggle "Detect + existing access" links only appear under DISABLED toggles — and would enable that + validation as a side effect. These two act on the existing autopilot gate + (idempotent when it is already on) and never touch the other toggles. */} +
+ + +
) : homedOnLegacyApp ? ( -
+
Autopilot device validation currently runs on the previous Autopilot Monitor app registration. It keeps working as-is — at your convenience, please re-consent to the new app registration; we will reach out with details as part of the migration.