Skip to content

Commit 5a92341

Browse files
os-zhuangclaude
andauthored
docs(permissions): rewrite content/docs/permissions/* to the Permission Model v2 vocabulary (ADR-0090) (#2717)
- roles.mdx → positions.mdx: flat positions (岗位), assignment BU anchor, built-in identity positions, everyone/guest audience anchors; the hierarchy narrative moves to business units. - profiles.mdx → migration tombstone: the Profile concept was removed (D2); maps each former use to everyone-anchor bindings, isDefault suggestions, and position-bound sets. - permission-sets.mdx: rewritten as the single capability container — union semantics, access depth (moved here from profiles), capabilities, built-in sets (additive member_default baseline), governed assignment tables, isDefault suggestion, adminScope delegated administration, package provenance. - sharing-rules.mdx: OWD default corrected to fail-closed private (D1 — the page previously documented the pre-v2 fail-open default), canonical four values only, externalSharingModel dial (D11), recipient types position / unit_and_subordinates / team. - authorization.mdx: position vocabulary, D1/D11 in the enforcement chain, delegated-admin gate in anti-escalation, new explain-engine section (D6), D7 linter + access-matrix snapshot added to governance, ADR-0090 in the index. - permissions-matrix.mdx: role-hierarchy section replaced with business-unit hierarchy & positions; isProfile removed from samples; position recipients. - permission-metadata.mdx: isProfile → isDefault/adminScope in the field table; union-semantics section replaces Profile-vs-Set; current_user.positions. - index.mdx: five-concepts overview, v2 implementation-status callout, best practices and example updated. - access-recipes.mdx / field-level-security.mdx: link + heading fixes. Authoritative reference: docs/design/permission-model.md; decision record: ADR-0090 (P1 #2697, P2 #2708, P3 #2711, P4 #2716). Claude-Session: https://claude.ai/code/session_012oLzaP8n7A3YKFmgaHWC8H Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5a1e41 commit 5a92341

12 files changed

Lines changed: 536 additions & 617 deletions

content/docs/permissions/access-recipes.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ definePermissionSet({
3737
});
3838
```
3939

40-
- **"My own"** vs **"my team's"** vs **"the org's"** is the RLS *depth* axis (`readScope` / `writeScope`: `own | own_and_reports | unit | unit_and_below | org`, ADR-0057). A manager set uses `unit`; a rep uses `own`. The hierarchy-relative scopes (`own_and_reports` / `unit` / `unit_and_below`) need the enterprise hierarchy resolver and **fail closed to `own`** without it — see [Access depth](/docs/permissions/profiles#access-depth--readscope--writescope-adr-0057-d1).
40+
- **"My own"** vs **"my team's"** vs **"the org's"** is the RLS *depth* axis (`readScope` / `writeScope`: `own | own_and_reports | unit | unit_and_below | org`, ADR-0057). A manager set uses `unit`; a rep uses `own`. The hierarchy-relative scopes (`own_and_reports` / `unit` / `unit_and_below`) need the enterprise hierarchy resolver and **fail closed to `own`** without it — see [Access depth](/docs/permissions/permission-sets#access-depth--readscope--writescope-adr-0057-d1).
4141
- Grants combine **most-permissively** across a user's sets; the tenant-isolation policy `AND`s on top; the superuser bypass (`viewAllRecords` / `modifyAllRecords`) short-circuits RLS where the object's posture allows it (ADR-0066).
4242
- For genuinely sensitive objects, set `access: { default: 'private' }` so they are **not** covered by blanket wildcard grants — access needs an explicit grant.
4343

@@ -62,7 +62,7 @@ Keeping capability, assignment and requirement decoupled means resources stay st
6262

6363
## Runnable example
6464

65-
- Built-in profiles: [`default-permission-sets.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/plugins/plugin-security/src/objects/default-permission-sets.ts) (`member_default` shows owner-scoped RLS + tenant isolation).
65+
- Built-in permission sets: [`default-permission-sets.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/plugins/plugin-security/src/objects/default-permission-sets.ts) (`member_default` shows owner-scoped RLS + tenant isolation).
6666
- Security objects: [`packages/plugins/plugin-security/src/objects`](https://github.com/objectstack-ai/framework/tree/main/packages/plugins/plugin-security/src/objects).
6767

6868
## Anti-patterns

content/docs/permissions/authorization.mdx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ Authorization splits into three concerns that stay decoupled:
2828

2929
1. **Capability***what can be done* (`manage_users`, `export_data`).
3030
Defined by the platform or a package; extended by admins.
31-
2. **Assignment***who holds it* — permission sets / roles / user bindings
32-
(`sys_permission_set`, `sys_role`, `sys_user_permission_set`,
33-
`sys_role_permission_set`). Runtime records, maintained by admins in Setup.
31+
2. **Assignment***who holds it* — permission sets / positions / user
32+
bindings (`sys_permission_set`, `sys_position`, `sys_user_permission_set`,
33+
`sys_position_permission_set`, `sys_user_position`). Runtime records,
34+
maintained by admins in Setup — and, since ADR-0090 D12, a **governed**
35+
surface: writing them requires tenant-level administration or a covering
36+
delegated `adminScope`, never just CRUD on the tables.
3437
3. **Requirement***what a resource needs* — an object / field / action
3538
references a capability as a contract. A resource never bakes in "who",
3639
only "what is required".
@@ -43,17 +46,22 @@ site — the file you read when behavior surprises you.
4346
| # | Gate | What it decides | Enforcement site | Failure direction |
4447
|---|---|---|---|---|
4548
| 1 | **Anonymous deny** | No identity → HTTP 401 on `/data/*`. **Default-on** (ADR-0056 D2): public data serving requires an explicit `api.requireAuth: false` opt-out, which logs a boot warning. Control plane (`/auth`, `/health`, `/discovery`) is exempt; share-links validate their token then read as SYSTEM. | `packages/rest/src/rest-server.ts` `enforceAuth` (default in `packages/spec/src/api/rest-server.zod.ts`) | fail-closed |
46-
| 2 | **Public-form grant** | An anonymous form submission carries a declaration-derived `publicFormGrant` authorizing ONLY create + read-back on the form's declared target object — never anything else (ADR-0056 Option A). No `guest_portal` profile needed. | `packages/plugins/plugin-security/src/security-plugin.ts` (ObjectQL middleware) | scope-limited allow |
49+
| 2 | **Public-form grant** | An anonymous form submission carries a declaration-derived `publicFormGrant` authorizing ONLY create + read-back on the form's declared target object — never anything else (ADR-0056 Option A). No guest-portal configuration needed (anonymous principals hold the `guest` position). | `packages/plugins/plugin-security/src/security-plugin.ts` (ObjectQL middleware) | scope-limited allow |
4750
| 3 | **Object CRUD** | `allowRead/Create/Edit/Delete` (+ the destructive lifecycle class `allowTransfer/Restore/Purge`, gated ahead of the M2 operations — #1883) resolved across the caller's permission sets. | `packages/plugins/plugin-security/src/permission-evaluator.ts` `checkObjectPermission` | fail-closed 403 |
48-
| 4 | **OWD / sharing** | Org-wide default (`private` / `public_read` / `public_read_write` / `controlled_by_parent`), manual record shares, criteria sharing rules (owner-type rules are declared but seed-skipped — [not enforced](/docs/permissions/sharing-rules#owner-based-sharing-rules)), business-unit hierarchy widening (ADR-0057 D5: scope-depth hierarchy lives on `sys_business_unit`, not roles). | `packages/plugins/plugin-sharing/src/sharing-service.ts` + `sharing-rule-service.ts` | owner-only baseline |
51+
| 4 | **OWD / sharing** | Org-wide default (`private` / `public_read` / `public_read_write` / `controlled_by_parent`; **unset or unknown ⇒ `private`, fail-closed** — ADR-0090 D1) plus the external dial (`externalSharingModel`, ADR-0090 D11), manual record shares, criteria sharing rules (owner-type rules are declared but seed-skipped — [not enforced](/docs/permissions/sharing-rules#owner-based-sharing-rules)), business-unit hierarchy widening (ADR-0057 D5: scope-depth hierarchy lives on `sys_business_unit`, not positions). | `packages/plugins/plugin-sharing/src/sharing-service.ts` + `sharing-rule-service.ts` | fail-closed to owner-only |
4952
| 5 | **Row-level security** | CEL predicates (`using` read filter, `check` write post-image) compiled into the query. If **no** applicable policy compiles, the result is a deny-all sentinel (fail-closed); an uncompilable policy alongside compilable ones is excluded from the OR-union **with a logged warning** — exclusion can only narrow access, never widen it. Tenant isolation is a wildcard RLS rule AND-ed on top. | `packages/plugins/plugin-security/src/rls-compiler.ts` + `security-plugin.ts` | fail-closed |
5053
| 6 | **Field-level security** | Read mask (strip non-readable fields) + write deny per `fields` rules. Caller queries that **filter, sort, group, or aggregate by** a non-readable field are rejected outright (HTTP 403, `field_predicate_denied`) — masking only the output would leave row presence as a value oracle. RLS-injected predicates are exempt (they run after the guard and may reference hidden fields like `owner_id`). | `packages/plugins/plugin-security/src/field-masker.ts` + `predicate-guard.ts` | fail-closed on predicates; see posture note below |
5154

5255
Two orthogonal identity-layer gates run before all of this: the ADR-0069
5356
**authentication-policy gate** (password expiry / enforced MFA blocks a gated
5457
session from protected resources while keeping remediation reachable), and
55-
**anti-escalation** (RBAC tables are read-only for `organization_admin`, so an
56-
org admin cannot self-grant platform admin).
58+
**anti-escalation** — RBAC tables are read-only for `organization_admin`, and
59+
since ADR-0090 D12 every RBAC-table write is additionally checked by the
60+
**delegated-admin gate**: tenant admins pass, delegates are confined to their
61+
`adminScope` (BU subtree + assignable-set allowlist, no self-escalation,
62+
strict containment for scope grants), and everyone else is denied. The
63+
`everyone`/`guest` audience-anchor bindings reject high-privilege sets at the
64+
data layer for every caller.
5765

5866
## Combination semantics (the fixed order)
5967

@@ -123,7 +131,7 @@ one of two doors, each writing only what it owns:
123131
resolve).
124132
- **Environment-admin door** (`metadata-admin`) — the cross-package all-objects
125133
matrix plus subject **assignment** (`sys_user_permission_set`,
126-
`sys_role_permission_set`), edited **live** (config). It owns env-authored
134+
`sys_position_permission_set`), edited **live** (config). It owns env-authored
127135
sets (`managedBy` `platform`/`user`) and assignments — not package sets.
128136
- **Data-layer write gate** — the security middleware **refuses** any admin-door
129137
write to a `managedBy:'package'` `sys_permission_set` row, and refuses a
@@ -141,14 +149,46 @@ one of two doors, each writing only what it owns:
141149
|---|---|---|
142150
| 1 · Package development | Zod-validated authoring; roles / sharingRules / permissions seeded at boot with provenance | ADR-0057 D6, ADR-0086 D5, ADR-0049/0078 gates |
143151
| 2 · Distribution / install / upgrade / uninstall | Install-consent scopes (ADR-0025 — consent ≠ RBAC grants); namespaced, collision-free composition; provenance axis makes uninstall well-defined | ADR-0025/0028/0048/0086 |
144-
| 3 · Environment composition / assignment | Platform-owned assignment records (`sys_user_role` etc.); anti-escalation; union semantics | ADR-0057 D4 |
152+
| 3 · Environment composition / assignment | Platform-owned assignment records (`sys_user_position` etc.); anti-escalation; union semantics | ADR-0057 D4 |
145153
| 4 · Runtime enforcement | The six-gate chain above; ~18 primitives enforced and CI-guarded | ADR-0056 D10 matrix |
146154
| 5 · Production / enterprise | ADR-0056 D8 dispositions settled (2026-07): compliance configs, data masking, and the global RLSConfig were **removed** (never enforced); field encryption stays honestly `[EXPERIMENTAL]` (roadmap); agent `visibility` is marked `[EXPERIMENTAL]` pending #1901. Enterprise authentication hardening staged per ADR-0069 | ADR-0049/0056 D8, ADR-0069 |
147155

156+
## Explaining a decision (ADR-0090 D6)
157+
158+
The `security` kernel service exposes `explain(request, callerContext)` — the
159+
first-class answer to "why can 张三 PATCH 李四's leave_request?". It walks the
160+
SAME code paths the middleware enforces with (shared set resolution,
161+
evaluator, FLS mask, RLS composition — *explained by construction*) and
162+
reports every pipeline layer in order:
163+
164+
```
165+
principal → required_permissions → object_crud → fls → owd_baseline
166+
→ depth → sharing → vama_bypass → rls
167+
```
168+
169+
Each layer carries a verdict (`grants` / `denies` / `narrows` / `widens` /
170+
`neutral` / `not_applicable`), a human explanation, and **contributor
171+
attribution** — which permission set granted, reached via which position /
172+
additive baseline / direct grant. For reads, the decision includes the
173+
composed row filter as the machine artifact. Explaining another user requires
174+
the `manage_users` capability.
175+
148176
## Governance: how "declared = enforced" is kept true
149177

150-
Two CI mechanisms make the security posture a **checked artifact** rather than
151-
a belief:
178+
Four CI-time mechanisms make the security posture a **checked artifact**
179+
rather than a belief:
180+
181+
- **Security publish linter** (ADR-0090 D7, `validateSecurityPosture` in
182+
`@objectstack/lint`, gating `os compile`): unset OWD on custom objects,
183+
retired OWD aliases, an external dial wider than internal, `'*'` wildcards
184+
carrying View/Modify All outside the platform admin set, high-privilege
185+
`isDefault` (everyone-suggested) sets, and the reserved word "role" in
186+
security identifiers — every error rule mirrors a runtime gate.
187+
- **Access-matrix snapshot** (ADR-0090 D6, `buildAccessMatrix` /
188+
`diffAccessMatrix`): with `access-matrix.json` committed next to the config,
189+
`os compile` fails on any capability drift with semantic lines
190+
(`'crm_admin' gains delete on 'crm_lead'`) until the snapshot is updated via
191+
`--update-access-matrix` — the snapshot's git diff is the review artifact.
152192

153193
- **Conformance matrix** (`packages/dogfood/test/authz-conformance.matrix.ts`,
154194
ADR-0056 D10): every authorization primitive sits in exactly one honest
@@ -212,3 +252,4 @@ The complete, prioritized gap map lives in issue **#2561** (the production
212252
| [0069](/adr/0069-enterprise-authentication-hardening) | Enterprise authentication hardening (phased) |
213253
| [0078](/adr/0078-no-inert-declarable-metadata) | No inert declarable metadata |
214254
| [0086](/adr/0086-authz-metadata-config-boundary-and-cross-package-composition) | Metadata↔config boundary, package provenance, cross-package composition |
255+
| 0090 | Permission Model v2: position rename + vocabulary freeze, profile removal, fail-closed OWD default + external dial, audience anchors, principal taxonomy, publish linter, delegated administration, explain engine + access matrix |

content/docs/permissions/field-level-security.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: "Control visibility and editability of specific fields — readable
77

88
Control visibility and editability of specific fields.
99

10-
## Field Permissions in Profiles
10+
## Field Permissions in Permission Sets
1111

1212
Field permissions are keyed `<object>.<field>` and use `readable` / `editable`:
1313

@@ -99,5 +99,5 @@ writes.
9999
## See also
100100

101101
- [Permissions & Identity overview](/docs/permissions)
102-
- [Profiles](/docs/permissions/profiles)
102+
- [Permission Sets](/docs/permissions/permission-sets)
103103
- [Permission Sets](/docs/permissions/permission-sets)

0 commit comments

Comments
 (0)