Skip to content

docs(plans): accept granular RBAC plan#282

Open
mcharles-square wants to merge 3 commits into
mainfrom
docs/rbac-plan
Open

docs(plans): accept granular RBAC plan#282
mcharles-square wants to merge 3 commits into
mainfrom
docs/rbac-plan

Conversation

@mcharles-square
Copy link
Copy Markdown
Collaborator

Summary

Adds the technical design for moving Proto Fleet from the current SUPER_ADMIN / ADMIN / RequireAdmin gate to a granular permission model with scoped role assignments, editable built-in roles, and per-org custom roles. Status: accepted — implementation will follow in subsequent PRs per the plan's own PR sequencing.

Key decisions baked in

  • Three seeded roles. SUPER_ADMIN is immutable; ADMIN and FIELD_TECH are editable by SUPER_ADMIN but cannot be deleted. Two distinct error codes (BUILTIN_ROLE_IMMUTABLE vs BUILTIN_ROLE_NON_DELETABLE) so the UI can render the right copy.
  • Permission catalog. Resource:action pairs (miner:reboot, rack:manage, fleet:read, etc.) with a read-pairing rule enforced at role save — any action permission requires its matching read partner.
  • Scope semantics: narrowing (intersection-on-overlap). A site-scoped assignment overrides the org-scoped grant at that site; the org grant still applies everywhere else. This lets admins add a smaller site-scoped role to narrow a user without first removing their org assignment.
  • Filter-don't-reject list semantics. A user with site-scoped fleet:read gets a filtered list, not a 403.
  • Route-guard parity. Every primary-nav entry has a permission predicate; the nav hides AND the route redirects when the predicate is false. The administrative set {user:manage, role:manage, site:manage, apikey:manage, serverlog:read} defines Settings visibility. A <NoAccess> terminal page handles users with no available primary nav.
  • Additive-only startup reconciliation for ADMIN / FIELD_TECH so operator edits survive upgrades; SUPER_ADMIN is fully reconciled to AllPermissions() on every boot.
  • FIELD_TECH seed includes rack:read / rack:manage so field techs can organize the physical layout at their assigned sites.

Out of scope (deferred follow-ups)

  • Building-level scope (only org + site for v1).
  • Per-key API permission declarations / snapshot-at-issue.
  • Audit-log table as a product surface.
  • Deny rules / ABAC.

Implementation PR sequence

The plan calls out four PRs: foundation (schema + sqlc + proto + seed + backfill), security-critical (resolver + middleware + handler swap), role-management surface (RPC + admin UI), and E2E + contract tests. Each is independently shippable — the existing RequireAdmin keeps working until the security-critical PR lands the swap.

Test plan

  • Plan file passes lefthook pre-commit hooks (markdown only).
  • Reviewers confirm the decisions section, scope boundaries, and PR sequence match the team's intent before PR 1 starts.

Adds the technical design for moving Proto Fleet from the current
SUPER_ADMIN/ADMIN/RequireAdmin gate to a granular permission model with
scoped role assignments, editable built-in roles, and per-org custom
roles.

Key decisions baked in:
- Three seeded roles: SUPER_ADMIN (immutable), ADMIN and FIELD_TECH
  (editable by SUPER_ADMIN, not deletable)
- Permission catalog plus read-pairing rule (action perms require
  matching read perms; enforced at role save)
- Site-scope narrowing semantics (site-scoped assignment overrides
  org-scoped grant at that site; org grant still applies elsewhere)
- Filter-don't-reject list semantics for site-scoped readers
- Route-guard parity: every primary-nav entry has a permission
  predicate; nav hides AND route redirects when false
- Additive-only startup reconciliation for ADMIN/FIELD_TECH so
  operator edits survive upgrades; SUPER_ADMIN fully reconciled
- Two error codes: BUILTIN_ROLE_IMMUTABLE (SUPER_ADMIN only) and
  BUILTIN_ROLE_NON_DELETABLE (ADMIN/FIELD_TECH delete attempts)
