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
| Table | Description |
|---|---|
user_roles | id, 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
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
name | TEXT | Display name, editable on custom roles |
description | TEXT | Optional description |
slug | TEXT | Machine identifier, auto-derived from name |
is_system | BOOL | System roles cannot be deleted |
is_default | BOOL | Exactly one role has this set; it is the fallback for unassigned users |
created_at | TIMESTAMPTZ | |
updated_at | TIMESTAMPTZ |
Table: user_permission_overrides
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK | References users.id |
permission | TEXT | resource:action string |
granted | BOOL | true = explicitly grant, false = explicitly deny |
created_at | TIMESTAMPTZ |
Table: account_permission_overrides
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
account_id | UUID FK | References accounts.id |
permission | TEXT | resource:action string |
granted | BOOL | true = explicitly grant for all account members, false = explicitly deny |
created_at | TIMESTAMPTZ |
Default Roles
Three roles are seeded at startup via seed_default_user_roles. All three have is_system = true and cannot be deleted.
| Role | Slug | is_default | Permissions |
|---|---|---|---|
| Member | member | true | ideas:create, ideas:vote, ideas:comment, ideas:edit_own, ideas:delete_own, ideas:comment_edit_own, ideas:comment_delete_own, profile:read, profile:edit |
| Restricted | restricted | false | profile:read, profile:edit — can browse but not submit or vote |
| Moderator | moderator | false | All 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:
- Check the Redis cache —
lumio:user_permissions:{user_id}. If present, use the cached set and skip steps 2–5. - Load the user's role assignment — query
user_role_assignmentsfor the user. If absent, use the role withis_default = true. - Load role permissions — query
user_role_permissionsfor that role'sid. - Apply per-user overrides — for each row in
user_permission_overridesfor 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).
- Apply account overrides — for the user's active account (
account_idfrom the JWT), loadaccount_permission_overridesand apply the same grant/deny logic on top of the user-override result. - 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
| Property | Value |
|---|---|
| Redis key | lumio:user_permissions:{user_id} |
| TTL | 300 seconds (5 minutes) |
| Type | JSON-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 / Mutation | Permission | Description |
|---|---|---|
adminUserRoles | user-roles:read | List all user roles |
adminUserRole(id: UUID!) | user-roles:read | Get a single role with its permissions |
adminCreateUserRole(input: CreateUserRoleInput!) | user-roles:edit | Create a custom user role |
adminUpdateUserRole(input: UpdateUserRoleInput!) | user-roles:edit | Update role name, description, permissions |
adminDeleteUserRole(id: UUID!) | user-roles:edit | Delete a custom role (system roles are rejected) |
REST
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/admin/user-roles | user-roles:read | List user roles |
GET | /v1/admin/user-roles/{id} | user-roles:read | Get a single role |
POST | /v1/admin/user-roles | user-roles:edit | Create a role |
PATCH | /v1/admin/user-roles/{id} | user-roles:edit | Update a role |
DELETE | /v1/admin/user-roles/{id} | user-roles:edit | Delete a role |
GET | /v1/admin/user-roles/permissions | user-roles:read | List all available user-scope permission strings |
Per-User Role Assignments (Admin)
Gated on user-roles:edit.
| GraphQL Mutation / REST | Description |
|---|---|
adminAssignUserRole(userId: UUID!, roleId: UUID!) / PUT /v1/admin/users/{id}/user-role | Assign a user role (upsert) |
adminRemoveUserRole(userId: UUID!) / DELETE /v1/admin/users/{id}/user-role | Remove assignment (falls back to default role) |
Per-User Permission Overrides (Admin)
Gated on accounts:manage_permissions.
| GraphQL Mutation / REST | Description |
|---|---|
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-overrides | List overrides for a user |
Per-Account Permission Overrides (Admin)
Gated on accounts:manage_permissions.
| GraphQL Mutation / REST | Description |
|---|---|
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-overrides | List overrides for an account |
Resolved Permissions (Admin)
| Query / REST | Description |
|---|---|
adminUserResolvedPermissions(userId: UUID!) / GET /v1/admin/users/{id}/resolved-permissions | Returns 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
| Permission | Description |
|---|---|
user-roles:read | View user roles, their permissions, and user assignments in the admin panel |
user-roles:edit | Create, update, and delete custom user roles; assign or remove role assignments |
accounts:manage_permissions | Set 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
| File | Purpose |
|---|---|
crates/lo-auth/src/rbac.rs | User-scope permission constants (pub mod user {}) and default role definitions |
crates/lo-auth/src/context.rs | require_user_permission() and has_user_permission() on AuthContext |
crates/lo-cache/src/client.rs | invalidate_user_permissions() Redis helper |
apps/api/src/graphql/user_roles.rs | GraphQL queries and mutations for user roles, assignments, overrides |
apps/api/src/routes/user_roles.rs | REST handlers for user roles, assignments, overrides |
apps/api/src/db/user_roles.rs | DB operations: CRUD, assignment, override management, permission resolution |
apps/api/src/db/admin.rs | seed_default_user_roles() called at startup |
apps/admin/src/app/(admin)/user-roles/ | Admin panel pages for user role management |