Skip to content

Commit 0474ea3

Browse files
committed
test(plugin-auth): the self-service route harness runs the by-id stored-row check
The harness's terminal next() stood in for the engine without running the write-image check the security middleware now installs on a by-id update, so the middleware's fail-closed guard refused every own-row write it pins as admitted. The terminal now runs it on the fixture row merged with the payload, through the producer's dispatch predicate, without adding a read to the recorded pre-image wheres. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
1 parent a76a6dc commit 0474ea3

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

‎packages/plugins/plugin-auth/src/sys-user-self-service-route.test.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import { describe, it, expect, vi } from 'vitest';
6262
import { SysUser } from '@objectstack/platform-objects/identity';
6363
import { SecurityPlugin, securityDefaultPermissionSets } from '@objectstack/plugin-security';
64+
import { assertEngineUpdateDispatch } from '@objectstack/objectql';
6465
import {
6566
registerIdentityWriteGuard,
6667
registerManagedUpdateWhitelist,
@@ -204,9 +205,31 @@ async function route(
204205
context,
205206
};
206207

208+
// [#19989] The engine's half of the write gate, which the terminal `next()`
209+
// stands in for. `ObjectQL.update` runs the installed
210+
// `postHookWriteImageCheck` on the row a by-id update writes (the stored row
211+
// merged with the payload) before the statement; a terminal that skipped it
212+
// would be refused fail-closed by the middleware, and would read here as a
213+
// `row-scope` refusal. The row comes straight from the fixture, NOT through
214+
// `findOne`, so `preImageWheres` still records only the middleware's reads.
215+
// The guard below runs after this, so the image carries the pre-guard
216+
// payload; the check this file composes (`sys_user_self`, whose `using` is
217+
// the check: `id == current_user.id`) reads only `id`, which the guard never
218+
// touches. Only the by-id path is modelled, through the producer's own
219+
// dispatch predicate.
220+
const runByIdWriteImageCheck = async () => {
221+
const seam = opCtx.postHookWriteImageCheck;
222+
if (!seam || operation !== 'update') return;
223+
const dispatch = assertEngineUpdateDispatch(opCtx.data, opCtx.options);
224+
if (dispatch.kind !== 'by-id') return;
225+
seam.honoured = true;
226+
const row = ROWS[String(dispatch.id)];
227+
await seam.evaluate(row ? [{ ...row, ...opCtx.data }] : []);
228+
};
229+
207230
const snapshotWrites: Array<{ key: string; value: any }> = [];
208231
try {
209-
await middleware(opCtx, async () => {});
232+
await middleware(opCtx, runByIdWriteImageCheck);
210233
} catch (error: any) {
211234
return {
212235
// The pre-image re-read is the first engine call past the CRUD gate, so

0 commit comments

Comments
 (0)