@mcharles-square mcharles-square requested a review from a team as a code owner May 21, 2026 12:15
Copilot AI review requested due to automatic review settings May 21, 2026 12:15
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 21, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (254400167beb58371bcc5fe1172a47be1ca910a6...bac7657cbbe20a2a5edcb3678813605e351aa042, exact PR three-dot diff)
  • Model: gpt-5.5

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: HIGH

Findings

[HIGH] Assignment Schema Does Not Enforce Org Ownership for Roles or Targets

  • Category: Auth | SQLi/Database
  • Location: docs/plans/2026-05-19-001-feat-granular-rbac-plan.md:538
  • Description: user_organization_role.role_id references only role(id), while roles are supposed to be org-owned. The plan also has role/assignment RPCs taking bare role_id, assignment_id, and user_id, but the safety rules only call out scope validation, not role ownership, assignment ownership, or target-user membership validation.
  • Impact: A bug in AssignRole, UpdateCustomRole, DeleteCustomRole, or UnassignRole could let one org use or mutate another org’s role/assignment by ID. This undermines the per-org RBAC boundary.
  • Recommendation: Make role.organization_id non-null after backfill, add a composite FK from (role_id, organization_id) to role(id, organization_id), add membership enforcement for (user_id, organization_id), and require all role/assignment/user lookups to include session.Info.OrganizationID.

[HIGH] Resolver “First Match” Algorithm Can Bypass Site-Scoped Narrowing

  • Category: Auth
  • Location: docs/plans/2026-05-19-001-feat-granular-rbac-plan.md:902
  • Description: The plan says Has returns true on the first assignment whose scope contains the resource scope. That conflicts with the chosen “site assignment overrides org assignment at that site” semantics.
  • Impact: If an org-scoped ADMIN grant is evaluated before a narrower site-scoped FIELD_TECH grant, sensitive actions like miner:reboot, miner:update_pools, or firmware updates may be allowed at a site where the admin explicitly tried to narrow access.
  • Recommendation: Implement Has as order-independent precedence: collect matching assignments, choose the narrowest applicable scope first, then allow if any role in that chosen scope has the permission. Add tests proving assignment order cannot change the result.

[HIGH] Planned U5 Migration Breaks the Staged Rollout

  • Category: Reliability | SQLi/Database
  • Location: docs/plans/2026-05-19-001-feat-granular-rbac-plan.md:839
  • Description: U5 renames user_organization.role_id and installs a trigger that rejects writes, but the delivery plan says U1-U5 land first with “no behavior change.” Current auth/session/user-management code still depends on user_organization.role_id until later units.
  • Impact: Applying the migration before all readers/writers are moved will break login role lookup, user creation, and existing sqlc queries, causing an outage rather than a safe soak.
  • Recommendation: Keep the legacy column usable during the dual-write period, migrate all readers/writers in the same release before renaming, or provide a compatibility layer. Only add the “fail loudly” trigger after CI and runtime code no longer reference the legacy column.

[MEDIUM] Built-In Role UI Contradicts Editable ADMIN/FIELD_TECH Design

  • Category: Frontend | Auth
  • Location: docs/plans/2026-05-19-001-feat-granular-rbac-plan.md:1463
  • Description: The plan repeatedly states ADMIN and FIELD_TECH are editable per org, but U11 specifies built-in roles are read-only in the RoleEditor and replace Save with Close.
  • Impact: Operators cannot use the supported UI to remove sensitive permissions such as miner:update_pools or miner:firmware_update from ADMIN, or add expected remediation actions to FIELD_TECH.
  • Recommendation: Make only SUPER_ADMIN read-only. ADMIN and FIELD_TECH should be editable but non-deletable, with clear warning copy and tests covering both built-in update flows.

Notes

This diff only adds an accepted planning document; no runtime code, migrations, generated protobufs, or frontend implementation changed yet. I did not find hardcoded pool, wallet, payout, or stratum addresses in the reviewed diff.


Generated by Codex Security Review |
Triggered by: @mcharles-square |
Review workflow run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an accepted technical design plan for evolving Proto Fleet authorization from the current coarse SUPER_ADMIN/ADMIN gate (RequireAdmin) to a granular, scoped permission model with role assignments, seeded built-in roles, and custom roles.

