diff --git a/docs/requirements/rbac-grant-model.md b/docs/requirements/rbac-grant-model.md new file mode 100644 index 00000000..edcd86f8 --- /dev/null +++ b/docs/requirements/rbac-grant-model.md @@ -0,0 +1,259 @@ +# RBAC Grant Model Contract + +Status: DRAFT — awaiting ratification (webui-audit S2, contract 2 of 9). +Authority: PRD Part I §4 ("Granular RBAC: admins restrict access per company, +estate, and project; grants are evaluated down the chain") and the +native-kanban SOT Amendment A1 (§8.1.3 RBAC evaluation, §8.3 acceptance 2). +This document defines the grant vocabulary, evaluation semantics, and +revocation propagation that the hierarchy schema contract +(`docs/requirements/hierarchy-schema.md`, contract 1) attaches to. Contract 1 +pins the `hierarchy_grants` table shape and defers the `role` vocabulary and +the meaning of "authority" here; the identity contract +(`docs/requirements/identity-lifecycle.md` §1.4) pins that account creation +grants nothing. + +Revision 2 (independent review, GLM 5.3): §1.1 consequence analysis +completed — the two existing platform-admin bypass code paths are named as +non-conformant and §7.4 retires them; team grant subjects suspended pending +a team contract (§1.4, §3.3–3.4, §7.5); no-self-escalation restated with +its true rationale and a constructible observable (§4.2, §7.7); +node-creation seeding scoped to the bootstrap path, resolving the §7.7/§4.3 +contradiction; A1 quotation corrected; audit-field provenance corrected; +principal-position consequence named (§1.3); membership-row, +fail-closed-fault, and existence-oracle observables added (§7); +role-string namespacing rule added (§4.5); ruling request now names the +interpretive resolution of PRD "admins". + +Scope: the roles that can appear in `hierarchy_grants.role`, what a grant at +each hierarchy level confers, how grants evaluate down the chain, how +revocation propagates, and who may manage grants. Out of scope: the hierarchy +tables themselves (contract 1), workspace-internal membership and its +role/capability vocabulary (native-kanban SOT REQ-ID-001 and its implementing +schema), roll-up projection semantics (contract 8), wizard seeding +(contract 3), the team model (suspended here; see §1.4). + +## 1. Three authority layers, none substitutable + +1. **Platform role** (`users.role`, better-auth: `member` | `admin`) governs + instance administration — user management, system settings, provider + configuration. It is not tenancy authority: holding platform `admin` + confers **no implicit hierarchy grant and no workspace authorization**. + An operator who should see tenant content holds an explicit, audited + grant like anyone else. This is the deny-by-default consequence of A1 + §8.1.3 ("not a bypass of workspace authorization"). `AdminGuard`'s + `role === 'admin'` check on admin endpoints stays the platform role's + only meaning. **Two shipped code paths violate this rule today and are + implementation defects this contract makes non-conformant:** (a) the + command authorization service short-circuits every command scope to + allowed for platform admins + (`apps/gateway/src/commands/command-authorization.service.ts`, + `hasScope` returning true when `role === 'admin'`), and (b) the MCP + scope derivation maps platform `admin` to tenant-admin MCP scopes + including task create/update + (`apps/gateway/src/mcp/mcp.service.ts`, + `deriveMcpToolScopesForUser`). Ratifying this contract revokes both; + §7.4 names them as the surfaces the deny-by-default test retires. +2. **Hierarchy grants** (`hierarchy_grants`, contract 1 §3) declare tenancy + authority at company, estate, or platform-project scope and evaluate down + the chain to workspace-scoped authorization (§3 below). +3. **Workspace membership** (SOT REQ-ID-001) remains its own mechanism. + A chain grant confers command authorization over descendant workspaces; + it does not create membership rows, and row-level principal positions + (task owner, proposer, decision actor) still require ACTIVE workspace + membership exactly as REQ-TEN-001/REQ-ID-001 acceptance states. + Consequence, stated so implementing PRs do not weaken REQ-TEN-001 to + remove the friction: a chain-granted actor who is not a workspace member + may issue the write commands their role implies but cannot occupy a + principal position — any command taking a principal argument must name + an ACTIVE member of the target workspace (§7.2 enumerates this cell). +4. **Team grant subjects are suspended.** Contract 1 §3.1 reserves a + `team_id` attachment point, but no ratified contract yet defines the + team it would bind: the only existing `teams` table is the legacy global + Brain table (own authority columns, no workspace binding, not + repurposed per contract 1 §1.3), while the SOT's teams are + workspace-bound (REQ-ID-001) — and a workspace-bound team holding a + company-level grant would be a cross-workspace authority group nothing + has ratified. Until a team contract defines the subject (which table, + which membership rows, and its relation to D2/REQ-ID-001), creating a + grant with a team subject MUST be refused at the command surface (the + schema column remains, per contract 1). §3's evaluation semantics for + team-conferred grants are specified now so the team contract activates + them without amending this one. + +## 2. Role vocabulary + +One vocabulary at every hierarchy level, totally ordered — a higher role +includes everything below it: + +1. `viewer` — read: sees the node, its subtree structure, and the roll-up + aggregates over descendant workspaces (within contract 8's carve-out + bounds); read access to descendant workspace content per the SOT's read + command families. No mutation of anything. +2. `member` — work: everything `viewer` has, plus write authorization for + business/orchestration command families in descendant workspaces (the + concrete command-family mapping is implementation work under SOT + REQ-ID-001; this contract pins that `member` maps to the workspace write + families and nothing structural). +3. `owner` — structure: everything `member` has, plus hierarchy mutations on + the subtree (create/rename/delete child nodes, transfers per §5), and + grant management on the node and its subtree (§4). + +No other value is valid in `hierarchy_grants.role`; the column is +constraint-checked against exactly these three. Extending the vocabulary is a +contract amendment, not an implementation decision. + +## 3. Evaluation semantics + +1. **Deny by default.** No grant on any ancestor → no authority. There are + no implicit grants: not from platform role (§1.1), not from creating a + node (§4.3), not from workspace membership (membership without a chain + grant confers exactly what the SOT's own membership rules confer inside + that workspace, nothing up the chain). +2. **Down-the-chain only.** A grant on a node applies to that node and its + entire descendant subtree. Nothing evaluates upward or sideways: a grant + on an estate says nothing about the parent company or sibling estates. +3. **Effective role = maximum.** A subject's effective role at any node is + the highest role among grants held directly by the subject's user on + that node or any ancestor — and, once the team contract activates team + subjects (§1.4), grants held by any team the user is a member of on that + node or any ancestor. Roles never subtract — there is no negative/deny + grant in this model; revocation is deletion (§6). +4. **Team grants follow live membership** (specified now, active only per + §1.4). A team grant confers its role on the team's current members, + evaluated at decision time. Leaving the team is loss of the grant with + §6's propagation bound. +5. **Live evaluation, fail closed.** Authorization decisions derive from the + live grant and team-membership rows (or from a cache that is invalidated + in the same transaction as any grant/membership/hierarchy mutation). A + decision path that cannot read grant state denies. No materialized ACL is + ever authoritative. +6. **Tenant context stays derived from authenticated authority** + (REQ-TEN-001). The chain adds where grants can be declared; a workspace + request is still authorized against that workspace, with the chain + contributing the effective role — never letting the chain become what A1 + §8.1.3 forbids: "a bypass of workspace authorization". + +## 4. Grant management + +1. Creating, changing, or revoking a grant on a node requires effective + `owner` on that node (directly or via any ancestor). +2. **No self-escalation.** A grant manager cannot create a grant with a role + higher than their own effective role on the target node. Under the §2 + vocabulary this rule is currently implied by §4.1 (managers are `owner`, + the top role — no constructible grant exceeds it); it is stated + explicitly so it survives any future amendment that decouples + grant-management authority from role height. Its observable is the §7.7 + audit invariant, not a refusal test. +3. **Bootstrap of authority is explicit; inheritance covers the rest.** + Creating the first company (the wizard path, contract 3) and any + top-level company creation MUST name the initial `owner` grant in the + same audited operation — a top-level node has no ancestor to inherit + from, so without this the node would be unownable. Creating a child node + (estate, platform-project, workspace) requires effective `owner` on the + parent (§2.3) and confers no automatic grant; the creator's authority + over the new node already follows from §3.2 down-the-chain evaluation. + The creating command MAY additionally name an explicit initial grant for + a child node; it is not required to. +4. Every grant mutation is a semantic audit event under contract 1 §5.2's + guarantees, extended by this contract with two further fields: the event + carries actor, verb, target, **subject, and role** (subject and role are + this contract's addition; contract 1 §5.2 does not enumerate them). +5. **Role strings are namespaced.** `viewer`/`member` exist at hierarchy + level, `member`/`admin` on `users.role`, and the current command layer + uses a third `viewer|member|admin` vocabulary — same strings, different + meanings. Any serialized role string (audit events per §4.4, API + responses, logs) MUST identify its layer (e.g. `hierarchy:owner`, + `platform:admin`); a bare role string in a serialized artifact is + non-conformant. + +## 5. Transfer authority (completes contract 1 §4.2) + +"Authority over BOTH the source and the destination parent" means: effective +`owner` on the current parent node (or an ancestor) AND effective `owner` on +the destination parent node (or an ancestor), evaluated at transfer time in +the transfer's own transaction. One subject must hold both; two cooperating +half-authorized subjects are not a transfer protocol this contract defines. + +## 6. Revocation propagation + +1. Revoking a grant (deleting the row), removing a user from a team that + carries a grant (once team subjects activate, §1.4), or the cascade + deletion of a node's grants during node deletion (contract 1 §3.3) all + propagate identically: the authority derived from that grant is gone for + every descendant workspace. +2. **Bound:** the next authorization decision on any affected transport + decides against the revoked grant. Concretely: no new HTTP/MCP command + authorized by the revoked grant after the revoking transaction commits; + an open Socket.IO connection whose subscriptions depend on the revoked + grant is re-evaluated within 30 seconds or at its next inbound message, + whichever comes first (same bound as the identity contract's §7.1 + deactivation rule; same mechanism may serve both). +3. Revocation is subtractive only in effect, not in representation: the + evaluator never needs tombstones; deletion of the row is the revocation. + +## 7. Verification requirements + +Binding on the implementing PRs (extends A1 §8.3 acceptance 2–3 and +contract 1 §6): + +1. Vocabulary: the role CHECK constraint rejects any value outside + `viewer|member|owner` (real-PostgreSQL witness, `ci-postgres` service in + the `test` CI step). +2. Per-level conferral: for each of the three levels × three roles, a grant + yields exactly the implied workspace authorization in a descendant + workspace and nothing in a non-descendant workspace (the A1 §8.3 + "exactly the permissions the chain implies" matrix, enumerated). The + matrix includes: a chain grant creates zero workspace-membership rows + (assert row counts); a chain-granted non-member is refused as the + principal argument of any principal-taking command while their + non-principal writes succeed (§1.3); structure reads leak no existence + of nodes the reader holds no grant on (no cross-tenant existence + oracle, A1 §8.3 acceptance 3). +3. Ordering: `owner` ⊇ `member` ⊇ `viewer` behaviorally — each higher role + passes every lower role's positive cases. +4. Deny-by-default: platform `admin` with no grant reaches no tenant + content — asserted against the two §1.1 non-conformant surfaces after + their retirement: the command-authorization admin short-circuit and the + MCP tenant-admin scope derivation both gone (a platform admin with no + grant is refused workspace commands and receives no tenant MCP scopes); + workspace member with no chain grant gains nothing outside SOT + membership semantics; fresh account reaches nothing (identity contract + §1.4 cross-check). +5. Team subjects: while suspended (§1.4), creating a team-subject grant is + refused at the command surface. On activation by the team contract: + user-direct and team-conferred grants combine to the maximum; team-leave + drops authority within the §6.2 bound; decision-time evaluation + witnessed (grant added → next decision allows; no restart or re-login + required). +6. Revocation: each revocation path in §6.1 denies the next command on + every transport; the socket bound is measured; a cached-authorization + implementation proves transactional invalidation (grant revoked and + decision made on two distinct physical connections). Fail-closed fault + witness for §3.5: with grant state unreadable (fault injection), the + decision denies. +7. Grant management: non-`owner` cannot mutate grants; top-level company + creation without the named initial `owner` grant is refused, while child + node creation under ancestor authority succeeds without one (§4.3 both + directions); every mutation produces its audit event with the §4.4 + fields. Self-escalation observable: over the audit event stream, every + grant-create/change event's role is ≤ the acting user's effective role + on the target at event time (reconstructable invariant, not a refusal + test — see §4.2). +8. Transfer: both-sides `owner` accepted, each single-side case refused + (completing contract 1 §6.5). + +## Ruling request + +Ratify sections 1–7 as written, with one decision embedded and one +interpretive resolution named: + +- Decision: platform `admin` confers no implicit tenant access — operators + see tenant content only through explicit, audited grants (§1.1), which + retires the two existing admin bypass paths named there. Say "agreed" or + name the implicit access you want platform admins to keep. +- Interpretive resolution (for visibility, not a separate question): PRD + Part I §4 says "admins restrict access per company, estate, and project"; + this contract resolves "admins" as hierarchy `owner`s (§4.1), not + platform admins. A1 §8.1.3 does not attribute grant declaration to + platform admins, and the §1.1 decision above is what makes this reading + binding.