Skip to content

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

Description

@baozhoutao

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:

GET   /api/v1/data/showcase_project/8jdgvvB-1vbptKgO   → 200
PATCH /api/v1/data/showcase_project/8jdgvvB-1vbptKgO
      {"description":"contributor edit"}                → 403 PERMISSION_DENIED

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_contributor edit: 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

  1. Boot stock showcase isolated.
  2. 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.
  3. As that persona, PATCH any seeded showcase_project it 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 ISharingService abstains 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:

  1. 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.
  2. Cross-object, on an object with no app RLS anywhere: a showcase_field_ops_delegate reads 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.

Why this is not PR #7792

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.
  • The fix(plugin-security): require by-id write targets to be within the caller's readable set under select-only RLS #7792 by-id write-visibility behaviour is not regressed (a contributor still cannot write a row outside its select scope).

Source

Retest of QA run #7637 (owd-sharing-matrix clause 3, also surfacing as the single failing cell of crud-permission-matrix clause 0) at b602d536.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions