From c4a6f34beb2dfe1322d44ca532cc8e3fce7a2b47 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 00:45:36 +0000 Subject: [PATCH] test(plugin-security,plugin-auth,plugin-approvals): narrow the blanket pre-18 tenant-column relax rigs to the tables they write organization-less Each blanket `relaxEveryRegisteredTenantColumn` / `relaxEveryTenantColumn` call now names the tables its rig writes organization-less, measured per call site with a NULL-write trigger probe, through `relaxTenantColumns`. Two sites wrote no organization-less row at all, so their relax is gone. The blanket exports leave the three packages' testkits. Test files only. No behavior change. Claude-Session: https://claude.ai/code/session_014aRYTwvuSQoDgWzLdw1nc9 Co-authored-by: Claude --- ...min-rescue-named-slate.integration.test.ts | 9 +++--- ...oval-notification-body.integration.test.ts | 9 +++--- .../src/pre-18-tenant-column.testkit.ts | 29 ++----------------- .../src/account-identity-preflight.test.ts | 9 +++--- .../driver-fault-auth-log-carriers.test.ts | 9 +++--- .../src/grant-readers-by-name.golden.test.ts | 9 +++--- .../src/grant-readers-unnamed-grant.test.ts | 9 +++--- .../last-admin-guard.anchor-parity.test.ts | 10 +++---- .../last-admin-guard.config-anchor.test.ts | 4 +-- .../src/pre-18-tenant-column.testkit.ts | 29 ++----------------- .../signup-existing-address-refusal.test.ts | 9 +++--- ...nup-existing-address-success-shape.test.ts | 6 ++-- .../sso-register-platform-admin-gate.test.ts | 9 +++--- ...latform-admin-authenticable-target.test.ts | 9 +++--- ...latform-admin-existing-holder-scan.test.ts | 9 +++--- ...platform-admin-promotion-selection.test.ts | 9 +++--- .../src/builtin-positions.boot.test.ts | 9 +++--- .../src/catalog-reference-report.test.ts | 9 +++--- .../claim-seed-ownership-dispatch.pin.test.ts | 4 +-- ...lled-by-parent-chain-middle-master.test.ts | 5 ---- ...ed-admin-gate-holding-organization.test.ts | 4 +-- ...ated-write-never-exceeds-delegator.test.ts | 9 +++--- .../src/explain-asker-gate.test.ts | 9 +++--- .../src/explain-enforce-parity.test.ts | 27 +++++++---------- .../src/explain-principal-person.test.ts | 9 +++--- .../explain-removed-member-principal.test.ts | 9 +++--- .../grant-holder-membership-refusal.test.ts | 4 +-- ...nt-permission-set-name.equivalence.test.ts | 9 +++--- .../src/grant-permission-set-name.test.ts | 4 +-- .../src/grant-readers-by-name.golden.test.ts | 9 +++--- .../src/grant-readers-unnamed-grant.test.ts | 9 +++--- .../default-row-scope-declarations.test.ts | 9 +++--- ...ed-sets-identity-default-row-scope.test.ts | 9 +++--- .../src/orgless-position-name-fold.test.ts | 9 +++--- .../src/position-catalog-refusal.test.ts | 4 +-- .../src/pre-18-tenant-column.testkit.ts | 29 ++----------------- ...lve-authz-grant-set-by-name.golden.test.ts | 9 +++--- .../resolve-authz-grant-set-by-name.test.ts | 9 +++--- .../src/sys-audit-log-row-scope.test.ts | 9 +++--- ...s-user-permission-set-window-order.test.ts | 4 +-- .../tenant-wall-organization-removal.test.ts | 4 +-- 41 files changed, 148 insertions(+), 253 deletions(-) diff --git a/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts b/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts index 5beefef10c5..9251906a46c 100644 --- a/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts +++ b/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts @@ -45,7 +45,7 @@ import { SqlDriver } from '@objectstack/driver-sql'; import { bindSecurityCatalogReader, type SecurityCatalogEntry, type SecurityCatalogReader } from '@objectstack/core'; import { ApprovalService } from './approval-service.js'; import { ApprovalsServicePlugin } from './approvals-plugin.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any; @@ -186,10 +186,9 @@ describe('an empty admin_rescue slate opens on the organization\'s administrator ]; for (const def of defs) engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test'); await engine.syncSchemas(); - // The organization-less rows below (positions, holdings) exist only on a - // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_user', 'sys_user_permission_set', 'crm_deal', 'sys_approval_delegation']); bindSecurityCatalogReader(engine, catalogServing([ 'organization_admin', 'organization_admin_no_bypass', 'admin_full_access', ])); diff --git a/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts index 96a1eb655dd..487abffe68f 100644 --- a/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts +++ b/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts @@ -76,7 +76,7 @@ import { SqlDriver } from '@objectstack/driver-sql'; import { APPROVAL_REVISE_NODE_TYPE } from '@objectstack/spec/automation'; import { ApprovalService } from './approval-service.js'; import { ApprovalsServicePlugin } from './approvals-plugin.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any; const asUser = (userId: string, positions: string[] = []) => @@ -214,10 +214,9 @@ describe('every approval notification carries its text in body', () => { engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test'); } await engine.syncSchemas(); - // The organization-less rows below (positions, holdings) exist only on a - // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_user', 'sys_approval_delegation']); for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any); svc = new ApprovalService({ diff --git a/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts index b0cbb5efa38..1843e358f4a 100644 --- a/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts +++ b/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts @@ -11,6 +11,9 @@ * attributed them. A suite whose subject is those rows calls this after * `syncSchemas()` and before seeding them. Done through each table's driver * and its own drift apply door (`relax_not_null`), never a hand `ALTER`. + * + * Name only the tables the suite writes organization-less: a relaxed table + * also admits an unstamped production write, which an 18.x store refuses. */ export async function relaxTenantColumns( engine: { getDriverForObject(object: string): unknown }, @@ -28,29 +31,3 @@ export async function relaxTenantColumns( if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`); } } - -/** - * {@link relaxTenantColumns} over every object of an app that carries a tenant - * column as the engine registered it — the shape a whole pre-18 store has. - */ -export async function relaxEveryTenantColumn( - engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown }, - objects: readonly { name: string }[], -): Promise { - const tenant = objects - .map((o) => o.name) - .filter((name) => { - const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined; - return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false; - }); - await relaxTenantColumns(engine, tenant); -} - -/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */ -export async function relaxEveryRegisteredTenantColumn(engine: { - getDriverForObject(object: string): unknown; - getSchema(object: string): unknown; - registry: { getAllObjects(): readonly { name: string }[] }; -}): Promise { - await relaxEveryTenantColumn(engine, engine.registry.getAllObjects()); -} diff --git a/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts b/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts index 2624c7eec6d..56682d7a69d 100644 --- a/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts +++ b/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts @@ -46,7 +46,7 @@ import { SYS_ACCOUNT_OBJECT, } from './account-identity-preflight.js'; import { authIdentityObjects } from './manifest.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM = { context: { isSystem: true } } as never; @@ -106,10 +106,9 @@ async function bootEngine(objects: unknown[]): Promise { engine.registry.registerObject(object as never, '@objectstack/plugin-auth'); } await engine.syncSchemas(); - // The organization-less rows below (global sets, grants, identities) exist - // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column - // NOT NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_account', 'sys_sso_provider']); return engine; } diff --git a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts index 8706c212e60..38d98f24fd5 100644 --- a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts +++ b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts @@ -47,7 +47,7 @@ import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; import { AuthManager } from './auth-manager.js'; import { authIdentityObjects } from './manifest.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; /** The caller's value. Synthetic; asserted ABSENT from every carrier. */ const S = 'SENTINEL-21274-BOUND-VALUE'; @@ -253,10 +253,9 @@ for (const cell of CELLS) { engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security'); engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security'); await engine.syncSchemas(); - // The organization-less rows below (global sets, grants, identities) exist - // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column - // NOT NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // The seeded holder of the sentinel, then the constraint the two writes // below will meet in the DATABASE — not in any check of ours. diff --git a/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts b/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts index 3e9e5c3d9f8..49ee94c01c2 100644 --- a/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts +++ b/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts @@ -42,7 +42,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin- import { ensureDefaultOrganization } from './ensure-default-organization.js'; import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js'; import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -135,10 +135,9 @@ async function standingByReader(posture: Posture): Promise undefined); registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, { diff --git a/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts b/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts index 1e3ce369050..f26398e8466 100644 --- a/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts +++ b/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts @@ -36,7 +36,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin- import { ensureDefaultOrganization } from './ensure-default-organization.js'; import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js'; import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { context: { isSystem: true } } as any; const GUARD_PACKAGE = 'test.grant-readers-unnamed'; @@ -114,10 +114,9 @@ async function soleGrantAnchoredAdmin(): Promise { ]), } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, grants, identities) exist - // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column - // NOT NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // [ADR-0131 D3/D5] The Default Organization a `single` boot creates before // `kernel:ready` (AuthPlugin's invariant); it owns the promotion's grant. await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, SYS); diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.anchor-parity.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.anchor-parity.test.ts index 2d3cb950549..db5c7cb018d 100644 --- a/packages/plugins/plugin-auth/src/last-admin-guard.anchor-parity.test.ts +++ b/packages/plugins/plugin-auth/src/last-admin-guard.anchor-parity.test.ts @@ -44,7 +44,7 @@ import { ADMIN_FULL_ACCESS } from '@objectstack/spec/identity'; import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM = { context: { isSystem: true } } as const; const OWNER = 'owner@corp.example'; @@ -132,10 +132,10 @@ async function boot(opts: { organizationObject: boolean; nullableTenantColumn?: const objects = [sysUser, sysMember, sysUserPermissionSet, sysUserPosition, ...(opts.organizationObject ? [sysOrganization] : [])]; for (const o of objects) engine.registry.registerObject(o as never); await engine.syncSchemas(); - // A tenant column that is NOT NULL nowhere: a store built before 18.x has one - // (ADR-0131 D1 makes a fresh one NOT NULL), and so does a driver that - // enforces no NOT NULL. - if (opts.nullableTenantColumn) await relaxEveryRegisteredTenantColumn(engine); + // A grant tenant column that is NOT NULL nowhere: a store built before 18.x + // has one (ADR-0131 D1 makes a fresh one NOT NULL), and so does a driver + // that enforces no NOT NULL. + if (opts.nullableTenantColumn) await relaxTenantColumns(engine, ['sys_user_permission_set']); bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] }); registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, { packageId: 'test.last-admin-guard-parity' }); return { engine, driver: driver as unknown as Rig['driver'] }; diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts index 94283bf3586..ac47d1db51b 100644 --- a/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts +++ b/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts @@ -52,7 +52,7 @@ import { resetPlatformAdminEmailMemo } from '@objectstack/core'; import { registerLastAdminGuard, USER_STANDING_KEYS, type LastAdminGuardEngine } from './last-admin-guard.js'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const ENV = 'OS_PLATFORM_OWNER_EMAIL'; /** @@ -224,7 +224,7 @@ async function boot(opts: { unguarded?: boolean; legacyStore?: boolean } = {}): // An organization-less row ({@link seedLegacyRow}) exists only on a store // built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): the // cases that seed one ask for that store. - if (opts.legacyStore) await relaxEveryRegisteredTenantColumn(engine); + if (opts.legacyStore) await relaxTenantColumns(engine, ['sys_permission_set', 'sys_user_permission_set']); // [ADR-0131 D3/D4] The set a grant names is the catalog's definition. bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] }); if (!opts.unguarded) { diff --git a/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts index b0cbb5efa38..1843e358f4a 100644 --- a/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts +++ b/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts @@ -11,6 +11,9 @@ * attributed them. A suite whose subject is those rows calls this after * `syncSchemas()` and before seeding them. Done through each table's driver * and its own drift apply door (`relax_not_null`), never a hand `ALTER`. + * + * Name only the tables the suite writes organization-less: a relaxed table + * also admits an unstamped production write, which an 18.x store refuses. */ export async function relaxTenantColumns( engine: { getDriverForObject(object: string): unknown }, @@ -28,29 +31,3 @@ export async function relaxTenantColumns( if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`); } } - -/** - * {@link relaxTenantColumns} over every object of an app that carries a tenant - * column as the engine registered it — the shape a whole pre-18 store has. - */ -export async function relaxEveryTenantColumn( - engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown }, - objects: readonly { name: string }[], -): Promise { - const tenant = objects - .map((o) => o.name) - .filter((name) => { - const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined; - return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false; - }); - await relaxTenantColumns(engine, tenant); -} - -/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */ -export async function relaxEveryRegisteredTenantColumn(engine: { - getDriverForObject(object: string): unknown; - getSchema(object: string): unknown; - registry: { getAllObjects(): readonly { name: string }[] }; -}): Promise { - await relaxEveryTenantColumn(engine, engine.registry.getAllObjects()); -} diff --git a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts index e82a154a808..51a976fea7d 100644 --- a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts +++ b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts @@ -78,7 +78,7 @@ import { AuthManager } from './auth-manager.js'; import { authIdentityObjects } from './manifest.js'; import { SELF_REGISTRATION_CLOSED } from './audience-posture.js'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const BASE = 'http://localhost:3000'; const AUTH = `${BASE}/api/v1/auth`; @@ -165,10 +165,9 @@ async function bootEngine(): Promise { engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security'); engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security'); await engine.syncSchemas(); - // The organization-less rows below (global sets, grants, identities) exist - // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column - // NOT NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // [ADR-0131 D3/D4] The declared self-registration set is a catalog definition // — what the admission asks before it admits. return bindTestSecurityCatalog(engine, { permissions: [{ name: 'member_default' }] }); diff --git a/packages/plugins/plugin-auth/src/signup-existing-address-success-shape.test.ts b/packages/plugins/plugin-auth/src/signup-existing-address-success-shape.test.ts index 5c77244504c..357b924811c 100644 --- a/packages/plugins/plugin-auth/src/signup-existing-address-success-shape.test.ts +++ b/packages/plugins/plugin-auth/src/signup-existing-address-success-shape.test.ts @@ -40,7 +40,7 @@ import { AuthManager } from './auth-manager.js'; import { authIdentityObjects } from './manifest.js'; import { SELF_REGISTRATION_CLOSED } from './audience-posture.js'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const BASE = 'http://localhost:3000'; const AUTH = `${BASE}/api/v1/auth`; @@ -100,7 +100,9 @@ async function bootEngine(): Promise { engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security'); engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security'); await engine.syncSchemas(); - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less catalogue row below exists only on a store built + // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. + await relaxTenantColumns(engine, ['sys_permission_set']); const bound = await bindTestSecurityCatalog(engine, { permissions: [{ name: 'member_default' }] }); // Alice carries a stored language; Bob carries none. await engine.insert('sys_user', { id: 'usr_alice', email: EXISTING, name: 'Alice', locale: 'ja-JP' }, SYSTEM); diff --git a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts index d18b30b9ff7..6c2c2a44360 100644 --- a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts +++ b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts @@ -88,7 +88,7 @@ import type { PluginContext } from '@objectstack/core'; import { inviteForAudienceGate } from './audience-gate-test-support'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit'; import { ADMIN_FULL_ACCESS_CAPABILITIES } from '@objectstack/spec'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const BASE = 'http://localhost:3000'; const AUTH = `${BASE}/api/v1/auth`; @@ -188,10 +188,9 @@ async function bootEngine(): Promise { engine.registry.registerObject(sysPosition as never, '@objectstack/plugin-auth'); engine.registry.registerObject(sysUserPosition as never, '@objectstack/plugin-auth'); await engine.syncSchemas(); - // The organization-less rows below (global sets, grants, identities) exist - // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column - // NOT NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // [ADR-0131 D3/D4] The platform's `admin_full_access` definition, which a // grant of it resolves through (no security plugin binds one here). return bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS, ...ADMIN_FULL_ACCESS_CAPABILITIES }] }); diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts index 7718259ab8f..620cdb003ac 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts @@ -54,7 +54,7 @@ import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testki import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM_CTX = { isSystem: true }; @@ -111,10 +111,9 @@ async function boot(): Promise { ], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any // `kernel:ready` hook runs; it owns the promotion's grant. await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } }); diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts index 09693b40b30..7ff4ff27f90 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts @@ -86,7 +86,7 @@ import { import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM_CTX = { isSystem: true }; const OWNER_ENV = 'OS_PLATFORM_OWNER_EMAIL'; @@ -138,10 +138,9 @@ async function boot(): Promise { objects: [SysPermissionSet, SysUserPermissionSet, SysUser, SysAccount, SysOrganization], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_user_permission_set']); // [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any // `kernel:ready` hook runs; it owns the promotion's grant. await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } }); diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts index 55b45d567f1..7b2d353df36 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts @@ -74,7 +74,7 @@ import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testki import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYSTEM_CTX = { isSystem: true }; const OWNER_ENV = 'OS_PLATFORM_OWNER_EMAIL'; @@ -129,10 +129,9 @@ async function boot(): Promise { objects: [SysPermissionSet, SysUserPermissionSet, SysUser, SysAccount, SysOrganization], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); // [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any // `kernel:ready` hook runs; it owns the promotion's grant. await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } }); diff --git a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts index 535d57c2e00..3b7d7cb0fdb 100644 --- a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts +++ b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts @@ -70,7 +70,7 @@ import { securityObjects, SECURITY_PLUGIN_ID } from './manifest.js'; import { securityBuiltinPositions } from './builtin-positions.js'; import { withLegacyCatalogTables } from './__tests__/legacy-catalog-objects.testkit.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -232,10 +232,9 @@ async function boot(scenario: Scenario): Promise { objects: withLegacyCatalogTables([SysUser, SysAccount, SysMember, SysOrganization, ...securityObjects]), } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_position']); if (scenario.authoredPosition) { // How a metadata author's saved definition is hydrated: no package. (engine as any).registry.registerItem('position', { name: 'door_authored', label: 'Door Authored' }, 'name'); diff --git a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts index 3242f2b9f99..a99e45dbc4d 100644 --- a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts +++ b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts @@ -53,7 +53,7 @@ import { type CatalogReferenceReport, type CatalogReferenceReportEngine, } from './catalog-reference-report.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -133,10 +133,9 @@ async function boot(opts: { posture?: 'single' | 'isolated' } = {}): Promise { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); // The two delegators exist as users, so the on-behalf-of link resolves rather // than failing closed. Each holds the manager's set through a stored position diff --git a/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts b/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts index 684da9673dc..ebe52336d62 100644 --- a/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts +++ b/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts @@ -34,7 +34,7 @@ import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; import { DelegatedAdminGate } from './delegated-admin-gate.js'; import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const ORG_A = 'org_a_acme'; /** Sorts BEFORE every `org_a` / `*_a_*` id, like the sibling suites. */ @@ -90,7 +90,7 @@ async function bootEngine() { await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, OBJECTS); + await relaxTenantColumns(engine, ['sys_user_position']); engines.push(engine); return engine; } diff --git a/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts b/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts index 56f0d29bada..0ca6725ced9 100644 --- a/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts +++ b/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts @@ -45,7 +45,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; import { wireEngineSecuritySource } from './engine-security-source.testkit.js'; const SYS = { context: { isSystem: true } } as never; @@ -126,10 +126,9 @@ async function boot(posture: TenancyPosture) { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise; diff --git a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts index 4a15cdf6d38..c5082fa811f 100644 --- a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts +++ b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts @@ -66,7 +66,7 @@ import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; import { PermissionEvaluator } from './permission-evaluator.js'; import { explainAccess, explainAccessForAsker, type ExplainEngineDeps, type ExplainInput } from './explain-engine.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { context: { isSystem: true } } as never; const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!; @@ -173,10 +173,9 @@ beforeAll(async () => { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: unknown, opts: never) => Promise }; for (const id of [MEMBER, ADMIN.userId, BLIND_ADMIN.userId, DELEGATE.userId, REFUSED_ADMIN.userId, REFUSED_DELEGATE.userId, EDIT_ONLY_USER]) { diff --git a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts index 4cca44d237c..fff116af300 100644 --- a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts +++ b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts @@ -90,7 +90,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; import { wireEngineSecuritySource } from './engine-security-source.testkit.js'; const SYS = { context: { isSystem: true } } as never; @@ -328,10 +328,9 @@ async function bootRls(predicate: string, opts: { grantCrud?: boolean } = {}) { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, [OBJ]); await engine.insert(OBJ, [ { id: 'r1', status: 'open', title: 'x', amount: 5 }, { id: 'r2', status: 'open', title: 'open', amount: 7 }, @@ -479,10 +478,9 @@ async function bootPrincipal(source: PostureSource) { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise }; for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS); @@ -665,10 +663,6 @@ async function bootSharing(opts: { faultReadFilter?: boolean; authored?: boolean ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); await engine.insert(LINE, [ { id: 'l_other', name: 'owned by someone else', owner_id: 'u_somebody' }, { id: 'l_own', name: 'owned by the control', owner_id: 'u_own' }, @@ -843,10 +837,9 @@ async function bootDelegated(posture: TenancyPosture) { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise }; for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS); diff --git a/packages/plugins/plugin-security/src/explain-principal-person.test.ts b/packages/plugins/plugin-security/src/explain-principal-person.test.ts index 420cef4b436..b7cba72a564 100644 --- a/packages/plugins/plugin-security/src/explain-principal-person.test.ts +++ b/packages/plugins/plugin-security/src/explain-principal-person.test.ts @@ -37,7 +37,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { context: { isSystem: true } } as never; const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!; @@ -123,10 +123,9 @@ beforeAll(async () => { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: unknown, opts: never) => Promise }; await e.insert('sys_user', [ diff --git a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts index 6540a058b42..9c96ec151f0 100644 --- a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts +++ b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts @@ -81,7 +81,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { context: { isSystem: true } } as never; const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!; @@ -135,10 +135,9 @@ async function boot(makeDriver: () => Driver, posture: TenancyPosture) { ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise }; for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS); diff --git a/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts b/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts index 6f4c2f78590..59fe67d8cfe 100644 --- a/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts +++ b/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts @@ -39,7 +39,7 @@ import { GRANT_HOLDER_MEMBERSHIP_HOOK_PACKAGE, registerGrantHolderMembershipRefusal, } from './grant-holder-membership-refusal.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; // --------------------------------------------------------------------------- // Fixture @@ -142,7 +142,7 @@ async function boot(opts: BootOptions = {}) { await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet]); + await relaxTenantColumns(engine, ['sys_position', 'sys_user_position', 'sys_user_permission_set']); // `sys_user` stays registered and unprovisioned, as in the sibling suites: // the engine's own `user_id` existence probe then cannot run and stands // down, so what is measured here is the membership predicate alone. diff --git a/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts b/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts index 7cacc318b33..1015162f0a0 100644 --- a/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts +++ b/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts @@ -52,7 +52,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js' import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -117,10 +117,9 @@ async function grantsByPrincipal(posture: Posture): Promise = { diff --git a/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts b/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts index df52d0907c3..26f7d60c633 100644 --- a/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts +++ b/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts @@ -43,7 +43,7 @@ import { grantSetNotInCatalogMessage, registerGrantPermissionSetNameHooks, } from './grant-permission-set-name.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; // --------------------------------------------------------------------------- // Fixture @@ -98,7 +98,7 @@ async function boot() { await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember]); + await relaxTenantColumns(engine, ['sys_user_permission_set']); // The authz resolver reads these; the auth plugin registers them in a deployment. for (const o of [SysUser, SysMember]) { if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set'); diff --git a/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts b/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts index df771499430..398658c30f7 100644 --- a/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts +++ b/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts @@ -52,7 +52,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js' import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; /** The environment-authored set the member holds a grant of, as the security catalog declares it. */ const LEGACY_REPORTS = { name: 'legacy_reports', label: 'Legacy reports' }; @@ -139,10 +139,9 @@ async function readersByPrincipal(posture: Posture): Promise = { diff --git a/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts b/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts index a59827ffa40..7861a307660 100644 --- a/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts +++ b/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts @@ -44,7 +44,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js' import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const CATALOG_SETS: Array<{ name: string; label?: string }> = [...defaultPermissionSets, { name: 'legacy_reports', label: 'Legacy' }]; @@ -82,10 +82,9 @@ async function boot(): Promise { ], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set']); engines.push(engine); const services: Record = { manifest: { register: vi.fn() }, diff --git a/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts b/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts index 7776f519d19..9c0b0b7f13c 100644 --- a/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts +++ b/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts @@ -52,7 +52,7 @@ import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from '../__te import { SysUserPosition } from './sys-user-position.object.js'; import { SysUserPermissionSet } from './sys-user-permission-set.object.js'; import { bindTestSecurityCatalog } from '../__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from '../pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from '../pre-18-tenant-column.testkit.js'; type Policy = { name: string; @@ -405,10 +405,9 @@ async function bootBaseline(declared: boolean) { objects: [...declarers, objects.get('sys_member')!, SysUserPermissionSet, SysUserPosition], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_user_preference']); for (const o of [SysPosition, SysPermissionSet, SysPositionPermissionSet]) { if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set'); } diff --git a/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts b/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts index 5473ae07428..c501900d90e 100644 --- a/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts +++ b/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts @@ -48,7 +48,7 @@ import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from '../__te import { SysUserPosition } from './sys-user-position.object.js'; import { SysUserPermissionSet } from './sys-user-permission-set.object.js'; import { bindTestSecurityCatalog } from '../__tests__/security-catalog.testkit.js'; -import { relaxEveryRegisteredTenantColumn } from '../pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from '../pre-18-tenant-column.testkit.js'; type Policy = { name: string; object?: string; operation: string; using?: string; enabled?: boolean }; type ObjectDef = { name: string; fields?: Record; defaultRowScope?: Policy }; @@ -265,10 +265,9 @@ async function boot(shape: Shape, sets: PermissionSet[]) { objects: [...schemas, SysUserPermissionSet, SysUserPosition], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_user_permission_set', 'sys_user_preference']); for (const o of [SysPosition, SysPermissionSet, SysPositionPermissionSet]) { if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set'); } diff --git a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts index 9c106f3df5d..349d38f433b 100644 --- a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts +++ b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts @@ -63,7 +63,7 @@ import { SysOrganization, SysUser, SysMember } from '@objectstack/platform-objec import { SecurityPlugin } from './security-plugin.js'; import { buildContextForUser } from './explain-engine.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { context: { isSystem: true } } as any; /** The organization that authors the colliding sets. Nobody below is its member except `USER_OTHER`. */ @@ -146,10 +146,9 @@ async function boot(): Promise { ], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set', 'sys_user_position', 'sys_user_permission_set']); engines.push(engine); const e = engine as any; await e.insert('sys_organization', { id: ORG_OTHER, name: ORG_OTHER, slug: 'other' }, SYS); diff --git a/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts b/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts index 369cdd7e6f0..a8529e1d90e 100644 --- a/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts +++ b/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts @@ -36,7 +36,7 @@ import { SysMetadataActivation } from '@objectstack/platform-objects/system'; import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { namesWithoutCatalogRow, positionNotInCatalogMessage } from './position-catalog-refusal.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; // --------------------------------------------------------------------------- // Fixture @@ -133,7 +133,7 @@ async function boot(opts: { walled?: boolean } = {}) { await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, [SysUserPosition, SysUserPermissionSet, SysMember, SysMetadataActivation, QA_INQUIRY]); + await relaxTenantColumns(engine, ['sys_user_position', 'qa_inquiry']); // [#21516] The authz resolver reads these on every grant resolution; in a // deployment the auth and security plugins register them. This harness // composes neither, so the ones its app does not declare are registered diff --git a/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts index b0cbb5efa38..1843e358f4a 100644 --- a/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts +++ b/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts @@ -11,6 +11,9 @@ * attributed them. A suite whose subject is those rows calls this after * `syncSchemas()` and before seeding them. Done through each table's driver * and its own drift apply door (`relax_not_null`), never a hand `ALTER`. + * + * Name only the tables the suite writes organization-less: a relaxed table + * also admits an unstamped production write, which an 18.x store refuses. */ export async function relaxTenantColumns( engine: { getDriverForObject(object: string): unknown }, @@ -28,29 +31,3 @@ export async function relaxTenantColumns( if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`); } } - -/** - * {@link relaxTenantColumns} over every object of an app that carries a tenant - * column as the engine registered it — the shape a whole pre-18 store has. - */ -export async function relaxEveryTenantColumn( - engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown }, - objects: readonly { name: string }[], -): Promise { - const tenant = objects - .map((o) => o.name) - .filter((name) => { - const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined; - return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false; - }); - await relaxTenantColumns(engine, tenant); -} - -/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */ -export async function relaxEveryRegisteredTenantColumn(engine: { - getDriverForObject(object: string): unknown; - getSchema(object: string): unknown; - registry: { getAllObjects(): readonly { name: string }[] }; -}): Promise { - await relaxEveryTenantColumn(engine, engine.registry.getAllObjects()); -} diff --git a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts index 0c8568f07ee..5d47b1f8489 100644 --- a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts +++ b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts @@ -46,7 +46,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js' import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -114,10 +114,9 @@ async function resolverByPrincipal(posture: Posture): Promise = { diff --git a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts index af6a6380779..1c6f5d3b49e 100644 --- a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts +++ b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts @@ -35,7 +35,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js' import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const SYS = { isSystem: true } as const; const POSTURE_ENV = 'OS_TENANCY_POSTURE'; @@ -76,10 +76,9 @@ async function newEngine(): Promise { ], } as any); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_permission_set', 'sys_user_permission_set']); engines.push(engine); vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined); return engine; diff --git a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts index dae76c50ba2..04b890d036d 100644 --- a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts +++ b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts @@ -32,7 +32,7 @@ import type { PermissionSet } from '@objectstack/spec/security'; import { SecurityPlugin } from './security-plugin.js'; import { defaultPermissionSets } from './objects/default-permission-sets.js'; import { isPlatformTenantPolicy } from './platform-tenant-policies.js'; -import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const LEDGER = 'sys_audit_log'; const POLICY = 'sys_audit_log_org'; @@ -122,10 +122,9 @@ async function boot(opts: { posture: Posture; sets?: PermissionSet[]; ledger?: R ], } as never); await engine.syncSchemas(); - // The organization-less rows below (global sets, positions, grants) exist only - // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT - // NULL): this is that store. - await relaxEveryRegisteredTenantColumn(engine); + // The organization-less rows below exist only on a store built before 18.x + // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store. + await relaxTenantColumns(engine, ['sys_audit_log']); await engine.insert('sys_organization', [{ id: 'org_a', name: 'A' }, ...(single ? [] : [{ id: 'org_b', name: 'B' }])], SYS); const before = opts.ledger === LEDGER_BEFORE; const ledgerRows = [ diff --git a/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts b/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts index f3870413f39..33fde7eecaf 100644 --- a/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts +++ b/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts @@ -43,7 +43,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js'; import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'; import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; const MEMBER_DEFAULT = { name: 'member_default', label: 'Member', objects: {} } as unknown as PermissionSet; const QA_ADMIN = { @@ -89,7 +89,7 @@ async function boot() { await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember]); + await relaxTenantColumns(engine, ['sys_user_permission_set']); for (const o of [SysUser, SysMember]) { if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set'); } diff --git a/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts b/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts index 6d51688a328..755bbac5d4a 100644 --- a/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts +++ b/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts @@ -56,7 +56,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; import type { PermissionSet } from '@objectstack/spec/security'; import { SecurityPlugin } from './security-plugin.js'; import { SysUserPosition, SysUserPermissionSet } from './objects/index.js'; -import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js'; +import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js'; /** The organization the caller holds (its active organization). */ const OWN_ORG = 'org_a'; @@ -151,7 +151,7 @@ async function boot(makeDriver: () => unknown, posture: Posture = 'isolated'): P await engine.syncSchemas(); // The organization-less holdings and rows below exist only on a store built // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it. - await relaxEveryTenantColumn(engine, OBJECTS); + await relaxTenantColumns(engine, ['qa_ledger', 'qa_vault', 'sys_user_position', 'sys_user_permission_set']); engines.push(engine); // The stored-row half: a hook that clears the column when asked to by name.