Repository navigation
docs(permissions): a write widener reaches only rows the caller can read - #19880
Conversation
The by-id write pre-image gate re-reads the target under the caller's own
read scope, and that is the ruled, intended behaviour ("you may not modify
what you cannot see"). The RLS and sharing pages said an authored update
policy "widens exactly as written", which on a private-OWD object sends
authors straight into a 403.
- rls.mdx: state the read floor for write targets, and add the known
limitation that app-authored wideners do nothing on a private object
without a matching read grant.
- sharing-rules.mdx: the supervisor recipe now says it needs a read grant on
private objects and shows the paired spelling (viewAllRecords / readScope
depth, or a read-level sharing rule); a select policy cannot supply it.
- permissions-matrix.mdx: the bypass is also gated by deployment posture;
under the default single posture the org-admin auto-grant is
organization_admin_no_bypass (ADR-0105 D4).
Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: An isolated reviewer ran at the served tier. It was given only the card #7401, the rulings 5791225941 and 5791407913, and this PR. It did not see the dispatch order or this seat's conclusions. The ① Derived judgments15 behavioural claims were checked against source. All 15 are RIGHT. Key evidence:
Advisory only, not blocking:
② Semver levelNone. The diff is 3 files under ③ Boundary flagsInside the ruling. Exactly the three named pages are touched, and Implemented-by: VERDICT: PASS Generated by Claude Code |
Closes #7401
Clause-②: no
Docs-only. Zero code changes, zero
packages/specedits, nothing undercontent/docs/releases/**.What changed
The maintainer ruled reading 1 on this card: a caller may not write a row they cannot read, even when an app-authored write policy admits it by predicate. The
plugin-securityby-id write pre-image gate keeps re-reading the target under the caller's own read scope. The docs said the opposite ("widens exactly as written"), so authors following them hit a 403 onprivateobjects. This PR brings the four named spots onto the ruled behaviour.content/docs/permissions/rls.mdx: the "widens exactly as written" sentence now says it decides the update filter alone. A new paragraph states the read floor: a single-recordupdate/deletere-reads its target under the caller's own read scope. OnprivateOWD, anupdatewidener therefore reaches only rows the caller can already read. Awarncallout carries the known limitation and names the read grants that actually work.content/docs/permissions/sharing-rules.mdx: the recipe "owners edit their own records, supervisors edit all" now says it needs a matching read grant onprivateobjects. It shows the paired spelling (object permission withviewAllRecordsplus theupdateRLS policy in the same set), and it names a read-level sharing rule or record share as the alternative read half.privateOWD without a matching read grant") is written into both pages above. It is not in release notes.content/docs/permissions/permissions-matrix.mdx: the bypass-posture paragraph now covers deployment posture too. Under the default wall-lesssingleposture,auto-org-admin-grantgives org owners and adminsorganization_admin_no_bypass(ADR-0105 D4), notorganization_admin. So onsinglean org admin is not short-circuited, even on a better-auth-managed object.Verified against source (at base
b940f32a56)packages/plugins/plugin-security/src/security-plugin.ts, step 2.7. It callsthis.ql.findOne(object, { where: { $and: [{ id }, ...writeParts] }, context: opCtx.context })under the caller's context, and a null result throwsPermissionDeniedError. The dogfood pinpackages/qa/dogfood/test/authored-row-write-scope.dogfood.test.tscase[E2E private]asserts that 403.privatecomes frompackages/plugins/plugin-sharing/src/sharing-service.tsbuildReadFilter. It is owner-match at the caller's__readScopedepth, OR record shares whose recipient is that user. It returns null when the read depth isorg, andpermission-evaluator.tsgetEffectiveScope('read')answersorgforviewAllRecords/modifyAllRecords. Sharing rules expand a position recipient to users (sharing-rule-service.tsexpandRecipient).packages/plugins/plugin-security/src/objects/default-permission-sets.tsderiveWallLessOrgAdmin, which strips both bits from the wildcard, and fromauto-org-admin-grant.tsorgAdminSetNameForPosture, which returns the no-bypass set unless the posture enforces a wall. The posture defaults tosinglewhen no org-scoping service is registered (security-plugin.ts, thetenancyPosturefield).One deviation from the ruling's wording (please confirm)
The ruling lists three read grants as its examples: a
selectpolicy,viewAllRecords, or sharing. On aprivateobject aselectpolicy is not a read grant. The sharing read filter and the RLS read filter both AND into the query (sharing-plugin.tsread branch,composeAnd), and nothing defers the sharing read filter to an authoredselectpolicy. RLS only narrows, asrls.mdxalready says in "RLS narrows what the earlier layers already allowed; it never widens". Documentingselectas the fix would send authors into the same 403. So the pages nameviewAllRecordsor areadScopedepth, or a read-level sharing rule or record share, and they say explicitly that an extraselectpolicy does not work. The ruling's substance is unchanged: widen read to match.Acceptance notes
permissions-matrix.mdxcallout still says "The built-inadmin_full_access/organization_adminsets therefore carryallowExport: true".default-permission-sets.tssays neither set grants export any more (theallowExportcomments on the admin wildcard and the org-admin set). It is left for a separate card so this PR stays within the ruled four items.Tests
Local gates were derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat head3fc2c9acc9. There were 44 commands, all run, and all exit 0.check:docs-transcript-driftandcheck:skill-examplesfirst refused with exit 3 (PREREQUISITE NOT MET) and were re-run green after building@objectstack/lint...and@objectstack/client-react...under the verify lock.--ranreconciliation: 44 derived, 44 run, 0 unrun. The page-relevant gates arecheck:doc-anchors,check:doc-authoring,check:docs,check:doc-security-posture,check:docs-single-h1,check:nul-bytesandcheck:doc-frontmatter, all green. Build Docs and the typecheck lanes are left to CI.No changeset.
content/docs/**ships only in the privateapps/docspackage, so this PR carriesskip-changeset.Generated by Claude Code