You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All authenticated users currently have identical permissions. The auth_middleware in src/channels/web/auth.rs checks token validity but nothing else — any valid token can call any endpoint. The workspace_members.role column from Issue #2 stores roles but nothing enforces them.
No other schema changes needed — workspace_members.role already exists from Issue #2.
Permission Matrix
Operation
viewer
member
admin
owner
superadmin
View workspace data (jobs, routines, memory)
✓
✓
✓
✓
✓
Search memory
✓
✓
✓
✓
✓
Send chat messages
✗
✓
✓
✓
✓
Create/cancel jobs
✗
✓
✓
✓
✓
Write memory
✗
✓
✓
✓
✓
Manage own routines
✗
✓
✓
✓
✓
Manage workspace settings
✗
✗
✓
✓
✓
Invite/remove members
✗
✗
✓
✓
✓
Change member roles (up to admin)
✗
✗
✓
✓
✓
Promote to admin
✗
✗
✗
✓
✓
Archive/delete workspace
✗
✗
✗
✓
✓
Transfer ownership
✗
✗
✗
✓
✓
Create/suspend users
✗
✗
✗
✗
✓
View all workspaces
✗
✗
✗
✗
✓
Implementation: Permission Middleware Layer
Add a permission check layer that sits between auth and handlers:
// src/channels/web/permissions.rs#[derive(Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord)]pubenumRole{Viewer,Member,Admin,Owner,}#[derive(Debug,Clone,Copy)]pubenumPermission{WorkspaceRead,WorkspaceWrite,WorkspaceManageMembers,WorkspaceManageSettings,WorkspaceManageAdmins,WorkspaceDelete,SystemManageUsers,SystemViewAll,}implRole{pubfnhas_permission(&self,perm:Permission) -> bool{// Matrix lookup}}/// Axum extractor that verifies the authenticated user has the required/// role in the target workspace. Returns 403 Forbidden on failure.pubstructRequireRole<constROLE:u8>;// Or a middleware function approach:pubasyncfnrequire_workspace_role(user:&UserIdentity,workspace_id:Uuid,min_role:Role,store:&dynDatabase,) -> Result<(),(StatusCode,&'staticstr)>{if user.is_superadmin{returnOk(());}let role = store.get_member_role(workspace_id, user.db_user_id).await?;match role {Some(r)if r >= min_role => Ok(()),Some(_) => Err((StatusCode::FORBIDDEN,"Insufficient permissions")),None => Err((StatusCode::FORBIDDEN,"Not a workspace member")),}}
Handler Integration Pattern
Handlers call the permission check after extracting auth and workspace context:
pubasyncfnroutines_delete_handler(State(state):State<Arc<GatewayState>>,AuthenticatedUser(user):AuthenticatedUser,WorkspaceContext(ws):WorkspaceContext,Path(id):Path<String>,) -> Result<StatusCode,(StatusCode,String)>{// Personal workspace: user owns everything// Team workspace: check roleifletSome(ref ws) = ws {require_workspace_role(&user, ws.id,Role::Member, store).await?;}// ... existing logic}
Personal Workspace Permissions
For personal workspace (no workspace_id), the user is implicitly owner of all their own data. No DB lookup needed — just verify user_id matches, same as today.
API Token Scopes (Future-Ready)
The api_tokens.scopes column from Issue #1 can optionally restrict a token below its user's role. For example, a token with scopes: ["read"] on a member user acts as viewer. This issue should wire the plumbing but not implement fine-grained scope parsing — just ensure the architecture supports it.
Architecture Notes
Permission checks are NOT in the auth middleware. Auth middleware validates identity (who are you). Permission checks happen in handlers or a per-route middleware layer (are you allowed to do this). Keep these concerns separated.
Role comparison uses PartialOrd: Viewer < Member < Admin < Owner. This lets you do role >= Role::Member instead of matching each variant.
Cache workspace membership in GatewayState or alongside the auth cache from Issue Move whatsapp channel source to channels-src/ for consistency #1. Membership changes less frequently than requests. A 60s TTL LRU keyed on (user_id, workspace_id) avoids per-request DB hits.
Superadmin is intentionally not a role — it's a user flag. A superadmin still needs to be a member of a workspace to interact with it normally, but can bypass membership checks for administrative operations.
src/channels/web/handlers/jobs.rs — jobs_cancel_handler, jobs_restart_handler — examples of mutation endpoints that need Role::Member check
src/channels/web/handlers/routines.rs — routines_delete_handler, routines_toggle_handler — need Role::Member for own routines, Role::Admin for others'
src/channels/web/handlers/settings.rs — workspace settings need Role::Admin
src/channels/web/server.rs — GatewayState — add membership cache here
src/db/mod.rs — WorkspaceMgmtStore.get_member_role() from Issue feat: adding Web UI #2 — the DB method permissions will call
Acceptance Criteria
is_superadmin column added to users table (both backends)
Role enum with PartialOrd ordering (Viewer < Member < Admin < Owner)
Permission enum covering all operations in the permission matrix
require_workspace_role() function (or equivalent) callable from handlers
All workspace-scoped mutation endpoints check Role::Member minimum
Workspace member management endpoints check Role::Admin minimum
Workspace deletion/archival checks Role::Owner
User management endpoints (create, suspend) check is_superadmin
viewer role can read but not write memory, cannot send chat, cannot create jobs
member role can do everything except manage members or workspace settings
admin can manage members (up to admin) and workspace settings
owner can promote to admin, transfer ownership, delete workspace
Personal workspace (no workspace_id) bypasses role checks — user is implicit owner
superadmin bypasses all workspace role checks
Permission denied returns 403 with clear error message (not 404)
Membership cache with configurable TTL, eviction on role change
Unit tests for Role::has_permission() — test every cell in the matrix
Integration tests: viewer tries mutation → 403, member succeeds, role change reflected
cargo clippy zero warnings, cargo fmt clean
Pitfalls & Landmines
Don't put permission checks in auth_middleware. Auth is "who are you?" — permissions are "can you do this?". Mixing them makes it impossible to have endpoints with different permission requirements on the same route prefix.
Role escalation: an admin must NOT be able to promote someone to owner. Only owner can do that. Enforce max_assignable_role based on the caller's role: members can't change roles at all, admins can assign up to admin, owners can assign anything.
Ownership transfer must be atomic: old owner becomes admin, new owner becomes owner, in a single transaction. If the process crashes between the two updates, you have zero owners or two owners.
Don't forget SSE subscriptions: a viewer should receive workspace events (they can read), but a non-member should not. The SSE subscription logic in src/channels/web/sse.rs needs to verify membership, not just user_id matching.
The superadmin flag is a security-critical column. Default to false. The first user created (or migrated from env-var) should be superadmin. Add an explicit startup log line when a superadmin user is active.
Race condition on role checks: if an admin removes a member while that member has an in-flight request, the request should still complete (check at handler entry, not at DB write). Don't add permission checks deep in the DB layer.
Non-Goals
Fine-grained API token scopes — the column exists, the architecture supports it, but parsing/enforcement is a follow-up.
Custom roles — only the four built-in roles. Custom role definitions are out of scope.
Per-resource permissions (e.g., "this user can see job X but not job Y within the same workspace") — out of scope.
Problem / Motivation
All authenticated users currently have identical permissions. The
auth_middlewareinsrc/channels/web/auth.rschecks token validity but nothing else — any valid token can call any endpoint. Theworkspace_members.rolecolumn from Issue #2 stores roles but nothing enforces them.This means:
Depends on #1605 (User Management) and #1607 (Workspaces).
Proposed Solution
Permission Model
Four roles in ascending privilege order:
viewermemberadminownerAdditionally, a system-level
superadminflag onusersfor cross-workspace operations (user suspension, global settings).Database Changes
No other schema changes needed —
workspace_members.rolealready exists from Issue #2.Permission Matrix
Implementation: Permission Middleware Layer
Add a permission check layer that sits between auth and handlers:
Handler Integration Pattern
Handlers call the permission check after extracting auth and workspace context:
Personal Workspace Permissions
For personal workspace (no
workspace_id), the user is implicitlyownerof all their own data. No DB lookup needed — just verifyuser_idmatches, same as today.API Token Scopes (Future-Ready)
The
api_tokens.scopescolumn from Issue #1 can optionally restrict a token below its user's role. For example, a token withscopes: ["read"]on amemberuser acts asviewer. This issue should wire the plumbing but not implement fine-grained scope parsing — just ensure the architecture supports it.Architecture Notes
PartialOrd:Viewer < Member < Admin < Owner. This lets you dorole >= Role::Memberinstead of matching each variant.GatewayStateor alongside the auth cache from Issue Move whatsapp channel source to channels-src/ for consistency #1. Membership changes less frequently than requests. A 60s TTL LRU keyed on(user_id, workspace_id)avoids per-request DB hits.Code Pointers
src/channels/web/auth.rs—auth_middleware,AuthenticatedUser— identity extraction (don't modify, build on top)src/channels/web/handlers/jobs.rs—jobs_cancel_handler,jobs_restart_handler— examples of mutation endpoints that needRole::Memberchecksrc/channels/web/handlers/routines.rs—routines_delete_handler,routines_toggle_handler— needRole::Memberfor own routines,Role::Adminfor others'src/channels/web/handlers/settings.rs— workspace settings needRole::Adminsrc/channels/web/server.rs—GatewayState— add membership cache heresrc/db/mod.rs—WorkspaceMgmtStore.get_member_role()from Issue feat: adding Web UI #2 — the DB method permissions will callAcceptance Criteria
is_superadmincolumn added touserstable (both backends)Roleenum withPartialOrdordering (Viewer < Member < Admin < Owner)Permissionenum covering all operations in the permission matrixrequire_workspace_role()function (or equivalent) callable from handlersRole::MemberminimumRole::AdminminimumRole::Owneris_superadminviewerrole can read but not write memory, cannot send chat, cannot create jobsmemberrole can do everything except manage members or workspace settingsadmincan manage members (up to admin) and workspace settingsownercan promote to admin, transfer ownership, delete workspacesuperadminbypasses all workspace role checksRole::has_permission()— test every cell in the matrixcargo clippyzero warnings,cargo fmtcleanPitfalls & Landmines
auth_middleware. Auth is "who are you?" — permissions are "can you do this?". Mixing them makes it impossible to have endpoints with different permission requirements on the same route prefix.adminmust NOT be able to promote someone toowner. Onlyownercan do that. Enforcemax_assignable_rolebased on the caller's role: members can't change roles at all, admins can assign up toadmin, owners can assign anything.viewershould receive workspace events (they can read), but a non-member should not. The SSE subscription logic insrc/channels/web/sse.rsneeds to verify membership, not just user_id matching.superadminflag is a security-critical column. Default tofalse. The first user created (or migrated from env-var) should besuperadmin. Add an explicit startup log line when a superadmin user is active.Non-Goals
Part 3 of 4 — depends on #1605 (User Management) and #1607 (Workspaces). See also: Admin Panel (#4)