Skip to content

Commit 98b0e5b

Browse files
committed
test(plugin-security): pin the RLS write check on a lone scalar written to a declared multi-valued column (red on main)
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent be5a83c commit 98b0e5b

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

‎packages/plugins/plugin-security/src/rls-check-stored-form.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ async function boot(makeDriver: () => Driver, predicate: string) {
123123
due_on: { name: 'due_on', type: 'date' },
124124
due_at: { name: 'due_at', type: 'datetime' },
125125
start_time: { name: 'start_time', type: 'time' },
126+
tags: { name: 'tags', type: 'tags' },
127+
owners: { name: 'owners', type: 'select', multiple: true, options: [{ label: 'X', value: 'x' }, { label: 'XY', value: 'xy' }] },
126128
},
127129
},
128130
],
@@ -199,6 +201,16 @@ const CELLS: Cell[] = [
199201
// Control: a TEXT column is judged as written, whatever its value looks like.
200202
{ predicate: "record.title == '2026-01-05'", column: 'title', value: '2026-01-05T15:00:00Z', stored: '2026-01-05T15:00:00Z', admitted: false },
201203
{ predicate: "record.title > '2026-01-05'", column: 'title', value: '2026-01-05T15:00:00Z', stored: '2026-01-05T15:00:00Z', admitted: true },
204+
// [#21238] A declared multi-valued column: a lone scalar is stored as a one-member list.
205+
{ predicate: "record.tags.contains('x')", column: 'tags', value: 'x', stored: ['x'], admitted: true },
206+
{ predicate: "record.tags.contains('x')", column: 'tags', value: 'xy', stored: ['xy'], admitted: false },
207+
{ predicate: "record.tags.contains('x')", column: 'tags', value: ['x'], stored: ['x'], admitted: true },
208+
{ predicate: "!record.tags.contains('x')", column: 'tags', value: 'x', stored: ['x'], admitted: false },
209+
{ predicate: "record.owners.contains('x')", column: 'owners', value: 'x', stored: ['x'], admitted: true },
210+
{ predicate: "record.owners.contains('x')", column: 'owners', value: 'xy', stored: ['xy'], admitted: false },
211+
// Control: a TEXT column keeps its scalar, and `contains` stays a substring test.
212+
{ predicate: "record.title == 'x'", column: 'title', value: 'x', stored: 'x', admitted: true },
213+
{ predicate: "record.title.contains('x')", column: 'title', value: 'xy', stored: 'xy', admitted: true },
202214
];
203215

204216
describe("formula's whole-day copy is out of reach in this file", () => {
@@ -235,6 +247,29 @@ for (const [driverName, makeDriver] of DRIVERS) {
235247
.toEqual(DENIED);
236248
expect((await r.storedRow('u'))?.due_on).toBe('2026-01-05');
237249
});
250+
251+
it("[#21238] a by-id update judges a lone scalar on a multi-valued column as its stored list: 'x' admitted, 'xy' 403 and unchanged", async () => {
252+
const r = await boot(makeDriver, "record.tags.contains('x')");
253+
await r.engine.insert(r.OBJ, { id: 'u', tags: ['x', 'z'] }, { context: SYS_CTX } as never);
254+
expect(await outcome(r.engine.update(r.OBJ, { tags: 'x' }, { where: { id: 'u' }, context: r.caller } as never)))
255+
.toBe('admitted');
256+
expect((await r.storedRow('u'))?.tags).toEqual(['x']);
257+
expect(await outcome(r.engine.update(r.OBJ, { tags: 'xy' }, { where: { id: 'u' }, context: r.caller } as never)))
258+
.toEqual(DENIED);
259+
expect((await r.storedRow('u'))?.tags).toEqual(['x']);
260+
expect(await r.shownTo('u')).toBe(true);
261+
});
262+
263+
it("[#21238] a predicate update judges a lone scalar on a multi-valued column as its stored list: 'x' admitted, 'xy' 403 and unchanged", async () => {
264+
const r = await boot(makeDriver, "record.tags.contains('x')");
265+
await r.engine.insert(r.OBJ, { id: 'p', title: 'batch', tags: ['x'] }, { context: SYS_CTX } as never);
266+
expect(await outcome(r.engine.update(r.OBJ, { tags: 'x' }, { where: { title: 'batch' }, multi: true, context: r.caller } as never)))
267+
.toBe('admitted');
268+
expect((await r.storedRow('p'))?.tags).toEqual(['x']);
269+
expect(await outcome(r.engine.update(r.OBJ, { tags: 'xy' }, { where: { title: 'batch' }, multi: true, context: r.caller } as never)))
270+
.toEqual(DENIED);
271+
expect((await r.storedRow('p'))?.tags).toEqual(['x']);
272+
});
238273
});
239274
}
240275

0 commit comments

Comments
 (0)