Skip to main content

RBAC & Permissions

Lumio uses a role-based access control (RBAC) system with granular permissions. Every authenticated action is checked against the user's assigned role for the active account.

Default Roles

Every new account is created with four default roles:

RoleSlugColoris_systemis_defaultNotes
Ownerowner#f59e0btruetrueAll permissions. Cannot be edited or deleted.
Administratoradministrator#ef4444falsetrueAll permissions except account:delete and plan:edit.
Moderatormoderator#22c55efalsetrueChat moderation, event monitoring, read access.
Viewerviewer#6b7280falsetrueRead-only: events:read, overlays:read, events:userinfo.

Role Properties

PropertyTypeDescription
slugstringMachine-readable identifier. Stable, lowercase, used in code checks. Never changes.
namestringHuman-readable display name. Editable on custom roles, translated for default roles.
is_systemboolCannot be edited or deleted. Only the Owner role.
is_defaultboolCannot be deleted. All four default roles have this set.
colorstringHex color for UI display (e.g., "#f59e0b").

Role Name Translation

Default role names are translated via the useTranslateRole() hook in apps/web/src/lib/translate-role.ts. The hook returns a (slug, name) => string function that checks if the slug matches a known default role and returns the localized name; custom roles fall back to their name field.

Translation keys live in apps/web/messages/{en,de}.json under "roles.labels":

{
"roles": {
"labels": {
"owner": "Owner",
"administrator": "Administrator",
"moderator": "Moderator",
"viewer": "Viewer"
}
}
}

Permission Format

All permissions follow the resource:action format. Examples:

  • events:read, events:create, events:delete
  • chat:ban, chat:timeout, chat:notes
  • automations:execute, automations:history

Rules

  1. Always granular. Every resource needs at minimum read plus granular create/edit/delete.
  2. Domain-specific actions are added when CRUD is insufficient (e.g., automations:execute, chat:ban).
  3. Never use wildcards. No resource:manage or resource:* for account-level permissions. The only wildcard is the global admin permission admin:*, which is restricted to system administrators.

Account Permissions

The tables below summarize the major account permission categories. The authoritative list of constants lives in crates/lo-auth/src/rbac.rs (under pub mod account {}) — treat that file as the source of truth; categories and counts may shift as features evolve.

Events

PermissionLabel
events:readRead Events
events:createCreate Events
events:deleteDelete Events
events:userinfoEvent User Info

Overlays

PermissionLabel
overlays:readRead Overlays
overlays:createCreate Overlays
overlays:editEdit Overlays
overlays:deleteDelete Overlays

Spotify (6)

PermissionLabelOwnerAdminModViewer
spotify:readRead Spotifyxxx
spotify:playbackSpotify Playbackxxx
spotify:queueSpotify Queuexxx
spotify:playlistSpotify Playlistsxxx
spotify:deviceSpotify Devicesxxx
spotify:workerSpotify Workerxx

Chat (10)

PermissionLabelOwnerAdminModViewer
chat:readRead Chatxxx
chat:writeWrite Chatxxx
chat:userinfoChat User Infoxxx
chat:deleteDelete Chat Messagesxxx
chat:banBan Chat Usersxxx
chat:timeoutTimeout Chat Usersxxx
chat:notesChat User Notesxxx
chat:raidCancel Raidsxxx
chat:pollEnd Pollsxxx
chat:predictionEnd Predictionsxxx

Connections

PermissionLabel
connections:readRead Connections
connections:createCreate Connections
connections:editEdit Connections
connections:deleteDelete Connections

Settings (2)

PermissionLabelOwnerAdminModViewer
settings:readRead Settingsxx
settings:editEdit Settingsxx

Members (4)

PermissionLabelOwnerAdminModViewer
members:readRead Membersxxx
members:createCreate Invitesxx
members:editEdit Membersxx
members:deleteDelete Membersxx

Roles (3)

PermissionLabelOwnerAdminModViewer
roles:readRead Rolesxxx
roles:editEdit Rolesxx
roles:deleteDelete Rolesxx

Uploads

PermissionLabel
uploads:readRead Uploads
uploads:createCreate Uploads
uploads:deleteDelete Uploads

Rewards

PermissionLabel
rewards:readRead Rewards
rewards:createCreate Rewards
rewards:editEdit Rewards
rewards:deleteDelete Rewards

Tokens

PermissionLabel
tokens:readRead Tokens
tokens:createCreate Tokens
tokens:editEdit Tokens
tokens:deleteDelete Tokens

Automations (4)

PermissionLabelOwnerAdminModViewer
automations:readRead Automationsxxx
automations:writeWrite Automationsxx
automations:executeExecute Automationsxxx
automations:historyAutomation Historyxxx

Account (3)

PermissionLabelOwnerAdminModViewer
account:readRead Accountxx
account:editEdit Accountxx
account:deleteDelete Accountx

Plan (2)

PermissionLabelOwnerAdminModViewer
plan:readRead Planxx
plan:editEdit Planx

Admin-Scope Permissions

Admin-scope permissions gate the Lumio admin panel (/admin) and are entirely separate from account-scope permissions. They are stored in the admin_permissions column of CachedPermissions (not account_permissions) and enforced by require_admin_permission().

