Skip to content

Commit 8767201

Browse files
committed
fix(plugin-security)!: a caller who resolves no permission set is not served a capability-gated field
The zero-set stand-in (`resolveCallerPosture`) now carries the posture's per-field capability contract, beside the masking rules it carries since the zero-set masking fix. Every reader takes the fold it already takes (`foldFieldRequiredPermissions`), so for a non-system caller who resolves no permission set a field that declares `requiredPermissions` is not served, not queryable and refused in a write payload, as it declares. The explain engine already reported it hidden; the record doors now agree. The step 2.5 field write gate is no longer gated on a resolved set, as 2.5a and 2.5b are not; its verdict is one helper (`computeForbiddenFieldWrites`) read by the middleware and by `canWriteObject`, whose zero-set arm now asks it for a caller carrying a principal. `getWritableFields` stays its complement. The object's own capability contract is not carried: this caller's object admission is unchanged. Every caller who resolves a set reads the posture as before. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
1 parent d49cc26 commit 8767201

2 files changed

Lines changed: 148 additions & 91 deletions

File tree

‎packages/plugins/plugin-security/src/get-writable-fields.test.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,13 @@ describe('getWritableFields — the answers the contract names', () => {
158158
expect(await plugin.getWritableFields('invoice', { isSystem: true })).toEqual(FIELDS);
159159
});
160160

161-
it('no permission sets resolved: the full field set, as the middleware skips its write gate', async () => {
162-
const { plugin } = await boot([], { noBaseline: true });
163-
expect(await plugin.getWritableFields('invoice', WRITER_CTX)).toEqual(FIELDS);
161+
it('no permission sets resolved: the full field set minus the capability-gated field, which the write gate refuses', async () => {
162+
const { plugin, middleware } = await boot([], { noBaseline: true });
163+
// [#21063] The caller holds no capability, so `margin`'s
164+
// `requiredPermissions` refuses it on write; nothing else is refused.
165+
expect(await plugin.getWritableFields('invoice', WRITER_CTX)).toEqual(FIELDS.filter((f) => f !== 'margin'));
166+
expect(await middlewareAdmits(middleware, 'update', WRITER_CTX, { margin: PAYLOAD_VALUE.margin })).toBe(false);
167+
expect(await middlewareAdmits(middleware, 'update', WRITER_CTX, { secret: PAYLOAD_VALUE.secret })).toBe(true);
164168
});
165169

166170
it('an unresolvable object is no answer (undefined), not an empty one', async () => {

0 commit comments

Comments
 (0)