Skip to content

Commit c0ac779

Browse files
committed
test(security): the delegator-seed double answers its bound and refuses combinators (#15887)
Two findings from the derived gate family, both about the fake engine the on-behalf-of leg reads through — not about the pins: * `check:objectql-double-limit`: core's grants resolution hands every read a `limit` (200 on the `sys_member` legs), and the double ignored it. The bound is now applied AFTER the filter and BY PRESENCE, the shape the gate names. * `check:where-matcher`: `matches` compared a `$`-prefixed key as a FIELD NAME, which matches nothing and says nothing. The double now REFUSES the combinators and operators it does not implement — plain equality is all the delegator resolution ever asks it for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
1 parent 541c0bc commit c0ac779

1 file changed

Lines changed: 21 additions & 2 deletions

File tree

packages/plugins/plugin-security/src/tenant-layer0-verdict-on-operation.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,16 @@ async function boot(opts: {
184184
sys_member: del.memberOf.map((organization_id) => ({ user_id: del.userId, organization_id })),
185185
}
186186
: {};
187+
// Plain equality is all the delegator resolution ever asks for (`{ id }`,
188+
// `{ user_id }`). A combinator read as a FIELD NAME would match nothing and
189+
// say nothing, so this double REFUSES what it does not implement rather than
190+
// answering quietly — `check:where-matcher` refuses exactly the quiet shape.
187191
const matches = (row: Record<string, unknown>, where: Record<string, unknown> | undefined): boolean =>
188-
Object.entries(where ?? {}).every(([k, v]) => row[k] === v);
192+
Object.entries(where ?? {}).every(([k, v]) => {
193+
if (k.startsWith('$')) throw new Error(`fake engine: unsupported combinator ${k}`);
194+
if (v && typeof v === 'object') throw new Error(`fake engine: unsupported operator on '${k}'`);
195+
return row[k] === v;
196+
});
189197
const rowsOf = (object: string, where: Record<string, unknown> | undefined) =>
190198
(tables[object] ?? []).filter((r) => matches(r, where));
191199
const services: Record<string, unknown> = {
@@ -194,7 +202,18 @@ async function boot(opts: {
194202
registerMiddleware: (mw: any) => middlewares.push(mw),
195203
getSchema: (name: string) => SCHEMAS[name],
196204
findOne: vi.fn(async (object: string, o: any) => rowsOf(object, o?.where)[0] ?? null),
197-
...(del ? { find: async (object: string, o: any) => rowsOf(object, o?.where) } : {}),
205+
...(del
206+
? {
207+
// The caller's bound is applied AFTER the filter and BY PRESENCE:
208+
// core's grants resolution hands every one of these reads a `limit`,
209+
// and a double that silently ignores it cannot report what the real
210+
// engine would (`check:objectql-double-limit`).
211+
find: async (object: string, o: any) => {
212+
const rows = rowsOf(object, o?.where);
213+
return typeof o?.limit === 'number' ? rows.slice(0, o.limit) : rows;
214+
},
215+
}
216+
: {}),
198217
},
199218
metadata: {
200219
get: async (_type: string, name: string) => SCHEMAS[name],

0 commit comments

Comments
 (0)