From 013fc565e5be10d4e68d240092a55cbb38f6f1b8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:19:16 +0800 Subject: [PATCH] feat(auth): surface "SSO Providers" in the Setup nav when SSO is enabled (ADR-0024 / cloud#551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sys_sso_provider admin object (register/list/delete external OIDC IdPs) had no nav entry — reachable only by direct URL. AuthPlugin now contributes an "SSO Providers" entry into the Setup app's Access Control group, gated on AuthManager.isSsoWired() (captures self-host OS_SSO_ENABLED + cloud per-env planAllowsSso via plugins.sso) so it never renders when the RP is off. Owning-plugin-contributes pattern (ADR-0029 K2), mirrors plugin-security. isSsoWired() made public for the gate. Verified: entry appears under Setup → Access Control when SSO on; the register form renders all fields (incl. discoveryEndpoint/scopes/mapping) and posts to the bridge (200). (The list view shows empty for now — a pre-existing platform behavior shared by all better-auth-managed objects, e.g. OAuth Applications / Identity Links already in nav read 0 via the data API; separate follow-up.) Co-Authored-By: Claude Opus 4.8 --- .changeset/adr-0024-sso-nav.md | 7 +++++ .../plugins/plugin-auth/src/auth-manager.ts | 5 +++- .../plugins/plugin-auth/src/auth-plugin.ts | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .changeset/adr-0024-sso-nav.md diff --git a/.changeset/adr-0024-sso-nav.md b/.changeset/adr-0024-sso-nav.md new file mode 100644 index 0000000000..717fc91ddd --- /dev/null +++ b/.changeset/adr-0024-sso-nav.md @@ -0,0 +1,7 @@ +--- +'@objectstack/plugin-auth': patch +--- + +Auth: surface "SSO Providers" in the Setup app nav when SSO is enabled (ADR-0024 / cloud#551) + +The `sys_sso_provider` admin object (register / list / delete external OIDC IdPs) had no navigation entry, so an admin could only reach it by direct URL. `AuthPlugin` now contributes an **"SSO Providers"** entry into the Setup app's **Access Control** group — but only when the external-IdP RP is wired (`AuthManager.isSsoWired()`, which captures both self-host `OS_SSO_ENABLED` and the cloud per-env `planAllowsSso` arriving via `plugins.sso`). Owning-plugin-contributes pattern (ADR-0029 K2), mirroring `plugin-security`. `isSsoWired()` is made public for this gate. diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 17c10c7d56..9a35abe26f 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -1970,8 +1970,11 @@ export class AuthManager { * in `buildPlugins()` (`ssoFromEnv ?? pluginConfig.sso ?? false`) so the * advertised capability can never disagree with the actual `/sign-in/sso` * route. `OS_SSO_ENABLED` (when set) wins over the config-file setting. + * Public so `AuthPlugin` can gate the Setup-nav "SSO Providers" entry on it + * (captures both self-host `OS_SSO_ENABLED` and the cloud per-env + * `planAllowsSso` config, since that arrives via `plugins.sso`). */ - private isSsoWired(): boolean { + public isSsoWired(): boolean { const ssoEnv = (globalThis as any)?.process?.env?.OS_SSO_ENABLED; const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === 'true' : undefined; return ssoFromEnv ?? (this.config.plugins as any)?.sso ?? false; diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index b2d8cfcef2..9a03c9f7df 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -200,6 +200,34 @@ export class AuthPlugin implements Plugin { dashboards: [SystemOverviewDashboard], // ADR-0021 — datasets backing the System Overview dashboard's widgets. datasets: SystemOverviewDatasets, + // ADR-0024 / cloud#551 — surface "SSO Providers" (sys_sso_provider) in the + // Setup app's Access Control group, but ONLY when the external-IdP RP is + // wired (self-host `OS_SSO_ENABLED`, or the cloud per-env `planAllowsSso` + // arriving via `plugins.sso`). Without the gate the entry would render an + // empty list + a "Register" button whose endpoint 404s when SSO is off. + // Owning-plugin-contributes pattern (ADR-0029 K2), mirroring plugin-security. + ...(this.authManager.isSsoWired() + ? { + navigationContributions: [ + { + app: 'setup', + group: 'group_access_control', + // After Roles/Permission-Sets (100) and Sharing (200), near API Keys (300). + priority: 250, + items: [ + { + id: 'nav_sso_providers', + type: 'object', + label: 'SSO Providers', + objectName: 'sys_sso_provider', + icon: 'log-in', + requiredPermissions: ['manage_platform_settings'], + }, + ], + }, + ], + } + : {}), }); ctx.logger.info('Auth Plugin initialized successfully');