Skip to content

Roles and Permissions

WhiteMuush edited this page Sep 1, 2026 · 2 revisions

Roles and Permissions

Gardik uses permission-based access control. A role is a named set of permissions drawn from a fixed vocabulary, and a user holds at most one role within their company.

The old two-role ADMIN / VIEWER enum was removed in the rbac_roles migration. requireAdmin() no longer exists.

The permission vocabulary

Permissions are domain:action strings, defined once in src/lib/rbac/permissions.ts and frozen at 36 entries. Namespacing lets a domain be subdivided later without breaking a role that held the broader grant.

Domain Permissions
Alerts alerts:read, alerts:assign, alerts:status, alerts:comment, alerts:close, alerts:remediate
Employees employees:read, employees:manage, employees:scan
Exposure register register:read, register:manage, register:evidence
Dashboard dashboard:read, dashboard:customize, dashboard:manage_shared
Reports reports:read, reports:export, reports:schedule
Directory connectors connectors:read, connectors:manage, connectors:sync
Data API api_credentials:read, api_credentials:manage
Notifications notifications:read, notifications:manage
Security policy policy:read, policy:manage
Identity / IdP sso:read, sso:config, sso:role_map
RBAC users:read, users:manage, roles:read, roles:manage
Audit audit:read

Adding a feature means adding its permissions to that file. A coverage test fails the build if a mutating API route is not mapped to a permission.

Built-in role presets

Seeded per company from src/lib/rbac/presets.ts.

Preset Mandate
Administrator Every permission. Built-in, cannot be edited or deleted.
Security Manager Policy, connectors, SSO config, users and reports. Deliberately excludes roles:manage and sso:role_map.
SOC Analyst Alert triage, scans, and the exposure register. Read access elsewhere.
Viewer Read-only across the workspace.

Only Administrator is a system role. The other three are ordinary roles you can edit, rename or delete, and you can create your own from any subset of the 36 permissions.

Enforcing a permission

Guards live in src/lib/apiAuth.ts:

const { session, error } = await requireAuth()              // any signed-in user
const { session, error } = await requirePermission("alerts:status")
if (error) return error
  • requireAuth() returns 401 { error: "Unauthorized" } with no session.
  • requirePermission(perm) returns 401 when unauthenticated and 403 when the session's role does not hold perm.

Page-level guards use src/lib/rbac/guard-page.ts, and the route-to-permission map lives in src/lib/rbac/route-permissions.ts. See API Reference for the full matrix.

Escalation controls

Three rules stop a role manager from quietly becoming more powerful than they are meant to be.

No-escalation subset rule (src/lib/rbac/escalation.ts). You can only put into a role, or assign, permissions you already hold yourself. This prevents minting a role stronger than your own and granting it to a puppet account or back to yourself.

Crown jewels (src/lib/rbac/crown-jewels.ts). Four permissions control who can do what: roles:manage, users:manage, sso:config, sso:role_map. Handing any of them out, or creating a role that holds one, additionally requires a fresh step-up re-authentication.

Step-up re-auth (src/lib/rbac/step-up.ts). The user re-enters their password at POST /api/rbac/step-up, which writes a StepUpGrant valid for 5 minutes. Escalation-sensitive calls fail with 403 { error: "Step-up required", code: "STEP_UP_REQUIRED" } until a valid grant exists, so the UI can open a re-auth dialog instead of a dead end.

Last-admin protection (src/lib/rbac/last-admin.ts). A company must always keep at least one user holding roles:manage, otherwise it locks itself out of RBAC entirely. A role change that would drop that count to zero is refused.

Tenancy boundary

Roles operate within a company. Every query is scoped to the session's companyId, so even an Administrator only ever sees their own tenant's data. See Database Schema.

Audit

Every role and user change is written to the AuditLog table with the actor, the before and after state, and the source IP. See Audit Log.

Clone this wiki locally