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 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
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:
The platform ownership floor owner_only_writes is declared object '*', so it applies to the detail like any other object.
Step 2.7 drops the floor only when resolveSharingWriteVerdict(...) === 'allow'.
That verdict comes from SharingService.checkEdit, which returns abstain for any object whose effectiveSharingModel is public — and controlled_by_parent maps to public there.
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.
Possible directions (not a recommendation — this needs triage)
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.
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.
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_parentdetail, 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) — beforeassertControlledByParentWriteruns at all — regardless of whether the caller may edit the master. The refusal isrecord_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:
owner_only_writesis declaredobject '*', so it applies to the detail like any other object.resolveSharingWriteVerdict(...) === 'allow'.SharingService.checkEdit, which returnsabstainfor any object whoseeffectiveSharingModelispublic— andcontrolled_by_parentmaps topublicthere.publicearly return sits above themodifyAllRecordsbranch (hasModifyAllBypassis asked last, at step 3), so the bypass is never consulted for such an object.Net: on a
controlled_by_parentdetail the floor is unconditional and unwidenable — not by ownership depth, not by anedit-levelsys_record_share, not by Modify All Data.Reproduction (unit, in-memory, no server)
Fixture: master
crm_campaign(private,owner_id), detailcrm_campaign_member(controlled_by_parent), a detail rowcreated_byADMIN under a campaign owned by MKT, and the shippedmember_defaultseed for the floor. Driving the realSecurityPluginmiddleware plus the real sharing middleware:The middle line is the sharp one:
admin_setcarriesviewAllRecords: true, modifyAllRecords: trueon 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_parentmeans "access is derived from the master". The detail declares nothing about who may write it —platform-ownership-policies.tssays exactly that, as the reason thepublic_read_writefloor exemption must NOT be extended tocontrolled_by_parent: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
modifyAllRecordsstill does not widen a by-id write on an object with NO owner field (sharing abstains, the platformcreated_byfloor holds) #6698 (closed) — "modifyAllRecordsstill does not widen a by-id write on an object with NO owner field (sharing abstains, the platformcreated_byfloor holds)". Same mechanism, different trigger: there the abstain came from a missing owner field, here from thecontrolled_by_parentposture. Thepublic-model early return is a separate route into the same dead end.public_read_writeobject is only writable by the row's creator —showcase_contributorgets 403 editing ashowcase_projectit can read, where the access matrix declaresedit:true#8023 (closed) — added the OWD floor exemption forpublic_read_writeand explicitly declined to extend it tocontrolled_by_parent. That decision is the reason this case exists; whether it should be revisited for details specifically is the question here.Possible directions (not a recommendation — this needs triage)
controlled_by_parentobjects, on the grounds thatassertControlledByParentWriteis that object's declared row-level write gate and already runs on the same operation.checkEditso themodifyAllRecordsbypass is consulted before thepublic-model early return — narrower, and fixes only the superuser arm, leaving ordinary cross-creator writes refused.controlled_by_parentdetails are creator-only for by-id updates — in which case Apublic_read_writeobject is only writable by the row's creator —showcase_contributorgets 403 editing ashowcase_projectit can read, where the access matrix declaresedit: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