Skip to content

A controlled_by_parent detail's own ownership floor is never droppable, so a cross-creator by-id UPDATE of a child is refused before the master gate — Modify All Data included #8757

Description

@qq9340100

Found while implementing #8679 (PR #8758). Not fixed there — it is a different gate with a different guard, so it is recorded rather than ridden along. #8679's acceptance cases deliberately hold the detail's floor constant so that card's verdicts stay attributable to the master gate alone.

What was measured

On an ADR-0055 controlled_by_parent detail, a by-id UPDATE of a child row created by another user is refused at the by-id write pre-image gate (step 2.7) — before assertControlledByParentWrite runs at all — regardless of whether the caller may edit the master. The refusal is record_access_denied ("You do not have access to this record…"), not the master-editability sentence.

The mechanism, and why no widening mechanism can reach it:

  1. The platform ownership floor owner_only_writes is declared object '*', so it applies to the detail like any other object.
  2. Step 2.7 drops the floor only when resolveSharingWriteVerdict(...) === 'allow'.
  3. That verdict comes from SharingService.checkEdit, which returns abstain for any object whose effectiveSharingModel is public — and controlled_by_parent maps to public there.
  4. The public early return sits above the modifyAllRecords branch (hasModifyAllBypass is asked last, at step 3), so the bypass is never consulted for such an object.

Net: on a controlled_by_parent detail the floor is unconditional and unwidenable — not by ownership depth, not by an edit-level sys_record_share, not by Modify All Data.

Reproduction (unit, in-memory, no server)

Fixture: master crm_campaign (private, owner_id), detail crm_campaign_member (controlled_by_parent), a detail row created_by ADMIN under a campaign owned by MKT, and the shipped member_default seed for the floor. Driving the real SecurityPlugin middleware plus the real sharing middleware:

update marketing mem_mkt_byadmin -> refused: "You do not have access to this record…"
update admin_set mem_mkt -> refused: "You do not have access to this record…"
update admin_set mem_admin_selfmade -> refused: "You do not have access to this record…"

The middle line is the sharp one: admin_set carries viewAllRecords: true, modifyAllRecords: true on both objects and is still refused on a detail row it did not personally create, under a master it can fully edit.

Why it looks wrong

controlled_by_parent means "access is derived from the master". The detail declares nothing about who may write it — platform-ownership-policies.ts says exactly that, as the reason the public_read_write floor exemption must NOT be extended to controlled_by_parent:

controlled_by_parent derives its access from the MASTER record, which has its own OWD and its own gate (assertControlledByParentWrite) — the detail declares nothing about who may write it

That reasoning justifies not treating the detail as org-wide-open. But the current behaviour goes further than "not open": it makes the detail creator-only, which is a row-level write rule the detail also never declared, and one the master gate cannot override. Two gates now answer the same write, and the stricter one is the one derived from nothing the author wrote.

Note the asymmetry that makes this easy to miss: an INSERT does not go through the pre-image gate at all, so creating a child under an editable master works; only later by-id updates of someone else's child are refused. The read path is unaffected.

Prior art, and why this is not a duplicate

Possible directions (not a recommendation — this needs triage)

  1. Let the master gate be authoritative for details: skip the platform ownership floor at step 2.7 for controlled_by_parent objects, on the grounds that assertControlledByParentWrite is that object's declared row-level write gate and already runs on the same operation.
  2. Reorder checkEdit so the modifyAllRecords bypass is consulted before the public-model early return — narrower, and fixes only the superuser arm, leaving ordinary cross-creator writes refused.
  3. Accept it as intended and document that controlled_by_parent details are creator-only for by-id updates — in which case 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's comment should say so, because today it reads as though the master gate governs.

Option 1 changes a permission boundary and should not be taken without a maintainer ruling.


Filed unassigned and unlabeled for triage. Related: #8679, PR #8758.

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions