Skip to main content

User Roles

Overview

User Roles form the third RBAC layer in Lumio, sitting alongside the existing account-scope RBAC (controlling what a team member can do inside a streaming account) and the admin-scope RBAC (controlling what Lumio staff can do in the admin panel).

The User Role system governs what authenticated users — specifically users with direct access to the platform — can do. It controls actions like submitting ideas, commenting, and other user-facing community features that are not account-scoped. The system supports:

  • Default roles seeded on every new user (member, restricted, moderator-equivalent).
  • Custom roles created by admins for specific access patterns.
  • Per-user role assignments — each user is assigned exactly one user role at a time; unassigned users fall back to the default role.
  • Per-user permission overrides — grant or deny specific permissions for an individual user, regardless of their role.
  • Per-account permission overrides — admins can expand or restrict what all users from a given streaming account can do.

All resolution is cached in Redis for performance.

Tables

TableDescription
user_rolesid, name, description, slug, is_system (bool), is_default (bool), created_at, updated_at
user_role_permissions(role_id, permission) — permissions granted by a role
user_role_assignments(user_id, role_id) — each user has at most one row; absence means "use default role"
user_permission_overrides(user_id, permission, granted) — explicit per-user allow/deny overrides
account_permission_overrides(account_id, permission, granted) — per-account allow/deny overrides applied to all members of that account

Table: user_roles

ColumnTypeNotes
idUUID PK
nameTEXTDisplay name, editable on custom roles
descriptionTEXTOptional description
slugTEXTMachine identifier, auto-derived from name
is_systemBOOLSystem roles cannot be deleted
is_defaultBOOLExactly one role has this set; it is the fallback for unassigned users
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Table: user_permission_overrides

ColumnTypeNotes
idUUID PK
user_idUUID FKReferences users.id
permissionTEXTresource:action string
grantedBOOLtrue = explicitly grant, false = explicitly deny
created_atTIMESTAMPTZ

Table: account_permission_overrides

ColumnTypeNotes
idUUID PK
account_idUUID FKReferences accounts.id
permissionTEXTresource:action string
grantedBOOLtrue = explicitly grant for all account members, false = explicitly deny
created_atTIMESTAMPTZ

Default Roles

Three roles are seeded at startup via seed_default_user_roles. All three have is_system = true and cannot be deleted.

RoleSlugis_defaultPermissions
Membermembertrueideas:create, ideas:vote, ideas:comment, ideas:edit_own, ideas:delete_own, ideas:comment_edit_own, ideas:comment_delete_own, profile:read, profile:edit
Restrictedrestrictedfalseprofile:read, profile:edit — can browse but not submit or vote
ModeratormoderatorfalseAll 9 member permissions plus ideas:moderate_comment, ideas:moderate_edit, ideas:moderate_delete, ideas:moderate_status

The member role is the fallback for every user who does not have an explicit user_role_assignments row.

Permission Resolution

When a request arrives and the middleware resolves user_permissions for a User auth context, the steps are:

  1. Check the Redis cachelumio:user_permissions:{user_id}. If present, use the cached set and skip steps 2–5.
  2. Load the user's role assignment — query user_role_assignments for the user. If absent, use the role with is_default = true.
  3. Load role permissions — query user_role_permissions for that role's id.
  4. Apply per-user overrides — for each row in user_permission_overrides for the user:
    • granted = true: add the permission to the set (grant even if not in role).
    • granted = false: remove the permission from the set (deny even if in role).
  5. Apply account overrides — for the user's active account (account_id from the JWT), load account_permission_overrides and apply the same grant/deny logic on top of the user-override result.
  6. Write to Redis — cache the final permission set with a 5-minute TTL.

The resolved set is stored in AuthContext::User::user_permissions: Vec<String>. Use auth.require_user_permission("resource:action") (from crates/lo-auth/src/context.rs) to enforce it in REST handlers.

Account Permission Overrides

Account overrides let platform admins give every member of a streaming account an elevated or restricted set of user-scope permissions. For example, a verified streamer account might receive ideas:create even if that permission were later removed from the default role.

Account overrides are loaded from account_permission_overrides using the account_id embedded in the user's JWT, and are applied after per-user overrides (they are the last layer).

AuthContext Changes

The AuthContext::User variant has a user_permissions: Vec<String> field populated by the auth middleware:

AuthContext::User {
user_id: Uuid,
account_id: Option<Uuid>,
session_id: Option<Uuid>,
global_permissions: Vec<String>,
account_permissions: Vec<String>,
user_permissions: Vec<String>, // <-- user-role layer
}

Enforcement helper:

// crates/lo-auth/src/context.rs
impl AuthContext {
pub fn require_user_permission(&self, permission: &str) -> Result<(), AuthError> { ... }
pub fn has_user_permission(&self, permission: &str) -> bool { ... }
}

Use these in REST handlers exactly like require_permission() but targeting the user layer:

pub async fn create_idea(auth: Auth, ...) -> Result<HttpResponse, ApiError> {
auth.require_user_permission(user_perms::IDEAS_CREATE)
.map_err(from_auth_error)?;
// ...
}

Cache

PropertyValue
Redis keylumio:user_permissions:{user_id}
TTL300 seconds (5 minutes)
TypeJSON-encoded Vec<String>

Invalidation — call RedisClient::invalidate_user_permissions(user_id) whenever:

  • A user's role assignment changes.
  • A user's permission overrides change.
  • A user role's permission set changes.
  • An account permission override for the user's active account changes.

