# 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.
