Skip to content

modifyAllRecords still does not widen a by-id write on an object with NO owner field (sharing abstains, the platform created_by floor holds) #6698

Description

@os-zhuang

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").

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:

  1. checkEdit abstains before it ever asks about the bypassif (!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.
  2. abstain must 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 neither modifyAllRecords nor sys_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).
  3. ADR-0066 ① withholds the superuser RLS short-circuit on an ordinary business posture (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 neither modifyAllRecords nor sys_record_share.access_level — both declared write-widening mechanisms are inert #5492's first dev stopped at needs_decision over, and that feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564's §7 prescription explicitly forbids.

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

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions