From d6af213b5b001315b92f7ad6b0ccde073f4ddda0 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:47:43 +0800 Subject: [PATCH] fix(plugin-auth): ensureDefaultOrganization resolves the L4 config-derived owner when no grant row exists (#13514 follow-through) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #13514 (L4) retired the walled grant row: bootstrapPlatformAdmin writes no sys_user_permission_set row under walled postures, standing is config-derived at the one derivation site. The ADR-0081 D1 default-org bootstrap still resolved the admin BY THE ROW (admin_full_access set → oldest cross-tenant grant) — so on every walled deployment it dead-ended on `no_admin` forever. The open AuthPlugin never hits this (its runner is gated on non-walled postures), but the enterprise organizations package invokes this same helper under walled postures by design ("every WALLED posture keeps its existing owner: the enterprise organizations package" — the runner comment); cloud's EE guided-path suite caught the dead end on its pin bump (signup-membership-policy.e2e: "the platform admin never received their default-organization owner row"). The fallback asks the same public predicates the derivation site asks — resolvePlatformAdminEmails() + isConfiguredPlatformAdminEmail from core, the #11343 verified-email allow-list from types — oldest verified owner wins (the bootstrap's own tiebreak), and the grant row stays PRIMARY where it exists. Fail-closed in every other shape: no declared owner, unverified match, or no row all still answer `no_admin`. Four new pins in the package suite (16/16 green): the walled L4 shape resolves the owner; unverified stays no_admin; undeclared stays no_admin; a grant row wins over the config fallback. Gates: plugin-auth build + check-dts-emitted green, full-repo lint green, changeset (patch) present. Co-Authored-By: Claude Fable 5 --- .changeset/d1-config-derived-owner.md | 5 ++ .../src/ensure-default-organization.test.ts | 65 +++++++++++++++++++ .../src/ensure-default-organization.ts | 58 +++++++++++------ 3 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 .changeset/d1-config-derived-owner.md diff --git a/.changeset/d1-config-derived-owner.md b/.changeset/d1-config-derived-owner.md new file mode 100644 index 0000000000..573c12dcaf --- /dev/null +++ b/.changeset/d1-config-derived-owner.md @@ -0,0 +1,5 @@ +--- +'@objectstack/plugin-auth': patch +--- + +`ensureDefaultOrganization` resolves the platform admin through the L4 config-derived standing when no cross-tenant grant row exists (#13514 follow-through): the walled bootstrap mints no `sys_user_permission_set` row any more, so the ADR-0081 D1 default-org bootstrap dead-ended on `no_admin` forever on walled deployments — the enterprise organizations package invokes this same helper there. The fallback asks the same public predicates the derivation site asks (`resolvePlatformAdminEmails` + `isConfiguredPlatformAdminEmail` + the #11343 verified-email allow-list), oldest verified owner wins, and the grant row stays primary where it exists. diff --git a/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts b/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts index ffe62f36cd..16f8e0a41c 100644 --- a/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts +++ b/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts @@ -36,6 +36,71 @@ function makeQl(seed: Partial> = {}) { }; } +describe('the L4 config-derived owner fallback (#13514 follow-through)', () => { + // Under a walled posture the bootstrap mints NO grant row, so the grant + // lookup answers nothing — the declared VERIFIED owner must be the admin, + // resolved with the same public predicates the derivation site asks. + const OWNER = 'owner@walled.example'; + const withOwnerEnv = async (value: string | undefined, fn: () => Promise) => { + const prev = process.env.OS_PLATFORM_OWNER_EMAIL; + if (value === undefined) delete process.env.OS_PLATFORM_OWNER_EMAIL; + else process.env.OS_PLATFORM_OWNER_EMAIL = value; + try { await fn(); } finally { + if (prev === undefined) delete process.env.OS_PLATFORM_OWNER_EMAIL; + else process.env.OS_PLATFORM_OWNER_EMAIL = prev; + } + }; + + it('no grant row + declared VERIFIED owner ⇒ the owner gets the default org', async () => + withOwnerEnv(OWNER, async () => { + const ql = makeQl({ + sys_user_permission_set: [], + sys_user: [ + { id: 'u_other', email: 'bystander@walled.example', email_verified: true, created_at: '2026-01-01' }, + { id: 'u_owner', email: OWNER, email_verified: true, created_at: '2026-01-02' }, + ], + }); + const res = await ensureDefaultOrganization(ql); + expect(res.defaultOrgCreated).toBe(true); + expect(res.memberCreated).toBe(true); + expect(ql.tables.sys_member[0]).toMatchObject({ user_id: 'u_owner', role: 'owner' }); + })); + + it('an UNVERIFIED declared owner stays no_admin — fail closed, the #11343 allow-list holds', async () => + withOwnerEnv(OWNER, async () => { + const ql = makeQl({ + sys_user_permission_set: [], + sys_user: [{ id: 'u_owner', email: OWNER, email_verified: false, created_at: '2026-01-02' }], + }); + const res = await ensureDefaultOrganization(ql); + expect(res.defaultOrgCreated).toBe(false); + expect(res.reason).toBe('no_admin'); + expect(ql.tables.sys_member).toEqual([]); + })); + + it('no declared owner at all stays no_admin — nobody is invented', async () => + withOwnerEnv(undefined, async () => { + const ql = makeQl({ + sys_user_permission_set: [], + sys_user: [{ id: 'u_owner', email: OWNER, email_verified: true, created_at: '2026-01-02' }], + }); + const res = await ensureDefaultOrganization(ql); + expect(res.defaultOrgCreated).toBe(false); + expect(res.reason).toBe('no_admin'); + })); + + it('a grant row still WINS over the config fallback — the historical spelling stays primary', async () => + withOwnerEnv(OWNER, async () => { + const ql = makeQl({ + sys_user: [{ id: 'u_owner', email: OWNER, email_verified: true, created_at: '2026-01-02' }], + }); + const res = await ensureDefaultOrganization(ql); + expect(res.defaultOrgCreated).toBe(true); + // makeQl's default grant row names u1 — that row, not the config owner. + expect(ql.tables.sys_member[0]).toMatchObject({ user_id: 'u1', role: 'owner' }); + })); +}); + describe('ensureDefaultOrganization (plugin-auth home)', () => { it('creates the default org and binds the admin as owner', async () => { const ql = makeQl(); diff --git a/packages/plugins/plugin-auth/src/ensure-default-organization.ts b/packages/plugins/plugin-auth/src/ensure-default-organization.ts index 5d7bbaf4fd..f2d701be94 100644 --- a/packages/plugins/plugin-auth/src/ensure-default-organization.ts +++ b/packages/plugins/plugin-auth/src/ensure-default-organization.ts @@ -85,6 +85,9 @@ interface BootstrapLogger { * tests — survive it perfectly, which is why no suite would catch it. The * property-access call form below keeps the receiver. */ +import { resolvePlatformAdminEmails, isConfiguredPlatformAdminEmail } from '@objectstack/core'; +import { isEmailVerifiedUserRow } from '@objectstack/types'; + function logDurabilityFailure( logger: BootstrapLogger | undefined, message: string, @@ -164,29 +167,44 @@ export async function ensureDefaultOrganization( return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' }; } - // 1. Find the platform admin permission-set id. - const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1); - if (adminPs.length === 0 || !adminPs[0].id) { - return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' }; - } - const adminPsId = adminPs[0].id; - - // 2. Find the platform admin user (oldest cross-tenant grant). - const adminGrants = await tryFind( - ql, - 'sys_user_permission_set', - { permission_set_id: adminPsId, organization_id: null }, - 50, - ); - if (adminGrants.length === 0) { - return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' }; - } - const sortedGrants = [...adminGrants].sort((a, b) => { + const oldestFirst = (a: any, b: any) => { const ta = a.created_at ? new Date(a.created_at).getTime() : 0; const tb = b.created_at ? new Date(b.created_at).getTime() : 0; return ta - tb; - }); - const adminUserId: string | undefined = sortedGrants[0]?.user_id; + }; + + // 1-2. Resolve the platform admin. The cross-tenant grant row is the + // historical spelling and still the primary answer where it exists + // (`single` posture first-user promotion, Choice 4A; legacy walled + // grants). Since #13514 (L4) a WALLED bootstrap mints no row at all — + // standing is config-derived at the authorization derivation site — so a + // missing row is no longer a verdict: fall back to the DECLARED VERIFIED + // OWNER, resolved with the same public predicates the derivation site + // asks (`resolvePlatformAdminEmails` + row-side membership + the #11343 + // verified-email allow-list), oldest wins — the bootstrap's own tiebreak. + // Without this fallback the walled default-org bootstrap dead-ends on + // `no_admin` forever, which is how cloud's EE guided-path suite caught it. + let adminUserId: string | undefined; + const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1); + if (adminPs.length > 0 && adminPs[0].id) { + const adminGrants = await tryFind( + ql, + 'sys_user_permission_set', + { permission_set_id: adminPs[0].id, organization_id: null }, + 50, + ); + adminUserId = [...adminGrants].sort(oldestFirst)[0]?.user_id; + } + if (!adminUserId) { + const config = resolvePlatformAdminEmails(); + if (config.emails.length > 0) { + const users = await tryFind(ql, 'sys_user', {}, 50); + const owners = users + .filter((u: any) => isConfiguredPlatformAdminEmail(u?.email, config) && isEmailVerifiedUserRow(u)) + .sort(oldestFirst); + adminUserId = owners[0]?.id; + } + } if (!adminUserId) { return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' }; }