Skip to content

Commit a81b1a9

Browse files
authored
docs(permissions): clarify View/Modify All × OWD interaction and posture-gated RLS bypass (#3008)
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. Closes #3005
1 parent 93fd58e commit a81b1a9

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

content/docs/permissions/permissions-matrix.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ ObjectStack's `ObjectPermission` schema defines these boolean flags for object a
3131

3232
<Callout type="tip">
3333
**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`.
34+
35+
The bypass is **posture-gated for row-level security** (ADR-0066 D2 ①): RLS
36+
policies are short-circuited only on objects whose posture permits it —
37+
`access: { default: 'private' }`, `tenancy.enabled: false`
38+
(platform-global), or better-auth-managed. On an ordinary tenant business
39+
object, authored and baseline RLS still applies (e.g. `member_default`'s
40+
owner-scoped write policies keep `org_member` holders owner-scoped on
41+
`update`/`delete` even when another set grants `modifyAllRecords`), and the
42+
Layer 0 tenant wall always ANDs on top. See
43+
[Sharing & OWD](/docs/permissions/sharing-rules#how-viewallrecords--modifyallrecords-interact-with-the-owd)
44+
and `plugin-security/src/security-plugin.ts` (`computeLayeredRlsFilter`).
3445
</Callout>
3546

3647
<Callout type="warn">

content/docs/permissions/sharing-rules.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,48 @@ export const LeaveRequest = ObjectSchema.create({
4141
> additionally makes an unset OWD a *build error* (`security-owd-unset` in
4242
> `os compile`), so the baseline is always an authored decision.
4343
44+
### How `viewAllRecords` / `modifyAllRecords` interact with the OWD
45+
46+
The View All / Modify All super-user bits widen the **sharing axis** to
47+
org-wide (depth `org`): under `private` they lift the read *and* write owner
48+
filter, under `public_read` the write one. Under **`public_read_write`** the
49+
sharing baseline is *already* org-wide read + write, so on this layer the
50+
bits have nothing left to widen — "granting `viewAllRecords` makes no visible
51+
difference" is the expected outcome of that OWD choice, not a failed grant.
52+
If some rows are confidential, the object's OWD should be `private` (or
53+
`public_read`), with the super-user bits (or shares) doing the widening.
54+
55+
Two things the bits do **not** override, regardless of the OWD:
56+
57+
- **Baseline row-level security.** RLS is a separate, *narrowing* layer. The
58+
platform baseline `member_default` ships owner-scoped `update`/`delete`
59+
policies (`created_by == current_user.id`, applicability domain
60+
`org_member`), so rank-and-file members stay owner-scoped on writes even
61+
under `public_read_write`. The super-user bits short-circuit RLS **only
62+
where the object's access posture permits it** — `access: { default:
63+
'private' }`, `tenancy.enabled: false` (platform-global), or better-auth
64+
managed objects (ADR-0066 D2 ①). On an ordinary tenant business object,
65+
`modifyAllRecords` does *not* lift those policies
66+
(`plugin-security/src/security-plugin.ts` `computeLayeredRlsFilter`).
67+
Org admins (`org_admin` / `org_owner`) are outside the `org_member`
68+
applicability domain and are not narrowed by the baseline.
69+
- **The Layer 0 tenant wall** (ADR-0095 D1) — always ANDs on top.
70+
71+
None of this is an enterprise/open-core split: both bits are enforced by the
72+
open-source `plugin-security`. The only enterprise-resolved axis is the
73+
**hierarchy depth scopes** (`own_and_reports` / `unit` / `unit_and_below`),
74+
which fail closed to owner-only without the `@objectstack/security-enterprise`
75+
resolver (ADR-0057).
76+
77+
**Recipe — "owners edit their own records, supervisors edit all":** bind an
78+
OR-widening RLS policy to a supervisor position in a permission set, e.g.
79+
`{ object: '*', operation: 'update', using: 'organization_id ==
80+
current_user.organization_id', positions: ['supervisor'] }` (policies
81+
OR-combine within an object, so members keep the owner gate while
82+
supervisors widen to the org) — or model the object with `access: {
83+
default: 'private' }` + explicit grants, where `modifyAllRecords` bypasses
84+
RLS by design.
85+
4486
## The external dial`externalSharingModel` (ADR-0090 D11)
4587

4688
Portal/partner scenarios get a second, independent dial with the same enum:

0 commit comments

Comments
 (0)