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
Observation-class finding, filed from the #5492/#5491 paired PR (#6684), where it is pinned as deliberate measured behaviour rather than left implicit. Filing it so the residual is graded by triage instead of living only in a test comment.
Measured
PR #6684 makes the row-level write pre-image gate consult ISharingService's tri-state verdict, so modifyAllRecords and edit-level shares finally widen by-id writes. On an object with no owner_id field, they still do not:
object: no `owner_id` column, sharingModel 'private', ordinary posture (no access.default)
principal: profile with viewAllRecords + modifyAllRecords on that object
PATCH a row created by someone else
-> sharing.checkEdit(...) = 'abstain'
-> platform floor `created_by == current_user.id` stays
-> 403 "[Security] … (row-level security)"
Pinned as a passing case in packages/plugins/plugin-security/src/row-write-widener-composition.test.ts ("an abstention does not become permission for a Modify-All holder either").
Three accepted decisions all point the same way, and the PR deliberately did not override any of them:
checkEdit abstains before it ever asks about the bypass — if (!hasOwnerField(schema)) return 'abstain' precedes the hasModifyAllBypass branch (plugin-sharing/src/sharing-service.ts). Record sharing does not enforce on owner-less rows at all.
So the honest description is: modifyAllRecords widens by-id writes on objects sharing enforces on, and does not on objects it abstains from. Author-defined objects without an owner_id column are the common shape of the second group.
Why it may still deserve a decision
packages/spec/src/security/permission.zod.ts describes modifyAllRecords as "Super-user write access. Bypasses Sharing Rules and Ownership checks." On an owner-less object it bypasses neither — the created_by floor is an ownership check written as RLS, and it wins. That is a declared ≠ enforced residual of exactly the class ADR-0049 targets, even though every individual decision producing it is sound.
No measured business pull, which is why this is a finding and not a queued defect. HotCRM's four probed objects all carry owner fields (their reads widened 43/43 under VAMA, and share rows materialised — both require the owner anchor), so #5492's reported symptom is fully covered by #6684. Nothing in the acceptance sweep exercises this cell.
Options, if triage decides to price it
A — leave as is, fix the declaration. Amend the modifyAllRecords description to say the bypass covers ownership as sharing computes it, and that a platform ownership floor on an owner-less object still applies. Zero risk, closes the wording gap; the capability surface does not grow.
B — let the sharing service answer the bypass before abstaining. Move the hasModifyAllBypass probe ahead of the hasOwnerField early-return in checkEdit/checkDelete, so an owner-less row returns allow for a bypass holder and abstain for everyone else. Keeps one implementation and one authority; needs care that it cannot re-open the E2 fail-open for non-holders. Touches plugin-sharing + the ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428 contract prose.
Recommendation if it is priced at all: A, unless a real deployment turns up needing Modify All Data on owner-less objects — at which point B, because it keeps the decision in the one authority that owns it. No pm:queue.
Observation-class finding, filed from the #5492/#5491 paired PR (#6684), where it is pinned as deliberate measured behaviour rather than left implicit. Filing it so the residual is graded by triage instead of living only in a test comment.
Measured
PR #6684 makes the row-level write pre-image gate consult
ISharingService's tri-state verdict, somodifyAllRecordsandedit-level shares finally widen by-id writes. On an object with noowner_idfield, they still do not:Pinned as a passing case in
packages/plugins/plugin-security/src/row-write-widener-composition.test.ts("an abstention does not become permission for a Modify-All holder either").Why it is currently correct, not a bug in #6684
Three accepted decisions all point the same way, and the PR deliberately did not override any of them:
checkEditabstains before it ever asks about the bypass —if (!hasOwnerField(schema)) return 'abstain'precedes thehasModifyAllBypassbranch (plugin-sharing/src/sharing-service.ts). Record sharing does not enforce on owner-less rows at all.abstainmust fall back to the floor — that is the whole point of ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428's tri-state, and Row-level write gate consults neithermodifyAllRecordsnorsys_record_share.access_level— both declared write-widening mechanisms are inert #5492's E2 experiment measured the cost of reading it as permission: an ordinary member's cross-creator UPDATE on an owner-less object went 403 → 200. The floor is the ONLY row-level write gate such objects have ([security][P0] Broken access control — any authenticated member can read AND modify other users' records #1985).posturePermits = isPrivate / tenancyDisabled / isBetterAuthManaged). Widening this cell from the security side would mean re-deriving the bypass there — the second implementation of one contract that Row-level write gate consults neithermodifyAllRecordsnorsys_record_share.access_level— both declared write-widening mechanisms are inert #5492's first dev stopped atneeds_decisionover, and that feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564's §7 prescription explicitly forbids.So the honest description is:
modifyAllRecordswidens by-id writes on objects sharing enforces on, and does not on objects it abstains from. Author-defined objects without anowner_idcolumn are the common shape of the second group.Why it may still deserve a decision
packages/spec/src/security/permission.zod.tsdescribesmodifyAllRecordsas "Super-user write access. Bypasses Sharing Rules and Ownership checks." On an owner-less object it bypasses neither — thecreated_byfloor is an ownership check written as RLS, and it wins. That is adeclared ≠ enforcedresidual of exactly the class ADR-0049 targets, even though every individual decision producing it is sound.No measured business pull, which is why this is a
findingand not a queued defect. HotCRM's four probed objects all carry owner fields (their reads widened 43/43 under VAMA, and share rows materialised — both require the owner anchor), so #5492's reported symptom is fully covered by #6684. Nothing in the acceptance sweep exercises this cell.Options, if triage decides to price it
modifyAllRecordsdescription to say the bypass covers ownership as sharing computes it, and that a platform ownership floor on an owner-less object still applies. Zero risk, closes the wording gap; the capability surface does not grow.hasModifyAllBypassprobe ahead of thehasOwnerFieldearly-return incheckEdit/checkDelete, so an owner-less row returnsallowfor a bypass holder andabstainfor everyone else. Keeps one implementation and one authority; needs care that it cannot re-open the E2 fail-open for non-holders. Touchesplugin-sharing+ the ISharingService 写判定补三态(放行/不表态/拒绝)—— #5492 裁决 B 案的 step1,二态 canEdit 已实测产出 fail-open #6428 contract prose.Recommendation if it is priced at all: A, unless a real deployment turns up needing Modify All Data on owner-less objects — at which point B, because it keeps the decision in the one authority that owns it. No
pm:queue.