From fec5f6363917e526ad414a5784cd6e67e809fed1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 04:32:25 +0000 Subject: [PATCH] =?UTF-8?q?docs(permissions):=20clarify=20View/Modify=20Al?= =?UTF-8?q?l=20=C3=97=20OWD=20interaction=20and=20posture-gated=20RLS=20by?= =?UTF-8?q?pass=20(#3005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers the #3005 clarification: under public_read_write the sharing baseline is already org-wide, so viewAllRecords has nothing to widen on that layer; and the super-user RLS short-circuit is posture-gated (ADR-0066 D2 revised ①) — on ordinary tenant business objects the member_default owner-scoped write policies still narrow org_member holders even when modifyAllRecords is granted. Neither bit is an enterprise capability; only the hierarchy depth scopes are enterprise-resolved (ADR-0057). Adds the supervisor-writes-all recipe. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019jw7PHSUa3rtRTnK6dwedX --- .../docs/permissions/permissions-matrix.mdx | 11 +++++ content/docs/permissions/sharing-rules.mdx | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/content/docs/permissions/permissions-matrix.mdx b/content/docs/permissions/permissions-matrix.mdx index cbfb31273..2440a3101 100644 --- a/content/docs/permissions/permissions-matrix.mdx +++ b/content/docs/permissions/permissions-matrix.mdx @@ -31,6 +31,17 @@ ObjectStack's `ObjectPermission` schema defines these boolean flags for object a **Super-user bypass:** When `modifyAllRecords` is set it satisfies write checks (`allowEdit`/`allowDelete`, and the lifecycle class `allowTransfer`/`allowRestore`/`allowPurge`) on any record; `viewAllRecords` (or `modifyAllRecords`) satisfies `allowRead` on any record — both bypass ownership and sharing. See `packages/plugins/plugin-security/src/permission-evaluator.ts`. + +The bypass is **posture-gated for row-level security** (ADR-0066 D2 ①): RLS +policies are short-circuited only on objects whose posture permits it — +`access: { default: 'private' }`, `tenancy.enabled: false` +(platform-global), or better-auth-managed. On an ordinary tenant business +object, authored and baseline RLS still applies (e.g. `member_default`'s +owner-scoped write policies keep `org_member` holders owner-scoped on +`update`/`delete` even when another set grants `modifyAllRecords`), and the +Layer 0 tenant wall always ANDs on top. See +[Sharing & OWD](/docs/permissions/sharing-rules#how-viewallrecords--modifyallrecords-interact-with-the-owd) +and `plugin-security/src/security-plugin.ts` (`computeLayeredRlsFilter`). diff --git a/content/docs/permissions/sharing-rules.mdx b/content/docs/permissions/sharing-rules.mdx index 120a74f1f..62df4c75a 100644 --- a/content/docs/permissions/sharing-rules.mdx +++ b/content/docs/permissions/sharing-rules.mdx @@ -41,6 +41,48 @@ export const LeaveRequest = ObjectSchema.create({ > additionally makes an unset OWD a *build error* (`security-owd-unset` in > `os compile`), so the baseline is always an authored decision. +### How `viewAllRecords` / `modifyAllRecords` interact with the OWD + +The View All / Modify All super-user bits widen the **sharing axis** to +org-wide (depth `org`): under `private` they lift the read *and* write owner +filter, under `public_read` the write one. Under **`public_read_write`** the +sharing baseline is *already* org-wide read + write, so on this layer the +bits have nothing left to widen — "granting `viewAllRecords` makes no visible +difference" is the expected outcome of that OWD choice, not a failed grant. +If some rows are confidential, the object's OWD should be `private` (or +`public_read`), with the super-user bits (or shares) doing the widening. + +Two things the bits do **not** override, regardless of the OWD: + +- **Baseline row-level security.** RLS is a separate, *narrowing* layer. The + platform baseline `member_default` ships owner-scoped `update`/`delete` + policies (`created_by == current_user.id`, applicability domain + `org_member`), so rank-and-file members stay owner-scoped on writes even + under `public_read_write`. The super-user bits short-circuit RLS **only + where the object's access posture permits it** — `access: { default: + 'private' }`, `tenancy.enabled: false` (platform-global), or better-auth + managed objects (ADR-0066 D2 ①). On an ordinary tenant business object, + `modifyAllRecords` does *not* lift those policies + (`plugin-security/src/security-plugin.ts` `computeLayeredRlsFilter`). + Org admins (`org_admin` / `org_owner`) are outside the `org_member` + applicability domain and are not narrowed by the baseline. +- **The Layer 0 tenant wall** (ADR-0095 D1) — always ANDs on top. + +None of this is an enterprise/open-core split: both bits are enforced by the +open-source `plugin-security`. The only enterprise-resolved axis is the +**hierarchy depth scopes** (`own_and_reports` / `unit` / `unit_and_below`), +which fail closed to owner-only without the `@objectstack/security-enterprise` +resolver (ADR-0057). + +**Recipe — "owners edit their own records, supervisors edit all":** bind an +OR-widening RLS policy to a supervisor position in a permission set, e.g. +`{ object: '*', operation: 'update', using: 'organization_id == +current_user.organization_id', positions: ['supervisor'] }` (policies +OR-combine within an object, so members keep the owner gate while +supervisors widen to the org) — or model the object with `access: { +default: 'private' }` + explicit grants, where `modifyAllRecords` bypasses +RLS by design. + ## The external dial — `externalSharingModel` (ADR-0090 D11) Portal/partner scenarios get a second, independent dial with the same enum: