Measured while running the #6736 bulk-path probe (PR #7274). Filed unassigned, out of that PR's scope, for triage.
What was measured
ISecurityService.checkAuthoredRowWrite resolves its verdict by re-reading the target row through the caller's own execution context:
const parts = [{ id: recordId }, ...(layer0 ? [layer0] : []), layer1];
const row = await this.ql.findOne(object, { where: { $and: parts }, context });
return row ? 'admit' : 'abstain';
packages/plugins/plugin-security/src/security-plugin.ts, in checkAuthoredRowWrite.
That findOne re-enters the middleware chain, so plugin-sharing's read filter applies. buildReadFilter scopes a private-OWD object to owner-match OR shares — so a cross-owner row is invisible to the caller, findOne returns null, and the verdict is abstain rather than admit. The middleware's refusal then stands and the write is refused 403, even though the app declared a widener that admits the row by name.
Measured end-to-end on the real stack (bootStack + real SecurityPlugin + real SharingServicePlugin + real ObjectQL engine). Two objects, identical in every respect except the OWD, same app-authored widener (operation: 'update', using: "stage == 'open'"), same principal, same cross-owner row shape:
| object |
OWD |
checkAuthoredRowWrite on the cross-owner admitted row |
by-id PATCH |
probe_note |
public_read |
admit |
200, row changes |
probe_secret |
private |
abstain |
403, row unchanged |
Control ruling out an inert or mis-parsed widener: on the same private object, checkAuthoredRowWrite for the caller's own open row answers admit. The declaration is live there; it is the cross-owner read that removes it.
Why this matters
private is the posture the widener surface was built for. #5493's own measured defect is described as occurring exactly where record sharing enforces — "a private/public_read OWD and an owner_id field" — and the sharing middleware's deferral comment says the probe exists so that gate does not refuse a row an app-authored widener admits. On private, it still refuses: the deferral is asked, and structurally cannot answer admit for any row the caller cannot already read.
So the shipped by-id half functions on read-open objects and stands down on read-closed ones — discriminated by a property the widener's author never mentions, which is the same shape #5493 was filed about one layer up.
The second half: the unit test cannot see this
packages/plugins/plugin-security/src/row-write-widener-composition.test.ts asserts
await expect(
stack.security.checkAuthoredRowWrite('crm_opportunity', OPP_THEIRS.id, 'update', WIDENED_CTX),
).resolves.toBe('admit');
on crm_opportunity, which is declared sharingModel: 'private' — the posture that answers abstain in production. It passes because that file's makeEngine() fake implements find as a direct row filter with no middleware chain, so the re-read inside checkAuthoredRowWrite is never scoped by the sharing read filter. The double is looser than the producer on precisely the axis the method's verdict depends on, so the test is green over behaviour the real stack does not have.
This half is actionable regardless of how the first half is ruled: if the abstention is intended (fail-closed, "you may not write what you cannot read"), the unit test is asserting the opposite of the contract and should be inverted plus the intent written into the method's doc; if it is not intended, the test is why it shipped unnoticed.
Two readings, offered honestly
I measured behaviour, not intent, and both readings are defensible:
- Intended fail-closed. Resolving the verdict under the caller's own scope is a deliberate refusal to let a write reach rows the caller cannot see. Then the gap is documentation plus the misleading test — and app authors need to be told that an update-widener on a
private object requires matching read widening (viewAllRecords, a read policy, or a share) to do anything.
- Unintended. The verdict is meant to answer "does the declaration admit this row", which is a question about the row and the policy, not about the caller's read scope; re-reading under the caller's context silently folds a read decision into a write decision. Then the read should be resolved under a scope that can actually see the row while the write decision stays with the pre-image gate.
Deliberately not choosing here — the difference is a contract question about what checkAuthoredRowWrite means, which belongs to the maintainer, not to the agent that tripped over it.
Repro
The measurement was taken with a throwaway variant of the probe added in PR #7274 (a second object identical except sharingModel: 'private', plus a case logging both verdicts and both HTTP statuses). It was deliberately not committed — that PR is scoped to the bulk path. The committed harness in packages/qa/dogfood/test/bulk-widener-probe.dogfood.test.ts is two dozen lines away from re-taking it.
Measured while running the #6736 bulk-path probe (PR #7274). Filed unassigned, out of that PR's scope, for triage.
What was measured
ISecurityService.checkAuthoredRowWriteresolves its verdict by re-reading the target row through the caller's own execution context:packages/plugins/plugin-security/src/security-plugin.ts, incheckAuthoredRowWrite.That
findOnere-enters the middleware chain, soplugin-sharing's read filter applies.buildReadFilterscopes aprivate-OWD object to owner-match OR shares — so a cross-owner row is invisible to the caller,findOnereturns null, and the verdict isabstainrather thanadmit. The middleware's refusal then stands and the write is refused 403, even though the app declared a widener that admits the row by name.Measured end-to-end on the real stack (
bootStack+ real SecurityPlugin + real SharingServicePlugin + real ObjectQL engine). Two objects, identical in every respect except the OWD, same app-authored widener (operation: 'update',using: "stage == 'open'"), same principal, same cross-owner row shape:checkAuthoredRowWriteon the cross-owner admitted rowprobe_notepublic_readadmitprobe_secretprivateabstainControl ruling out an inert or mis-parsed widener: on the same
privateobject,checkAuthoredRowWritefor the caller's own open row answersadmit. The declaration is live there; it is the cross-owner read that removes it.Why this matters
privateis the posture the widener surface was built for. #5493's own measured defect is described as occurring exactly where record sharing enforces — "aprivate/public_readOWD and anowner_idfield" — and the sharing middleware's deferral comment says the probe exists so that gate does not refuse a row an app-authored widener admits. Onprivate, it still refuses: the deferral is asked, and structurally cannot answeradmitfor any row the caller cannot already read.So the shipped by-id half functions on read-open objects and stands down on read-closed ones — discriminated by a property the widener's author never mentions, which is the same shape #5493 was filed about one layer up.
The second half: the unit test cannot see this
packages/plugins/plugin-security/src/row-write-widener-composition.test.tsassertson
crm_opportunity, which is declaredsharingModel: 'private'— the posture that answersabstainin production. It passes because that file'smakeEngine()fake implementsfindas a direct row filter with no middleware chain, so the re-read insidecheckAuthoredRowWriteis never scoped by the sharing read filter. The double is looser than the producer on precisely the axis the method's verdict depends on, so the test is green over behaviour the real stack does not have.This half is actionable regardless of how the first half is ruled: if the abstention is intended (fail-closed, "you may not write what you cannot read"), the unit test is asserting the opposite of the contract and should be inverted plus the intent written into the method's doc; if it is not intended, the test is why it shipped unnoticed.
Two readings, offered honestly
I measured behaviour, not intent, and both readings are defensible:
privateobject requires matching read widening (viewAllRecords, a read policy, or a share) to do anything.Deliberately not choosing here — the difference is a contract question about what
checkAuthoredRowWritemeans, which belongs to the maintainer, not to the agent that tripped over it.Repro
The measurement was taken with a throwaway variant of the probe added in PR #7274 (a second object identical except
sharingModel: 'private', plus a case logging both verdicts and both HTTP statuses). It was deliberately not committed — that PR is scoped to the bulk path. The committed harness inpackages/qa/dogfood/test/bulk-widener-probe.dogfood.test.tsis two dozen lines away from re-taking it.