diff --git a/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts b/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts index ec0ffc53528..cc4d10ec9fb 100644 --- a/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts +++ b/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts @@ -63,17 +63,27 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); * ONLY read refusals whose log frames may be withheld here. * * `beforeEach` boots a kernel with no datasource and attaches sqlite late, then - * syncs exactly four objects. Every approval decision below therefore probes - * the six authz tables `resolveUserAuthzGrants` reads plus - * `sys_approval_delegation`, which `ApprovalService.lookupActiveDelegation` - * reads best-effort on each decision. All are fail-soft, so the reads are - * EXPECTED — but the driver and the engine each log the fault on the way out. + * syncs its objects. Every approval decision below probes `sys_organization` + * (`ObjectQL.probeInstallOrganizations`) and `sys_approval_delegation`, which + * `ApprovalService.lookupActiveDelegation` reads best-effort on each decision. + * Both are fail-soft, so the reads are EXPECTED — but the driver and the engine + * each log the fault on the way out. * - * ⛔ MEASURED, not copied from the probers' source: a run of this file at - * `logger: { level: 'info' }` emitted 25 `refused a read on ''` driver lines - * and 25 matching `ERROR Find operation failed` engine frames — sys_user 5 / - * sys_member 4 / sys_position 4 / sys_user_position 4 / - * sys_user_permission_set 4 / sys_organization 2 / sys_approval_delegation 2. + * ⚠️ [#17985] The five authz tables `resolveUserAuthzGrants` reads USED TO BE on + * this list. They are not any more, because this fixture now provisions them + * (see `authzResolverObjects` below) — which is the outcome this channel's own + * contract names: "a table that started resolving means the fixture now + * provisions it". Being fail-soft, their refusal was not just noise: `tryFind` + * answers `[]` for a missing table, so every approval decision here resolved + * its grants from reads that never happened. ⛔ The count fell because the + * reads SUCCEED, not because anything was quietened. + * + * ⛔ MEASURED, not copied from the probers' source. At `225197cdb`-era HEAD, + * before this fixture provisioned anything, a run emitted 25 `refused a read on + * ''` driver lines and 25 matching engine frames — sys_user 5 / sys_member 4 + * / sys_position 4 / sys_user_position 4 / sys_user_permission_set 4 / + * sys_organization 2 / sys_approval_delegation 2. The 21 authz ones are gone; + * the remaining two channels are the ones declared below. * * ## Why a capture instead of the blanket `silent` this replaces * @@ -87,15 +97,102 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); * happen. A capture nobody asserts is a mute. */ const EXPECTED_ABSENT_PROBE_TABLES = [ - 'sys_user', - 'sys_member', - 'sys_user_position', - 'sys_user_permission_set', - 'sys_position', 'sys_organization', 'sys_approval_delegation', ] as const; +/** + * ⚠️ [#17985] The authz objects a real approval decision makes core's + * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and + * `@objectstack/plugin-security`; they are spelled LOCALLY here, with only the + * columns that resolver reads, so this fixture adds no dependency edge onto + * either package (the `find-envelope-limb-removal.test.ts` / + * `signup-existing-address-refusal.test.ts` precedent, applied by PR #17982 to + * the two `plugin-auth` fixtures that carried the same defect). + * + * Columns, and why each one is here — every other column of the real objects is + * deliberately absent, because no read on this path touches it: + * `sys_user` id (filter), email (the RLS owner-email fallback) + * `sys_member` user_id / organization_id (filters), role + * `sys_user_position` user_id (filter), position, organization_id + * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id + * `sys_position` name (filter), id, active (`isRowActive`), + * organization_id (the driver's tenant scope) + * + * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the + * resolver reaches them only once a `sys_position` row resolves and a + * permission-set id is collected, and these cases seed neither — registering + * them would add fixture surface no read in this file touches. The ADR-0091 + * validity columns are absent for the reason `sys_member` lacks them today: + * `isGrantActive` reads an absent bound as unbounded, so declaring them would + * change no verdict here. + */ +const authzResolverObjects = [ + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_user', + label: 'User', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + email: { name: 'email', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_member', + label: 'Member', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + role: { name: 'role', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_position', + label: 'User Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + position: { name: 'position', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_permission_set', + label: 'User Permission Set', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + permission_set_id: { name: 'permission_set_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_position', + label: 'Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + name: { name: 'name', type: 'text' as const }, + active: { name: 'active', type: 'boolean' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, +] as const; + /** [#11081] Shared by both kernels this file boots; asserted once in `afterAll`. */ const noise = captureExpectedReadRefusals([...EXPECTED_ABSENT_PROBE_TABLES]); @@ -207,6 +304,12 @@ describe('an approval decision cascades as the deciding user (#3783)', () => { for (const def of [opportunity, SysApprovalRequest, SysApprovalAction, SysApprovalApprover]) { objectql.registry.registerObject(def as any, 'approvals-test', 'approvals-test'); } + // [#17985] The authz resolver's own reads, so a decision resolves its + // grants from reads that HAPPEN rather than from `tryFind`'s empty answer + // for a missing table. + for (const o of authzResolverObjects) { + objectql.registry.registerObject(o.def as any, o.owner); + } // Real DDL for all four objects — including the three sys_approval_* tables // the ApprovalService writes through. await objectql.syncSchemas(); diff --git a/packages/services/service-automation/src/paused-run-visibility.test.ts b/packages/services/service-automation/src/paused-run-visibility.test.ts index 63d0b1252f6..c5870b58869 100644 --- a/packages/services/service-automation/src/paused-run-visibility.test.ts +++ b/packages/services/service-automation/src/paused-run-visibility.test.ts @@ -417,6 +417,103 @@ describe('#8050 merging a third source: no duplicates, and a stated precedence', * `where: { status: 'paused' }` scan the DB-backed store actually issues. That * is where "works against a Map, empty against a table" would hide. */ +/** + * ⚠️ [#17985] The authz objects a write here makes core's + * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and + * `@objectstack/plugin-security`; they are spelled LOCALLY, with only the + * columns that resolver reads, so this fixture adds no dependency edge onto + * either package (the `find-envelope-limb-removal.test.ts` precedent, applied + * by PR #17982 to the two `plugin-auth` fixtures with the same defect). + * + * Without them the driver REFUSED every one of those reads — measured at + * `2f1a6f696`: 5 `refused a read on` driver lines, one per table. + * `tryFind` classifies a missing table as "not provisioned" and answers `[]`, + * so nothing went red: the assertions below passed over grant reads that never + * happened. ⛔ Registering the tables is what makes those reads SUCCEED; the + * fix is not to quieten the line. + * + * Columns, and why each is here — every other column of the real objects is + * deliberately absent, because no read on this path touches it: + * `sys_user` id (filter), email (the RLS owner-email fallback) + * `sys_member` user_id / organization_id (filters), role + * `sys_user_position` user_id (filter), position, organization_id + * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id + * `sys_position` name (filter), id, active (`isRowActive`), + * organization_id (the driver's tenant scope) + * + * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the + * resolver reaches them only once a `sys_position` row resolves and a + * permission-set id is collected, and nothing here seeds either. The ADR-0091 + * validity columns are absent for the reason `sys_member` lacks them today: + * `isGrantActive` reads an absent bound as unbounded, so declaring them would + * change no verdict here. + */ +const authzResolverObjects = [ + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_user', + label: 'User', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + email: { name: 'email', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_member', + label: 'Member', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + role: { name: 'role', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_position', + label: 'User Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + position: { name: 'position', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_permission_set', + label: 'User Permission Set', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + permission_set_id: { name: 'permission_set_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_position', + label: 'Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + name: { name: 'name', type: 'text' as const }, + active: { name: 'active', type: 'boolean' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, +] as const; + describe('#8050 cold boot over the same sqlite file (full stack)', () => { let dir: string | undefined; const kernels: ObjectKernel[] = []; @@ -444,6 +541,11 @@ describe('#8050 cold boot over the same sqlite file (full stack)', () => { }); await driver.connect(); ql.registerDriver(driver, true); + // [#17985] The authz resolver's own reads, registered before the sync so + // they are provisioned rather than refused. + for (const o of authzResolverObjects) { + (ql as any).registry.registerObject(o.def as never, o.owner); + } await ql.syncSchemas(); const automation = kernel.getService('automation'); diff --git a/packages/triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts b/packages/triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts index 4ff6ae259eb..5ed41383c3e 100644 --- a/packages/triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts +++ b/packages/triggers/trigger-record-change/src/before-update-flow-payload-reach.test.ts @@ -214,6 +214,104 @@ function makeDriver(): any { * top-level key. That is the falsification candidate, and it cannot be tried * on a payload of scalars. */ +/** + * ⚠️ [#17985] The authz objects a write here makes core's + * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and + * `@objectstack/plugin-security`; they are spelled LOCALLY, with only the + * columns that resolver reads, so this fixture adds no dependency edge onto + * either package (the `find-envelope-limb-removal.test.ts` precedent, applied + * by PR #17982 to the two `plugin-auth` fixtures with the same defect). + * + * Without them the driver REFUSED every one of those reads — measured at + * `2f1a6f696`: 10 `refused a read on` driver lines, two per table across this + * file's one real-sqlite case. + * `tryFind` classifies a missing table as "not provisioned" and answers `[]`, + * so nothing went red: the assertions below passed over grant reads that never + * happened. ⛔ Registering the tables is what makes those reads SUCCEED; the + * fix is not to quieten the line. + * + * Columns, and why each is here — every other column of the real objects is + * deliberately absent, because no read on this path touches it: + * `sys_user` id (filter), email (the RLS owner-email fallback) + * `sys_member` user_id / organization_id (filters), role + * `sys_user_position` user_id (filter), position, organization_id + * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id + * `sys_position` name (filter), id, active (`isRowActive`), + * organization_id (the driver's tenant scope) + * + * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the + * resolver reaches them only once a `sys_position` row resolves and a + * permission-set id is collected, and nothing here seeds either. The ADR-0091 + * validity columns are absent for the reason `sys_member` lacks them today: + * `isGrantActive` reads an absent bound as unbounded, so declaring them would + * change no verdict here. + */ +const authzResolverObjects = [ + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_user', + label: 'User', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + email: { name: 'email', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_member', + label: 'Member', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + role: { name: 'role', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_position', + label: 'User Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + position: { name: 'position', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_permission_set', + label: 'User Permission Set', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + permission_set_id: { name: 'permission_set_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_position', + label: 'Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + name: { name: 'name', type: 'text' as const }, + active: { name: 'active', type: 'boolean' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, +] as const; + function registerObjects(registry: TestObjectRegistry, object: string): void { registry.registerObject({ name: object, label: object, @@ -793,6 +891,11 @@ describe('[#15356/#14744] S5 on the real SQL driver — the mutation reaches no await driver.connect(); objectql.registerDriver(driver, true); registerObjects(objectql.registry as unknown as TestObjectRegistry, 'sq'); + // [#17985] The authz resolver's own reads, registered before the sync so + // they are provisioned rather than refused. + for (const o of authzResolverObjects) { + (objectql.registry as unknown as TestObjectRegistry).registerObject(o.def as never, o.owner); + } await objectql.syncSchemas(); objectql.registerFunction('sq_mutate_nested', async (args: any) => { diff --git a/packages/triggers/trigger-record-change/src/record-change-integration.test.ts b/packages/triggers/trigger-record-change/src/record-change-integration.test.ts index 22e8413a8ca..7d0efb716f0 100644 --- a/packages/triggers/trigger-record-change/src/record-change-integration.test.ts +++ b/packages/triggers/trigger-record-change/src/record-change-integration.test.ts @@ -69,18 +69,28 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); * the ONLY read refusals whose log frames may be withheld here. * * Every `it` below boots a kernel with no datasource, attaches sqlite late, and - * registers exactly its own test object. The authz resolver - * (`core/src/security/resolve-authz-context.ts`) and - * `ObjectQL.probeInstallOrganizations` then probe these six on every write. - * Both probes are fail-soft by construction, so the reads are EXPECTED — but - * the driver and the engine each log the fault on the way out. + * registers its own test object. `ObjectQL.probeInstallOrganizations` then + * probes `sys_organization` on every write. That probe is fail-soft by + * construction, so the read is EXPECTED — but the driver and the engine each + * log the fault on the way out. * - * ⛔ This list is MEASURED, not copied from the probers' source: a run of this - * file at `logger: { level: 'info' }` emitted 84 `refused a read on ''` - * driver lines and 84 matching `ERROR Find operation failed` engine frames, - * split sys_user 15 / sys_member 15 / sys_user_position 15 / - * sys_user_permission_set 15 / sys_position 15 / sys_organization 9. A read - * this file stops provoking therefore shows up as a CHANGED SET (red + * ⚠️ [#17985] The five authz tables the resolver + * (`core/src/security/resolve-authz-context.ts`) reads USED TO BE on this list. + * They are not any more, because `attachSqlite` now provisions them (see + * `authzResolverObjects` below) — the outcome this channel's own contract + * names: "a table that started resolving means the fixture now provisions it". + * Their refusal was never only noise: `tryFind` answers `[]` for a missing + * table, so every write below resolved its grants from reads that never + * happened. ⛔ The count fell because the reads SUCCEED, not because anything + * was quietened. + * + * ⛔ This list is MEASURED, not copied from the probers' source: before this + * file provisioned anything, a run at `logger: { level: 'info' }` emitted 84 + * `refused a read on ''` driver lines and 84 matching `ERROR Find operation + * failed` engine frames, split sys_user 15 / sys_member 15 / sys_user_position + * 15 / sys_user_permission_set 15 / sys_position 15 / sys_organization 9. The + * 75 authz ones are gone; the one channel left is the one declared below. A + * read this file stops provoking still shows up as a CHANGED SET (red * `silentChannels()`), not as a silence. * * ## Why a capture instead of the blanket `silent` this replaces @@ -94,13 +104,96 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); * real console, and COUNTS what it withheld so `afterAll` can assert the * expected reads still happen. A capture nobody asserts is a mute. */ -const EXPECTED_ABSENT_PROBE_TABLES = [ - 'sys_user', - 'sys_member', - 'sys_user_position', - 'sys_user_permission_set', - 'sys_position', - 'sys_organization', +const EXPECTED_ABSENT_PROBE_TABLES = ['sys_organization'] as const; + +/** + * ⚠️ [#17985] The authz objects a write here makes core's + * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and + * `@objectstack/plugin-security`; they are spelled LOCALLY, with only the + * columns that resolver reads, so this fixture adds no dependency edge onto + * either package (the `find-envelope-limb-removal.test.ts` precedent, applied + * by PR #17982 to the two `plugin-auth` fixtures with the same defect). + * + * Columns, and why each is here — every other column of the real objects is + * deliberately absent, because no read on this path touches it: + * `sys_user` id (filter), email (the RLS owner-email fallback) + * `sys_member` user_id / organization_id (filters), role + * `sys_user_position` user_id (filter), position, organization_id + * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id + * `sys_position` name (filter), id, active (`isRowActive`), + * organization_id (the driver's tenant scope) + * + * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the + * resolver reaches them only once a `sys_position` row resolves and a + * permission-set id is collected, and nothing here seeds either. The ADR-0091 + * validity columns are absent for the reason `sys_member` lacks them today: + * `isGrantActive` reads an absent bound as unbounded, so declaring them would + * change no verdict here. + */ +const authzResolverObjects = [ + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_user', + label: 'User', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + email: { name: 'email', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_member', + label: 'Member', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + role: { name: 'role', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_position', + label: 'User Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + position: { name: 'position', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_permission_set', + label: 'User Permission Set', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + permission_set_id: { name: 'permission_set_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_position', + label: 'Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + name: { name: 'name', type: 'text' as const }, + active: { name: 'active', type: 'boolean' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, ] as const; /** [#11081] Shared by every kernel this file boots; asserted once in `afterAll`. */ @@ -161,6 +254,13 @@ async function attachSqlite(objectql: any): Promise { // The engine exists only after `kernel.bootstrap()`, which every caller has // already run; the probed reads all happen later, per write. noise.captureEngine(objectql); + // [#17985] Registered on the ONE seam every boot site in this file goes + // through, before any caller's `syncSchemas()`, so each write below resolves + // its grants from reads that HAPPEN rather than from `tryFind`'s empty answer + // for a missing table. + for (const o of authzResolverObjects) { + objectql.registry.registerObject(o.def as any, o.owner); + } openDrivers.push(driver); return driver; } diff --git a/packages/triggers/trigger-record-change/src/reentrant-start-condition.test.ts b/packages/triggers/trigger-record-change/src/reentrant-start-condition.test.ts index a120a2772aa..48a4baa8420 100644 --- a/packages/triggers/trigger-record-change/src/reentrant-start-condition.test.ts +++ b/packages/triggers/trigger-record-change/src/reentrant-start-condition.test.ts @@ -83,6 +83,104 @@ type EngineWithLogger = { logger: { warn(message: string, meta?: unknown): void const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +/** + * ⚠️ [#17985] The authz objects a write here makes core's + * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and + * `@objectstack/plugin-security`; they are spelled LOCALLY, with only the + * columns that resolver reads, so this fixture adds no dependency edge onto + * either package (the `find-envelope-limb-removal.test.ts` precedent, applied + * by PR #17982 to the two `plugin-auth` fixtures with the same defect). + * + * Without them the driver REFUSED every one of those reads — measured at + * `2f1a6f696`: 10 `refused a read on` driver lines, two per table across this + * file's two writes. + * `tryFind` classifies a missing table as "not provisioned" and answers `[]`, + * so nothing went red: the assertions below passed over grant reads that never + * happened. ⛔ Registering the tables is what makes those reads SUCCEED; the + * fix is not to quieten the line. + * + * Columns, and why each is here — every other column of the real objects is + * deliberately absent, because no read on this path touches it: + * `sys_user` id (filter), email (the RLS owner-email fallback) + * `sys_member` user_id / organization_id (filters), role + * `sys_user_position` user_id (filter), position, organization_id + * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id + * `sys_position` name (filter), id, active (`isRowActive`), + * organization_id (the driver's tenant scope) + * + * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the + * resolver reaches them only once a `sys_position` row resolves and a + * permission-set id is collected, and nothing here seeds either. The ADR-0091 + * validity columns are absent for the reason `sys_member` lacks them today: + * `isGrantActive` reads an absent bound as unbounded, so declaring them would + * change no verdict here. + */ +const authzResolverObjects = [ + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_user', + label: 'User', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + email: { name: 'email', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-auth', + def: { + name: 'sys_member', + label: 'Member', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + role: { name: 'role', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_position', + label: 'User Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + position: { name: 'position', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_user_permission_set', + label: 'User Permission Set', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + user_id: { name: 'user_id', type: 'text' as const }, + permission_set_id: { name: 'permission_set_id', type: 'text' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, + { + owner: '@objectstack/plugin-security', + def: { + name: 'sys_position', + label: 'Position', + fields: { + id: { name: 'id', type: 'text' as const, primaryKey: true }, + name: { name: 'name', type: 'text' as const }, + active: { name: 'active', type: 'boolean' as const }, + organization_id: { name: 'organization_id', type: 'text' as const }, + }, + }, + }, +] as const; + function makeSqliteDriver(): SqlDriver { return new SqlDriver({ client: 'better-sqlite3', @@ -167,6 +265,11 @@ describe("a record-change flow's start condition is evaluated on its own write-b openDrivers.push(driver); objectql.registry.registerObject(caseObjectDef('esc_case'), 'test', 'test'); + // [#17985] The authz resolver's own reads, registered before the sync so + // they are provisioned rather than refused. + for (const o of authzResolverObjects) { + objectql.registry.registerObject(o.def as never, o.owner); + } await objectql.syncSchemas(); automation.registerFlow('esc_escalation', escalationFlow('esc_escalation', 'esc_case') as never);