Two Modules, Two Contexts

The crates/lo-auth/src/rbac.rs file defines two modules:

  • pub mod account {} — Account-scope permission constants (listed above)
  • pub mod global {} — Admin-scope permission constants

Constants in global use the same resource:action format. The canonical list is exposed via:

lo_auth::rbac::all_admin_permissions() -> Vec<(&'static str, &'static str)>
// Returns (permission_string, category_label) pairs

This function is used by the permission picker UI and the REST/GraphQL permission validation.

Admin Roles

Admin roles are managed in the admin_roles / admin_role_permissions / user_admin_roles tables (introduced in migration 20260412000001). Unlike account roles, admin roles:

  • Are global (not account-scoped)
  • Have is_system: bool — system roles cannot be deleted
  • Auto-receive admin:access (the dashboard entry gate) on creation
  • Are CRUD-managed via adminRoles* GraphQL mutations and GET/POST/PATCH/DELETE /v1/admin/admin-roles

Shared Permissions

Some permission strings (e.g., copyright:read, obs:read, bot-modules:read) are declared in both pub mod account {} and pub mod global {} with identical string values. This is intentional — the same string gates different resources in different contexts:

  • Account scope: enforced by auth.require_permission(account::COPYRIGHT_READ) in account route handlers
  • Admin scope: enforced by auth.require_admin_permission(global::COPYRIGHT_READ) in admin route handlers

This follows the precedent set by bot-connections:*.

admin:access

admin:access is the single gate that controls dashboard access. Any user without this permission sees a "No Access" page. It is:

  • Always present in the global module as admin::ACCESS
  • Auto-injected into every admin role at creation time (both REST and GraphQL mutation)
  • Never removed by the permission diff algorithm

Backend Enforcement

GraphQL: PermissionGuard

Every GraphQL resolver that requires authorization uses the PermissionGuard from crates/lo-graphql/src/guard.rs:

#[graphql(guard = "lo_graphql::PermissionGuard::new(\"events:read\")")]
async fn events(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<Event>> {
// ...
}

The guard checks AuthContext::has_permission() and returns a GraphQL error ("Missing permission: events:read") if the user lacks the permission.

REST: require_permission

Every Actix route handler uses require_permission() from crates/lo-auth/src/context.rs:

use lo_auth::rbac::account;

pub async fn list_events(auth: Auth, state: web::Data<AppState>) -> Result<HttpResponse, ApiError> {
auth.require_permission(account::EVENTS_READ)
.map_err(from_auth_error)?;
// ...
}

This returns an AuthError::MissingPermission which maps to HTTP 403.

Permission Constants

All permission constants are defined in crates/lo-auth/src/rbac.rs inside pub mod account {}. Use the constants (not string literals) in REST handlers to prevent typos.

Frontend Enforcement

PermissionProvider

The PermissionProvider React context (apps/web/src/contexts/permission-context.tsx) receives the user's permissions from the server layout and makes them available to all client components via hooks.

Gate Component

Declaratively renders children only when the user has the required permission:

<Gate permission="overlays:write">
<Button>Create Overlay</Button>
</Gate>

PermissionErrorBoundary

Wraps entire page content to show a "No Access" fallback for users without the permission:

<PermissionErrorBoundary permission="spotify:read">
<MusicPlayer />
</PermissionErrorBoundary>

Imperative Checks

For use in hooks or event handlers:

const canEdit = useHasPerm("overlays:write");

See Frontend Permission System for the full API reference.

Redis Cache

Permissions are cached in Redis to avoid database lookups on every request.

CachedPermissions

The CachedPermissions struct in crates/lo-cache/src/client.rs stores permissions split by scope:

pub struct CachedPermissions {
pub global: Vec<String>, // Admin-level permissions
pub account: Vec<String>, // Account-level permissions
}
  • Cache key: lumio:perms:{user_id}:{account_id}
  • TTL: 300 seconds (5 minutes)
  • Invalidation: RedisClient::invalidate_permissions() is called whenever a user's role assignment changes

Key Files

FilePurpose
crates/lo-auth/src/rbac.rsPermission constants (account + global/admin) + all_admin_permissions()
crates/lo-auth/src/context.rsAuthContext::has_permission() + require_permission() + require_admin_permission()
crates/lo-graphql/src/guard.rsPermissionGuard for GraphQL resolvers
crates/lo-cache/src/client.rsCachedPermissions + Redis cache/invalidation
apps/api/src/graphql/roles.rsGraphQL available_permissions list (account scope)
apps/api/src/graphql/admin.rsGraphQL adminRoles* queries and mutations (admin scope)
apps/api/src/routes/roles.rsREST available_permissions list + validation
apps/api/src/routes/admin.rsREST admin-roles + admin-permissions endpoints
apps/api/src/db/admin_roles.rsDB helpers for admin role CRUD
apps/web/src/contexts/permission-context.tsxPermissionProvider + usePermissions() + useHasPerm()
apps/web/src/components/gate.tsx<Gate> declarative permission component
apps/web/src/components/permission-error-boundary.tsxPage-level access denial
apps/web/src/lib/translate-role.tstranslateRole() hook for default role names