The invalidation helper is defined in crates/lo-cache/src/client.rs.

User Role Permission Constants

User-scope permission constants live in crates/lo-auth/src/rbac.rs inside pub mod user {}:

pub mod user {
pub const IDEAS_CREATE: &str = "ideas:create";
pub const IDEAS_VOTE: &str = "ideas:vote";
pub const IDEAS_COMMENT: &str = "ideas:comment";
pub const IDEAS_EDIT_OWN: &str = "ideas:edit_own";
pub const IDEAS_DELETE_OWN: &str = "ideas:delete_own";
pub const IDEAS_COMMENT_EDIT_OWN: &str = "ideas:comment_edit_own";
pub const IDEAS_COMMENT_DELETE_OWN: &str = "ideas:comment_delete_own";
pub const IDEAS_MODERATE_COMMENT: &str = "ideas:moderate_comment";
pub const IDEAS_MODERATE_EDIT: &str = "ideas:moderate_edit";
pub const IDEAS_MODERATE_DELETE: &str = "ideas:moderate_delete";
pub const IDEAS_MODERATE_STATUS: &str = "ideas:moderate_status";
pub const PROFILE_READ: &str = "profile:read";
pub const PROFILE_EDIT: &str = "profile:edit";
}

Always use the constants in code rather than string literals to prevent typos.

API

User Role CRUD (Admin)

The user role management API is admin-scoped, gated on user-roles:read / user-roles:edit.

GraphQL

Query / MutationPermissionDescription
adminUserRolesuser-roles:readList all user roles
adminUserRole(id: UUID!)user-roles:readGet a single role with its permissions
adminCreateUserRole(input: CreateUserRoleInput!)user-roles:editCreate a custom user role
adminUpdateUserRole(input: UpdateUserRoleInput!)user-roles:editUpdate role name, description, permissions
adminDeleteUserRole(id: UUID!)user-roles:editDelete a custom role (system roles are rejected)

REST

MethodPathPermissionDescription
GET/v1/admin/user-rolesuser-roles:readList user roles
GET/v1/admin/user-roles/{id}user-roles:readGet a single role
POST/v1/admin/user-rolesuser-roles:editCreate a role
PATCH/v1/admin/user-roles/{id}user-roles:editUpdate a role
DELETE/v1/admin/user-roles/{id}user-roles:editDelete a role
GET/v1/admin/user-roles/permissionsuser-roles:readList all available user-scope permission strings

Per-User Role Assignments (Admin)

Gated on user-roles:edit.

GraphQL Mutation / RESTDescription
adminAssignUserRole(userId: UUID!, roleId: UUID!) / PUT /v1/admin/users/{id}/user-roleAssign a user role (upsert)
adminRemoveUserRole(userId: UUID!) / DELETE /v1/admin/users/{id}/user-roleRemove assignment (falls back to default role)

Per-User Permission Overrides (Admin)

Gated on accounts:manage_permissions.

GraphQL Mutation / RESTDescription
adminSetUserPermissionOverride(userId: UUID!, permission: String!, granted: Boolean!) / PUT /v1/admin/users/{id}/permission-overrides/{permission}Set an override
adminRemoveUserPermissionOverride(userId: UUID!, permission: String!) / DELETE /v1/admin/users/{id}/permission-overrides/{permission}Remove an override
adminUserPermissionOverrides(userId: UUID!) / GET /v1/admin/users/{id}/permission-overridesList overrides for a user

Per-Account Permission Overrides (Admin)

Gated on accounts:manage_permissions.

GraphQL Mutation / RESTDescription
adminSetAccountPermissionOverride(accountId: UUID!, permission: String!, granted: Boolean!) / PUT /v1/admin/accounts/{id}/permission-overrides/{permission}Set an override
adminRemoveAccountPermissionOverride(accountId: UUID!, permission: String!) / DELETE /v1/admin/accounts/{id}/permission-overrides/{permission}Remove an override
adminAccountPermissionOverrides(accountId: UUID!) / GET /v1/admin/accounts/{id}/permission-overridesList overrides for an account

Resolved Permissions (Admin)

Query / RESTDescription
adminUserResolvedPermissions(userId: UUID!) / GET /v1/admin/users/{id}/resolved-permissionsReturns the final effective permission set for a user after applying role + user overrides + account overrides, without writing to cache. Used by the admin effective-permissions view.

Admin Permissions

PermissionDescription
user-roles:readView user roles, their permissions, and user assignments in the admin panel
user-roles:editCreate, update, and delete custom user roles; assign or remove role assignments
accounts:manage_permissionsSet and remove per-user and per-account permission overrides

These are admin-scope permissions, defined in crates/lo-auth/src/rbac.rs::global and enforced via AdminPermissionGuard / require_admin_permission().

Key Files

FilePurpose
crates/lo-auth/src/rbac.rsUser-scope permission constants (pub mod user {}) and default role definitions
crates/lo-auth/src/context.rsrequire_user_permission() and has_user_permission() on AuthContext
crates/lo-cache/src/client.rsinvalidate_user_permissions() Redis helper
apps/api/src/graphql/user_roles.rsGraphQL queries and mutations for user roles, assignments, overrides
apps/api/src/routes/user_roles.rsREST handlers for user roles, assignments, overrides
apps/api/src/db/user_roles.rsDB operations: CRUD, assignment, override management, permission resolution
apps/api/src/db/admin.rsseed_default_user_roles() called at startup
apps/admin/src/app/(admin)/user-roles/Admin panel pages for user role management