Changes:

  • Introduces a comprehensive RBAC design document covering schema, resolver/middleware flow, permission catalog, and rollout sequencing.
  • Specifies key security behaviors (fail-closed enforcement, cross-org IDOR checks, privilege-parity rules) and test strategy (unit + contract + E2E).

Comment on lines +218 to +219
| `fleet:read` | Required for any list/dashboard view (miner list, telemetry). Implicit floor — a role with any action permission must also hold `fleet:read`; enforced at role save (see U8). |
| `miner:read` | Per-row detail view, status snapshot, error history. Required alongside any `miner:*` action permission. |
Comment on lines +241 to +245
**Read pairing rule (enforced at role save).** Every action permission has
a corresponding read permission that must be present in the same role:
- any `miner:*` action requires `miner:read` and `fleet:read`
- `rack:manage` requires `rack:read` and `fleet:read`
- `site:manage` requires `site:read` and `fleet:read`
Comment on lines +1331 to +1334
- **Built-in roles read-only:** the modal still opens (admins want
to inspect the permission set), but the form is disabled and a
full-width info banner at the top reads "Built-in role — permissions
are managed by the application." Save button replaced with Close.
The original plan made built-in roles globally unique and editable.
Codex security review correctly flagged this as a cross-tenant
authorization bleed: a SUPER_ADMIN editing ADMIN in org A would
affect users in org B that hold ADMIN. Fixed by making EVERY role
per-org.

Updates:

- Summary: roles are now per-org; editing one org's ADMIN cannot
  leak into another org's ADMIN.
- Tech design: role table carries organization_id; uq_role_name
  (from 000002) is dropped and replaced with two partial unique
  indexes — built-ins unique per (org, builtin_key), customs unique
  per (org, name). chk_role_builtin_key_matches_flag enforces the
  is_builtin ↔ builtin_key invariant at the DB layer.
- U2 schema example: full DDL updated to show the per-org schema
  and the partial unique indexes for both role and
  user_organization_role (replacing the broken NULL-distinct
  UNIQUE constraint).
- U3 sqlc: every role query is org-aware; UpsertBuiltinRoleForOrg
  targets the partial index. SoftDeleteCustomRole gates on
  is_builtin=FALSE.
- U4 reconciliation: rewritten to loop over active orgs and call
  per-org SeedOrgBuiltins. Onboarding wiring documented —
  CreateAdminUserWithOrganization seeds per-org built-ins inside
  its existing tx so a new org's founding user can be assigned a
  per-org SUPER_ADMIN immediately, no boot required.
- Per-org test scenarios: explicit isolation test (editing org A's
  ADMIN does not affect org B's ADMIN); onboarding seeds per-org
  built-ins.
- Built-in editability section reframed: ADMIN and FIELD_TECH are
  editable per-org by a SUPER_ADMIN of that org; SUPER_ADMIN
  remains immutable per-org (full reconcile every boot).
Codex security review (round 2) correctly flagged that the original
"additive-only" reconciliation behavior was a bug: re-asserting the
seed set on every boot silently restored permissions an operator had
deliberately revoked (e.g., miner:update_pools, miner:firmware_update).

Updated the plan to reflect the corrected semantics:

- ADMIN and FIELD_TECH per-org rows are seeded on first creation
  only. After that, reconciliation never touches role_permission —
  the operator owns the role.
- Catalog growth does NOT auto-propagate to existing orgs' ADMIN or
  FIELD_TECH. SUPER_ADMIN remains fully reconciled.
- Test scenarios updated to assert the corrected behavior: sensitive
  permissions stay revoked across restarts; new catalog keys never
  silently appear in existing built-in rows.
- Risks section updated to note that startup reconciliation is no
  longer the safety net for ADMIN/FIELD_TECH; the safety net is the
  privilege-parity rule plus the always-available SUPER_ADMIN
  re-edit path.
- Decisions section: built-ins reconciliation reframed from
  "additive-only" (misleading) to "seed-on-create" (accurate).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants