From 3538d331068c55ad960884b826d1a8d2cb8cc3f2 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 18:23:23 -0500 Subject: [PATCH 1/2] docs: RBAC grant model contract (S2 contract 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defines the hierarchy grant vocabulary (viewer|member|owner, totally ordered), evaluation semantics (deny-by-default, down-the-chain, max rule, live fail-closed evaluation), grant management (owner-managed, no self-escalation, explicit bootstrap of authority), transfer authority (both-sides owner, completing contract 1 §4.2), and revocation propagation with the same 30s/next-message bound as identity §7.1. Embedded decision for ratification: platform admin confers no implicit tenant access (§1.1). Sources: PRD Part I §4, native-kanban SOT Amendment A1 §8.1.3/§8.3, hierarchy-schema contract (PR #1435), identity-lifecycle contract §1.4 (PR #1433). --- docs/requirements/rbac-grant-model.md | 169 ++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/requirements/rbac-grant-model.md diff --git a/docs/requirements/rbac-grant-model.md b/docs/requirements/rbac-grant-model.md new file mode 100644 index 00000000..43359202 --- /dev/null +++ b/docs/requirements/rbac-grant-model.md @@ -0,0 +1,169 @@ +# 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. + +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). + +## 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's "never a bypass" rule; today's `AdminGuard` checks + `role === 'admin'` for admin endpoints and that stays its only meaning.) +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. + +## 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 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.** 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 a bypass of workspace-scoped + checks. + +## 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. (With the §2 + vocabulary this only bites managers acting through team-conferred + `owner`: the rule is stated so it survives vocabulary amendments.) +3. Creating a hierarchy node confers no automatic grant. Bootstrap of + authority is explicit: the creating command names the initial `owner` + grant in the same audited operation, and the wizard (contract 3) seeds + the first company's initial `owner` the same way. +4. Every grant mutation is a semantic audit event per contract 1 §5.2 + (actor, verb, subject, target, role). + +## 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, or deleting an ancestor node's grant 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.4): + +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). +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; workspace member with no chain grant gains nothing outside SOT + membership semantics; fresh account reaches nothing (identity contract + §1.4 cross-check). +5. Max-rule and teams: 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). +7. Grant management: non-`owner` cannot mutate grants; self-escalation + attempt refused; node creation without the explicit initial grant + refused; every mutation produces its audit event. +8. Transfer: both-sides `owner` accepted, each single-side case refused + (completing contract 1 §6.2). + +## Ruling request + +Ratify sections 1–7 as written, with one decision embedded: platform `admin` +confers no implicit tenant access — operators see tenant content only +through explicit, audited grants (§1.1) — say "agreed" or name the implicit +access you want platform admins to have. -- 2.54.0 From 501112d2a4e8e14d4e12d2a8fc67f9d7fb713d29 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 18:33:08 -0500 Subject: [PATCH 2/2] docs: RBAC grant model contract revision 2 (GLM review findings 1-10) - name the two shipped platform-admin bypass paths (command-authorization hasScope short-circuit, MCP tenant-admin scope derivation) as non-conformant; 7.4 retires them explicitly - suspend team grant subjects pending a team contract; command surface refuses team-subject grants; evaluation semantics pre-specified - no-self-escalation restated with future-decoupling rationale; observable is an audit invariant, not an unconstructible refusal test - bootstrap seeding scoped to top-level company creation; child nodes inherit via down-the-chain, resolving the 7.7/4.3 contradiction - exact A1 quote (not a bypass of workspace authorization) - subject/role audit fields attributed as this contract's addition - principal-position consequence named (1.3) with matrix cell - added observables: zero membership rows, fail-closed fault injection, no existence oracle - role-string namespacing rule (4.5) - ruling request names the PRD-admins interpretive resolution --- docs/requirements/rbac-grant-model.md | 172 ++++++++++++++++++++------ 1 file changed, 131 insertions(+), 41 deletions(-) diff --git a/docs/requirements/rbac-grant-model.md b/docs/requirements/rbac-grant-model.md index 43359202..edcd86f8 100644 --- a/docs/requirements/rbac-grant-model.md +++ b/docs/requirements/rbac-grant-model.md @@ -12,13 +12,25 @@ 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). +(contract 3), the team model (suspended here; see §1.4). ## 1. Three authority layers, none substitutable @@ -27,9 +39,20 @@ schema), roll-up projection semantics (contract 8), wizard seeding 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's "never a bypass" rule; today's `AdminGuard` checks - `role === 'admin'` for admin endpoints and that stays its only meaning.) + 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). @@ -38,6 +61,24 @@ schema), roll-up projection semantics (contract 8), wizard seeding 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 @@ -72,13 +113,15 @@ contract amendment, not an implementation decision. 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 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.** 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. + 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 @@ -87,23 +130,41 @@ contract amendment, not an implementation decision. 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 a bypass of workspace-scoped - checks. + 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. (With the §2 - vocabulary this only bites managers acting through team-conferred - `owner`: the rule is stated so it survives vocabulary amendments.) -3. Creating a hierarchy node confers no automatic grant. Bootstrap of - authority is explicit: the creating command names the initial `owner` - grant in the same audited operation, and the wizard (contract 3) seeds - the first company's initial `owner` the same way. -4. Every grant mutation is a semantic audit event per contract 1 §5.2 - (actor, verb, subject, target, 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) @@ -116,9 +177,10 @@ 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, or deleting an ancestor node's grant during node - deletion (contract 1 §3.3) all propagate identically: the authority - derived from that grant is gone for every descendant workspace. + 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; @@ -132,7 +194,7 @@ half-authorized subjects are not a transfer protocol this contract defines. ## 7. Verification requirements Binding on the implementing PRs (extends A1 §8.3 acceptance 2–3 and -contract 1 §6.4): +contract 1 §6): 1. Vocabulary: the role CHECK constraint rejects any value outside `viewer|member|owner` (real-PostgreSQL witness, `ci-postgres` service in @@ -140,30 +202,58 @@ contract 1 §6.4): 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). + "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; workspace member with no chain grant gains nothing outside SOT + 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. Max-rule and teams: 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). +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). -7. Grant management: non-`owner` cannot mutate grants; self-escalation - attempt refused; node creation without the explicit initial grant - refused; every mutation produces its audit event. + 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.2). + (completing contract 1 §6.5). ## Ruling request -Ratify sections 1–7 as written, with one decision embedded: platform `admin` -confers no implicit tenant access — operators see tenant content only -through explicit, audited grants (§1.1) — say "agreed" or name the implicit -access you want platform admins to have. +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. -- 2.54.0