Skip to content

feat: RBAC roles (tiered, profile-scoped) - #624

Open
littledivy wants to merge 2 commits into
mainfrom
feat/rbac-roles
Open

feat: RBAC roles (tiered, profile-scoped)#624
littledivy wants to merge 2 commits into
mainfrom
feat/rbac-roles

Conversation

@littledivy

Copy link
Copy Markdown
Member

What

Adds a role layer on top of the existing dashboard identity gate. Authorization was binary: any caller who cleared the gate (root password or a tailnet `operators` allowlist login) could edit any profile, set any credential, approve any device. Roles let an admin scope an operator to a single profile.

Model

Identity decoupled from transport — tailscale whois and dashboard password are identity providers; roles live in the DB, so the same model works for WireGuard-only nodes that have no whois.

  • Roles: `viewer` < `editor` < `admin`
  • Scope: `*` (global) or `profile:`

Changes

  • migration `0020_rbac.sql`: `rbac_users` / `rbac_identities` / `rbac_role_bindings`
  • `roles.go`: authorize predicates, seeding
  • `roles_api.go`: `authzGate` middleware + `/api/rbac/{me,users,grant,revoke}`
  • enforce: device→profile assign (`canEditProfile`), credential edits (`canEditGlobal`); `/api/rbac/*` admin-only with self-lockout guard
  • `doc/roles.md`

Backward compatibility

Additive / non-breaking. An already-gated identity with no bindings is lazily granted `admin/*`, so upgrade changes no behavior; narrowing is opt-in.

Not in V1 (see doc/roles.md)

Operators-allowlist removal, multi-user password login, dashboard UI, onboarding-approve scoping, audit log.

Test

`go test ./cmd/clawpatrol/ -run 'TestRole|TestGrant|TestAuthorize|TestSeed|TestEffective|TestRevoke|TestList'` — green. gofmt clean.

Add a role layer on top of the existing dashboard identity gate.
Authorization was binary: any caller who cleared the gate (root
password or a tailnet operators-allowlist login) could edit any
profile, set any credential, approve any device. Roles let an admin
scope an operator to a single profile.

Model decouples identity from transport: tailscale whois and dashboard
password are identity providers; roles live in the DB, so the same
model works for WireGuard-only nodes with no whois.

- migration 0020: rbac_users / rbac_identities / rbac_role_bindings
- roles.go: viewer<editor<admin x scope (* | profile:<name>),
  authorize predicates, seeding
- roles_api.go: authzGate middleware + /api/rbac/{me,users,grant,revoke}
- enforce: device->profile assign (canEditProfile), credential edits
  (canEditGlobal); /api/rbac/* admin-only with self-lockout guard
- doc/roles.md

Additive / non-breaking: an already-gated identity with no bindings is
lazily granted admin/*, so upgrade changes no behavior; narrowing is
opt-in. Operators allowlist, multi-user login, UI, onboarding-approve
scoping, audit log deferred (see doc/roles.md).
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

🚀 Preview deployed: https://demo.clawpatrol.dev/pr-preview/pr-624/

Built from e879709d4f1065d3ca6d8fe383523f4f72521733 at 2026-06-02T14:48:25Z.

- rows.Close via deferred closure (errcheck, repo convention)
- rewrite role-rank assertion without negation (staticcheck QF1001)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant