From fc10ff8e247dcc7356de6b1985cd59f15fba79e8 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Sat, 20 Jun 2026 16:35:42 +0200 Subject: [PATCH] fix(sso): gate SSO UI on plugin presence, not managed-cloud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSO controller is built with forceFallback: !SSO_ENABLED || SSO_FORCE_FALLBACK, so ssoController.isUsingPlugin() is true only when SSO_ENABLED is on AND a real plugin is loaded — it already encodes 'env var on + plugin available'. The UI gates were leading on isManagedCloud instead, which is neither necessary nor the intended signal (per review). - login button: drop the isManagedCloud host check; gate on isUsingPlugin() AND the hasSso global flag. hasSso is a DB-backed runtime kill switch for the SSO login button (toggleable without a redeploy), so it's kept. isUsingPlugin() is cheap and short-circuits before the flag fetch, so self-hosted/no-plugin pays nothing. - SSO settings page: drop isManagedCloud from both the loader and action gates; key both on isUsingPlugin() so config mutations require an active plugin too. - settings sidebar: drop isManagedCloud from the SSO menu item so it's gated on isSsoUsingPlugin() alone, matching the loader/action and keeping the page discoverable via normal navigation on self-hosted. Addresses PR #3911 review. --- .../navigation/OrganizationSettingsSideMenu.tsx | 2 +- .../route.tsx | 13 ++++--------- apps/webapp/app/routes/login._index/route.tsx | 17 +++++++---------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx index e0ab1bf3b7..50b0a6d9c9 100644 --- a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -147,7 +147,7 @@ export function OrganizationSettingsSideMenu({ data-action="roles" /> )} - {isManagedCloud && isSsoUsingPlugin && ( + {isSsoUsingPlugin && ( { - const { isManagedCloud } = featuresForRequest(request); - // Gate on managed cloud AND the SSO plugin actually being loaded - // (SSO_ENABLED off → OSS fallback → isUsingPlugin false). Without - // this the page renders for every managed-cloud org even when SSO - // is disabled for the deployment. - if (!isManagedCloud || !(await ssoController.isUsingPlugin())) { + // True only when SSO_ENABLED is on and a real SSO plugin is loaded. + if (!(await ssoController.isUsingPlugin())) { throw new Response("Not Found", { status: 404 }); } @@ -175,8 +170,8 @@ export const action = dashboardAction( throw new Response("Not Found", { status: 404 }); } - const { isManagedCloud } = featuresForRequest(request); - if (!isManagedCloud) { + // Mirror the loader gate. + if (!(await ssoController.isUsingPlugin())) { throw new Response("Not Found", { status: 404 }); } await requireSsoEntitlement(orgId); diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index cba10d5f11..9c7df4f912 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -13,7 +13,6 @@ import { FormError } from "~/components/primitives/FormError"; import { Header1 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { TextLink } from "~/components/primitives/TextLink"; -import { featuresForRequest } from "~/features.server"; import { isGithubAuthSupported, isGoogleAuthSupported } from "~/services/auth.server"; import { getLastAuthMethod } from "~/services/lastAuthMethod.server"; import { commitSession, setRedirectTo } from "~/services/redirectTo.server"; @@ -86,16 +85,14 @@ export async function loader({ request }: LoaderFunctionArgs) { ? "Your SSO session expired. Please sign in again." : null; - const { isManagedCloud } = featuresForRequest(request); - // /login is unauthenticated and high-traffic; don't pay the plugin - // resolution + flag fetch on self-hosted where the result is unused. + // SSO login requires both an active plugin (SSO_ENABLED on + a real plugin loaded) + // and the hasSso global flag — a DB-backed runtime kill switch that disables the + // SSO login button without a redeploy. isUsingPlugin() is cheap and short-circuits + // before the flag fetch, so self-hosted/no-plugin deployments pay nothing. let showSsoAuth = false; - if (isManagedCloud) { - const [pluginActive, globalFlags] = await Promise.all([ - ssoController.isUsingPlugin(), - getGlobalFlags(), - ]); - showSsoAuth = pluginActive && (globalFlags as Record).hasSso === true; + if (await ssoController.isUsingPlugin()) { + const globalFlags = await getGlobalFlags(); + showSsoAuth = (globalFlags as Record).hasSso === true; } if (redirectTo) {