You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A public_read_write object is only writable by the row's creator — showcase_contributor gets 403 editing a showcase_project it can read, where the access matrix declares edit:true #8023
Found by a retest of access-security.owd-sharing-matrix and access-security.crud-permission-matrix (QA run #7637) against origin/main @ b602d536, rebuilt. This is the only failing cell of a 124-cell matrix re-drive.
Explicitly NOT caused by PR #7792 (the by-id write-visibility fix retested in the same pass) — attribution evidence below. The previous run at 92f26f75 reported this cell as correct (123/124, the single break being the now-fixed #7673), so this looks like a change inside the 92f26f75 → b602d536 window.
Symptom
A showcase_contributor persona can read a seeded showcase_project but cannot edit it:
Message: "You do not have access to this record. Contact the person who owns it, or your administrator, if you need access." — the record-level pre-image sentence, not the object-level one. Reproduced twice; the project's fields are unchanged.
Expected 2xx, because all three inputs say so:
access-matrix.json grants showcase_contributoredit: true on showcase_project;
GET /api/v1/meta/object/showcase_project reports sharingModel: 'public_read_write';
no RLS policy is authored on showcase_project for any position.
The object-level gate still works correctly and is distinguishable: a member_default persona (edit:false) gets a different message — "You do not have permission to perform this action".
Reproduction
Boot stock showcase isolated.
Provision a contributor: POST /api/v1/auth/sign-up/email, then as admin POST /api/v1/data/sys_user_permission_set {user_id, permission_set_id: <showcase_contributor>} and POST /api/v1/data/sys_user_position {user_id, position:'contributor'}, then re-sign-in so the session carries the position.
As that persona, PATCH any seeded showcase_projectit did not create.
Root cause (suspected, mechanism-evidenced)
The platform's own row-level write ownership floor stays composed into Layer 1:
member_default's owner_only_writes — object '*', operation update, using: 'created_by == current_user.id', positions ['org_member'] (packages/plugins/plugin-security/src/platform-ownership-policies.ts, #5492) — remains in effect because ISharingServiceabstains on a public_read_write object ("sharing=not_applicable: Baseline already grants the rows sharing would add"), and per the documented #5492 composition an abstain KEEPS the floor.
Net effect: on a public_read_write object, record-level write is open only to the row's creator, which contradicts the OWD contract.
Two independent confirmations that this is the created_by floor and not a derived select scope:
POST /api/v1/security/explain {object:'showcase_project', operation:'update', userId:<persona>} reports the rls layer as narrows even though showcase_project has zero authored RLS — while operation:'read' reports not_applicable with readFilter: null.
Cross-object, on an object with no app RLS anywhere: a showcase_field_ops_delegatereads an admin-created sys_user_position row (200) but PATCHes it 403, while it can PATCH (200) and DELETE (200) a sys_user_position row it created itself. Readable-but-not-creator ⇒ refused; creator ⇒ allowed.
On showcase_project the contributor holds no select-class RLS at all, so the #7792 derivation (which derives a write scope from the caller's SELECT policies) can only compile a null filter there and cannot narrow anything. The retest also confirmed #7792's own guards positively — legitimate in-scope writes still succeed, the widener directions (#7401 / #6736) are not resurrected (6 dogfood files / 34 tests green, plus the fix's own 17/17 pin), and verify --rls now reports 20 PROVEN / 0 HOLES.
Honest limit on the attribution: a build ablation (rebuilding plugin-security at the parent commit) was not run — the environment forbids mutating repo sources — so the root cause above rests on the mechanism evidence, not on a differential build. Pinning it to a specific merge needs that ablation.
Acceptance criteria
A persona the access matrix grants edit:true on a public_read_write object can PATCH a row it did not create.
The object-level gate is unchanged: a persona with edit:false still gets the object-level refusal, with its distinct message.
The created_by floor still applies where it should — on objects whose OWD does not open writes (private, public_read), a non-owner write stays refused.
Found by a retest of
access-security.owd-sharing-matrixandaccess-security.crud-permission-matrix(QA run #7637) againstorigin/main@b602d536, rebuilt. This is the only failing cell of a 124-cell matrix re-drive.Symptom
A
showcase_contributorpersona can read a seededshowcase_projectbut cannot edit it:Message: "You do not have access to this record. Contact the person who owns it, or your administrator, if you need access." — the record-level pre-image sentence, not the object-level one. Reproduced twice; the project's fields are unchanged.
Expected 2xx, because all three inputs say so:
access-matrix.jsongrantsshowcase_contributoredit: trueonshowcase_project;GET /api/v1/meta/object/showcase_projectreportssharingModel: 'public_read_write';showcase_projectfor any position.The object-level gate still works correctly and is distinguishable: a
member_defaultpersona (edit:false) gets a different message — "You do not have permission to perform this action".Reproduction
POST /api/v1/auth/sign-up/email, then as adminPOST /api/v1/data/sys_user_permission_set {user_id, permission_set_id: <showcase_contributor>}andPOST /api/v1/data/sys_user_position {user_id, position:'contributor'}, then re-sign-in so the session carries the position.PATCHany seededshowcase_projectit did not create.Root cause (suspected, mechanism-evidenced)
The platform's own row-level write ownership floor stays composed into Layer 1:
member_default'sowner_only_writes— object'*', operationupdate,using: 'created_by == current_user.id', positions['org_member'](packages/plugins/plugin-security/src/platform-ownership-policies.ts, #5492) — remains in effect becauseISharingServiceabstains on apublic_read_writeobject ("sharing=not_applicable: Baseline already grants the rows sharing would add"), and per the documented #5492 composition an abstain KEEPS the floor.Net effect: on a
public_read_writeobject, record-level write is open only to the row's creator, which contradicts the OWD contract.Two independent confirmations that this is the
created_byfloor and not a derived select scope:POST /api/v1/security/explain {object:'showcase_project', operation:'update', userId:<persona>}reports the rls layer asnarrowseven thoughshowcase_projecthas zero authored RLS — whileoperation:'read'reportsnot_applicablewithreadFilter: null.showcase_field_ops_delegatereads an admin-createdsys_user_positionrow (200) but PATCHes it 403, while it can PATCH (200) and DELETE (200) asys_user_positionrow it created itself. Readable-but-not-creator ⇒ refused; creator ⇒ allowed.Why this is not PR #7792
On
showcase_projectthe contributor holds no select-class RLS at all, so the #7792 derivation (which derives a write scope from the caller's SELECT policies) can only compile a null filter there and cannot narrow anything. The retest also confirmed #7792's own guards positively — legitimate in-scope writes still succeed, the widener directions (#7401 / #6736) are not resurrected (6 dogfood files / 34 tests green, plus the fix's own 17/17 pin), andverify --rlsnow reports 20 PROVEN / 0 HOLES.Honest limit on the attribution: a build ablation (rebuilding
plugin-securityat the parent commit) was not run — the environment forbids mutating repo sources — so the root cause above rests on the mechanism evidence, not on a differential build. Pinning it to a specific merge needs that ablation.Acceptance criteria
edit:trueon apublic_read_writeobject can PATCH a row it did not create.edit:falsestill gets the object-level refusal, with its distinct message.created_byfloor still applies where it should — on objects whose OWD does not open writes (private,public_read), a non-owner write stays refused.Source
Retest of QA run #7637 (
owd-sharing-matrixclause 3, also surfacing as the single failing cell ofcrud-permission-matrixclause 0) atb602d536.