Skip to content

Commit 9cee19a

Browse files
committed
test(objectql, plugin-security): the nested-chain pins read the composed read's own statement and spell an undefined grandparent scope explicitly
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
1 parent 46905c0 commit 9cee19a

2 files changed

Lines changed: 36 additions & 15 deletions

File tree

‎packages/objectql/src/engine-readable-ids-nested-composition.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ describe('[ADR-0139 D6 as amended] a leaf inside a composed scope is composed to
209209
});
210210

211211
it("the grandparent's scope undefined ⇒ the nested leaf composes with no row restriction; the outer leaf's scope is still the rewritten copy", async () => {
212-
const { svc } = chainSecurity(undefined);
212+
const { svc } = makeSecurity({ scope: (object) => (object === 'rid_parent' ? derivedFrom('rid_grand') : undefined) });
213213
wire(engine, () => svc);
214214
await engine.find('rid_child', { where: branch() });
215215
const outer = inComparandOf(calls[0].ast.where) as any;

‎packages/plugins/plugin-security/src/readable-ids-nested-compose.test.ts‎

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,10 @@ async function boot() {
368368
type Rig = Awaited<ReturnType<typeof boot>>;
369369

370370
const member = (userId: string, set: string) => ({ userId, positions: [], permissions: [set], posture: 'MEMBER' });
371-
const touchesChain = (sql: string) => sql.includes(PARENT) || sql.includes(GRAND);
371+
/** The table a statement's FIRST `from` names — the table it reads, not one inside a subquery of it. */
372+
const fromTable = (sql: string): string | undefined => /^select .*? from `?([a-z0-9_]+)`?/i.exec(sql)?.[1];
373+
/** A statement that reads `table` — not one that reads another table with `table` inside a subquery. */
374+
const readsTable = (table: string) => (sql: string) => fromTable(sql) === table;
372375

373376
describe('[ADR-0139 D6 as amended] a two-level controlled_by_parent chain composes and serves the rows the id-list path serves', () => {
374377
let rig: Rig;
@@ -417,26 +420,44 @@ describe('[ADR-0139 D6 as amended] a two-level controlled_by_parent chain compos
417420
expect(new Set(rig.replacedPerCall)).toEqual(new Set([1, 2]));
418421
});
419422

420-
it('one statement per find and per count on the leaf path: the child\'s select, the parent and the grandparent inside nested subqueries — against the pre-scans of today\'s path', async () => {
423+
it('one statement per find and per count on the leaf path: the child\'s select with the parent and the grandparent inside nested subqueries, and no pre-scan of the parent — against the pre-scans of today\'s path', async () => {
421424
const context = member(HUMAN, SET.human);
422425
for (const [verb, run] of [
423426
['find', () => rig.engine.find(CHILD_LEAF, { context: { ...context } } as never)],
424427
['count', () => rig.engine.count(CHILD_LEAF, { context: { ...context } } as never)],
425428
] as const) {
426429
const statements = await rig.statementsDuring(run);
427-
const chain = statements.filter(touchesChain);
428-
expect(chain, `${verb}: ${statements.join('\n')}`).toHaveLength(1);
429-
expect(chain[0], verb).toMatch(new RegExp(`from .${CHILD_LEAF}.`, 'i'));
430-
expect(chain[0].match(/in \(select/gi) ?? [], `${verb}: ${chain[0]}`).toHaveLength(2);
431-
expect(chain[0], verb).toMatch(new RegExp(`from .${PARENT}.`, 'i'));
432-
expect(chain[0], verb).toMatch(new RegExp(`from .${GRAND}.`, 'i'));
430+
const all = `${verb}:\n${statements.join('\n')}`;
431+
// The read is ONE statement: the child's, the parent inside its subquery
432+
// and the grandparent inside that one.
433+
const reads = statements.filter(readsTable(CHILD_LEAF));
434+
expect(reads, all).toHaveLength(1);
435+
expect(reads[0].match(/in \(select/gi) ?? [], all).toHaveLength(2);
436+
expect(reads[0], all).toMatch(new RegExp(`in \\(select .id. from .${PARENT}. where .grand. in \\(select .id. from .${GRAND}.`, 'i'));
437+
// No statement reads the parent on its own: the leaf stands where the
438+
// pre-resolved id list stood, so the parent is never pre-scanned.
439+
expect(statements.filter(readsTable(PARENT)), all).toHaveLength(0);
440+
// What else the driver ran is the security service's own answering, not
441+
// the read: the real `getReadFilter(PARENT)` still derives PARENT's
442+
// scope through `computeControlledByParentFilter`, whose grandparent
443+
// pre-scan this rig's wrapper then REPLACES with the leaf (it retires
444+
// with the plugin-security switch, card (c)), and plugin-sharing reads
445+
// the share rows to answer the grandparent's scope. Neither is a read
446+
// of the child, the parent or — as a read of its own — the grandparent
447+
// by the composed statement.
448+
for (const sql of statements.filter(readsTable(GRAND))) expect(sql, all).toMatch(/^select .id. from/i);
433449
}
434-
// Today's path pre-scans the grandparent and then the parent under a
435-
// system context before the child's own statement: more than one statement
436-
// touches the chain per find, and none is a subquery.
437-
const today = (await rig.statementsDuring(() => rig.engine.find(CHILD_CBP, { context: { ...context } } as never))).filter(touchesChain);
438-
expect(today.length).toBeGreaterThan(1);
439-
expect(today.some((sql) => /in \(select/i.test(sql))).toBe(false);
450+
// Today's path: the grandparent and then the parent are pre-scanned under
451+
// a system context (`select id from …`), and the child's own statement
452+
// binds the parent ids as a list — no subquery anywhere.
453+
const today = await rig.statementsDuring(() => rig.engine.find(CHILD_CBP, { context: { ...context } } as never));
454+
const todayAll = `today:\n${today.join('\n')}`;
455+
expect(today.filter(readsTable(GRAND)), todayAll).toHaveLength(1);
456+
expect(today.filter(readsTable(PARENT)), todayAll).toHaveLength(1);
457+
const todayReads = today.filter(readsTable(CHILD_CBP));
458+
expect(todayReads, todayAll).toHaveLength(1);
459+
expect(todayReads[0], todayAll).not.toMatch(/in \(select/i);
460+
expect(today.some((sql) => /in \(select/i.test(sql)), todayAll).toBe(false);
440461
});
441462
});
442463

0 commit comments

Comments
 (0)