Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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',
]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []) =>
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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<void> {
const tenant = objects
.map((o) => o.name)
.filter((name) => {
const schema = engine.getSchema(name) as { fields?: Record<string, unknown>; 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<void> {
await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -106,10 +106,9 @@ async function bootEngine(objects: unknown[]): Promise<ObjectQL> {
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -135,10 +135,9 @@ async function standingByReader(posture: Posture): Promise<Record<string, unknow
objects: withLegacyPermissionSetTable([SysUser, SysAccount, SysMember, SysOrganization, SysMetadataActivation, ...securityObjects]),
} 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']);
await plugin.start(ctx);
vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -114,10 +114,9 @@ async function soleGrantAnchoredAdmin(): Promise<Rig> {
]),
} 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'] };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand Down Expand Up @@ -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) {
Expand Down
29 changes: 3 additions & 26 deletions packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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<void> {
const tenant = objects
.map((o) => o.name)
.filter((name) => {
const schema = engine.getSchema(name) as { fields?: Record<string, unknown>; 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<void> {
await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -165,10 +165,9 @@ async function bootEngine(): Promise<ObjectQL> {
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' }] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -100,7 +100,9 @@ async function bootEngine(): Promise<ObjectQL> {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -188,10 +188,9 @@ async function bootEngine(): Promise<ObjectQL> {
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 }] });
Expand Down
Loading
Loading