Guardian Architecture
Surkyl Access Control System Documentation
Section titled “Surkyl Access Control System Documentation”Table of Contents
Section titled “Table of Contents”- Overview
- Architecture
- Permission Scopes
- System Roles
- Permission Structure
- Database Schema
- Permission Checking Logic
- Custom Roles
- Implementation Guide
- Security Best Practices
- API Reference
- Examples
Overview
Section titled “Overview”Surkyl implements a hierarchical Role-Based Access Control (RBAC) system with three permission scopes: App-level, Tenant-level, and Workspace-level. This design supports:
- ✅ System-defined roles (immutable)
- ✅ User-defined custom roles per tenant
- ✅ Fine-grained permissions for CRUD operations
- ✅ Permission inheritance across scopes
- ✅ Permission overrides for special cases
- ✅ Audit trails for all role assignments
- ✅ Time-limited role grants
Key Concepts
Section titled “Key Concepts”| Concept | Description |
|---|---|
| Permission | Atomic unit of access (e.g., project.create, page.delete) |
| Role | Collection of permissions at a specific scope |
| Scope | Level at which permissions apply (app/tenant/workspace) |
| User Assignment | Mapping of users to roles at specific scopes |
| Override | Direct permission grant/deny that supersedes role-based permissions |
Architecture
Section titled “Architecture”Hierarchical Permission Model
Section titled “Hierarchical Permission Model”┌─────────────────────────────────────────────────┐│ APP-LEVEL SCOPE ││ (Super Admin, Support, Platform Engineers) ││ ││ Can access ALL tenants and workspaces │└────────────────┬────────────────────────────────┘ │ ├─► Tenant A ──┬─► Workspace A1 │ ├─► Workspace A2 │ └─► Workspace A3 │ ├─► Tenant B ──┬─► Workspace B1 │ └─► Workspace B2 │ └─► Tenant C ──┬─► Workspace C1 └─► Workspace C2
┌─────────────────────────────────────────────────┐│ TENANT-LEVEL SCOPE ││ (Tenant Owner, Tenant Admin, Billing Manager) ││ ││ Can access all workspaces within their tenant │└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐│ WORKSPACE-LEVEL SCOPE ││ (Workspace Owner, Editor, Viewer, Creator) ││ ││ Can only access their specific workspace │└─────────────────────────────────────────────────┘Permission Resolution Flow
Section titled “Permission Resolution Flow”User requests action on resource │ ├─► 1. Check permission overrides (highest priority) │ └─► If ALLOW override exists → GRANT ACCESS │ └─► If DENY override exists → DENY ACCESS │ ├─► 2. Check app-level roles │ └─► Has required permission? → GRANT ACCESS │ ├─► 3. Check tenant-level roles (if tenant resource) │ └─► Has required permission in this tenant? → GRANT ACCESS │ ├─► 4. Check workspace-level roles (if workspace resource) │ └─► Has required permission in this workspace? → GRANT ACCESS │ └─► 5. No permission found → DENY ACCESSPermission Scopes
Section titled “Permission Scopes”App-Level Scope
Section titled “App-Level Scope”Purpose: Platform-wide administration and support
Who uses it: Surkyl employees (founders, support team, DevOps)
Key characteristics:
- Can access ALL tenant data
- Can view/manage any workspace
- Used for platform operations and support
Example permissions:
app.admin.full- Complete system accessapp.tenants.view- View any tenantapp.support.read_only- Read-only access for support
Tenant-Level Scope
Section titled “Tenant-Level Scope”Purpose: Organization/company-level administration
Who uses it: Company admins, billing managers, team leads
Key characteristics:
- Limited to single tenant (their organization)
- Can access all workspaces within their tenant
- Cannot see other tenants’ data
Example permissions:
tenant.admin.full- Full tenant controltenant.members.manage- Manage team memberstenant.billing.manage- Manage billing
Workspace-Level Scope
Section titled “Workspace-Level Scope”Purpose: Project/workspace-specific access
Who uses it: Project teams, content creators, clients
Key characteristics:
- Limited to single workspace
- Cannot access other workspaces (unless explicitly granted)
- Most granular permission level
Example permissions:
workspace.admin.full- Full workspace controlproject.create- Create projectspage.publish- Publish pages
System Roles
Section titled “System Roles”App-Level System Roles
Section titled “App-Level System Roles”Super Admin
Section titled “Super Admin”- Scope: App
- Description: Complete platform access - FOUNDERS ONLY
- Max Count: 2 (enforced by database trigger)
- Permissions: ALL app-level permissions
- Typical Users: VivinMeth (founder), co-founder
Platform Engineer
Section titled “Platform Engineer”- Scope: App
- Description: Infrastructure and tenant management
- Permissions:
app.tenants.viewapp.tenants.createapp.tenants.updateapp.tenants.manageapp.users.viewapp.infrastructure.viewapp.infrastructure.manageapp.analytics.view
- Typical Users: DevOps team
Support Agent
Section titled “Support Agent”- Scope: App
- Description: Customer support - read-only access
- Permissions:
app.support.read_onlyapp.tenants.viewapp.users.viewapp.workspaces.viewapp.support.create_tickets
- Typical Users: Customer support team
Support Lead
Section titled “Support Lead”- Scope: App
- Description: Senior support with limited edit access
- Permissions: All Support Agent permissions plus:
app.tenants.updateapp.users.updateapp.users.impersonate
- Typical Users: Senior support staff
Billing Administrator
Section titled “Billing Administrator”- Scope: App
- Description: Manage billing across all tenants
- Permissions:
app.tenants.viewapp.billing.view_allapp.billing.manage_allapp.analytics.view
- Typical Users: Finance team
Analytics Viewer
Section titled “Analytics Viewer”- Scope: App
- Description: View platform analytics and metrics
- Permissions:
app.analytics.viewapp.tenants.view
- Typical Users: Business analysts
Tenant-Level System Roles
Section titled “Tenant-Level System Roles”Tenant Owner
Section titled “Tenant Owner”- Scope: Tenant
- Description: Full tenant control
- Permissions: ALL tenant-level permissions
- Typical Users: Company founder/CEO
Tenant Admin
Section titled “Tenant Admin”- Scope: Tenant
- Description: Admin without billing access
- Permissions:
tenant.settings.managetenant.members.managetenant.members.invitetenant.members.viewtenant.workspaces.createtenant.workspaces.viewtenant.workspaces.managetenant.roles.manage
- Typical Users: Company CTO/admin
Tenant Member
Section titled “Tenant Member”- Scope: Tenant
- Description: Basic tenant access
- Permissions:
tenant.workspaces.viewworkspace.view
- Typical Users: Regular employees
Billing Manager
Section titled “Billing Manager”- Scope: Tenant
- Description: Billing management only
- Permissions:
tenant.billing.managetenant.billing.view
- Typical Users: Finance team
Workspace-Level System Roles
Section titled “Workspace-Level System Roles”Workspace Owner
Section titled “Workspace Owner”- Scope: Workspace
- Description: Full workspace control
- Permissions: ALL workspace-level permissions
- Typical Users: Project lead
Workspace Editor
Section titled “Workspace Editor”- Scope: Workspace
- Description: Edit all content
- Permissions:
workspace.viewproject.createproject.readproject.updateproject.deletepage.createpage.readpage.updatepage.delete
- Typical Users: Content team, developers
Workspace Viewer
Section titled “Workspace Viewer”- Scope: Workspace
- Description: Read-only access
- Permissions:
workspace.viewproject.readpage.read
- Typical Users: Stakeholders, clients
Content Creator
Section titled “Content Creator”- Scope: Workspace
- Description: Create and edit own content
- Permissions:
workspace.viewproject.createproject.readproject.update(own content)page.createpage.readpage.update(own content)
- Typical Users: Freelancers, contractors
Publisher
Section titled “Publisher”- Scope: Workspace
- Description: Can publish to production
- Permissions:
workspace.viewproject.readproject.publishpage.readpage.publish
- Typical Users: QA team, senior editors
Permission Structure
Section titled “Permission Structure”Permission Naming Convention
Section titled “Permission Naming Convention”Format: {scope}.{resource}.{action}
Examples:
app.admin.full- Complete app accesstenant.settings.manage- Manage tenant settingsworkspace.view- View workspaceproject.create- Create projectspage.publish- Publish pages
Complete Permission List
Section titled “Complete Permission List”App-Level Permissions
Section titled “App-Level Permissions”| Code | Name | Resource | Action | Description |
|---|---|---|---|---|
app.admin.full | Full App Admin | ALL | admin | Complete system access |
app.users.manage | Manage All Users | user | manage | CRUD any user |
app.users.view | View All Users | user | read | View any user |
app.users.create | Create Users | user | create | Create user accounts |
app.users.update | Update Users | user | update | Edit user information |
app.users.delete | Delete Users | user | delete | Delete user accounts |
app.users.impersonate | Impersonate Users | user | impersonate | Login as any user |
app.tenants.manage | Manage All Tenants | tenant | manage | CRUD any tenant |
app.tenants.view | View All Tenants | tenant | read | View any tenant |
app.tenants.create | Create Tenants | tenant | create | Create new tenants |
app.tenants.update | Update Tenants | tenant | update | Edit tenant settings |
app.tenants.delete | Delete Tenants | tenant | delete | Delete tenant accounts |
app.workspaces.view | View All Workspaces | workspace | read | View any workspace |
app.support.view | Support View | ALL | view | View all data for support |
app.support.read_only | Support Read Access | ALL | read | Read-only support access |
app.support.create_tickets | Create Support Tickets | ticket | create | Create internal tickets |
app.billing.view_all | View All Billing | billing | read | View all tenant billing |
app.billing.manage_all | Manage All Billing | billing | manage | Manage any tenant billing |
app.analytics.view | View Analytics | analytics | read | View platform analytics |
app.infrastructure.view | View Infrastructure | infrastructure | read | View server status |
app.infrastructure.manage | Manage Infrastructure | infrastructure | manage | Manage infrastructure |
Tenant-Level Permissions
Section titled “Tenant-Level Permissions”| Code | Name | Resource | Action | Description |
|---|---|---|---|---|
tenant.admin.full | Tenant Admin | ALL | admin | Full tenant access |
tenant.settings.manage | Manage Settings | tenant | manage | Edit tenant settings |
tenant.members.invite | Invite Members | member | invite | Invite users to tenant |
tenant.members.manage | Manage Members | member | manage | Edit member roles |
tenant.members.view | View Members | member | read | View team members |
tenant.workspaces.create | Create Workspaces | workspace | create | Create new workspaces |
tenant.workspaces.view | View All Workspaces | workspace | read | View all workspaces |
tenant.workspaces.manage | Manage All Workspaces | workspace | manage | Edit/delete any workspace |
tenant.billing.view | View Billing | billing | read | View billing info |
tenant.billing.manage | Manage Billing | billing | manage | Update payment methods |
tenant.roles.manage | Manage Custom Roles | role | manage | Create/edit custom roles |
Workspace-Level Permissions
Section titled “Workspace-Level Permissions”| Code | Name | Resource | Action | Description |
|---|---|---|---|---|
workspace.admin.full | Workspace Admin | ALL | admin | Full workspace access |
workspace.settings.manage | Manage Settings | workspace | manage | Edit workspace settings |
workspace.view | View Workspace | workspace | read | View workspace |
workspace.members.invite | Invite Members | member | invite | Invite to workspace |
workspace.members.manage | Manage Members | member | manage | Edit member roles |
project.create | Create Projects | project | create | Create new projects |
project.read | View Projects | project | read | View projects |
project.update | Edit Projects | project | update | Edit project details |
project.delete | Delete Projects | project | delete | Delete projects |
project.publish | Publish Projects | project | publish | Publish to production |
page.create | Create Pages | page | create | Create new pages |
page.read | View Pages | page | read | View pages |
page.update | Edit Pages | page | update | Edit page content |
page.delete | Delete Pages | page | delete | Delete pages |
page.publish | Publish Pages | page | publish | Publish pages |
Database Schema
Section titled “Database Schema”Core Tables
Section titled “Core Tables”permissions
Section titled “permissions”Stores all available permissions in the system.
CREATE TABLE permissions ( id UUID PRIMARY KEY NOT NULL, code VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, scope VARCHAR(50) NOT NULL, resource_type VARCHAR(100), action VARCHAR(50), is_system BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());Stores both system-defined and user-defined roles.
CREATE TABLE roles ( id UUID PRIMARY KEY NOT NULL, tenant_id UUID, name VARCHAR(100) NOT NULL, description TEXT, scope VARCHAR(50) NOT NULL, is_system BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID, updated_by UUID,
UNIQUE(tenant_id, name, scope));Key Points:
tenant_id IS NULL= app-level roletenant_id IS NOT NULL= tenant-specific roleis_system = true= cannot be modified/deleted
role_permissions
Section titled “role_permissions”Many-to-many relationship between roles and permissions.
CREATE TABLE role_permissions ( id UUID PRIMARY KEY NOT NULL, role_id UUID NOT NULL, permission_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, CONSTRAINT fk_permission FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE, UNIQUE(role_id, permission_id));user_app_roles
Section titled “user_app_roles”Assigns app-level roles to users.
CREATE TABLE user_app_roles ( id UUID PRIMARY KEY NOT NULL, user_id UUID NOT NULL, role_id UUID NOT NULL, granted_by UUID NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), granted_reason TEXT NOT NULL, expires_at TIMESTAMPTZ, last_used_at TIMESTAMPTZ,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, CONSTRAINT fk_granted_by FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE RESTRICT, UNIQUE(user_id, role_id));Key Points:
granted_reasonis required for audit purposesexpires_atallows time-limited accesslast_used_attracks role usage
user_tenant_roles
Section titled “user_tenant_roles”Assigns tenant-level roles to users.
CREATE TABLE user_tenant_roles ( id UUID PRIMARY KEY NOT NULL, user_id UUID NOT NULL, tenant_id UUID NOT NULL, role_id UUID NOT NULL, granted_by UUID NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, CONSTRAINT fk_granted_by FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE RESTRICT, UNIQUE(user_id, tenant_id, role_id));user_workspace_roles
Section titled “user_workspace_roles”Assigns workspace-level roles to users.
CREATE TABLE user_workspace_roles ( id UUID PRIMARY KEY NOT NULL, user_id UUID NOT NULL, workspace_id UUID NOT NULL, role_id UUID NOT NULL, granted_by UUID NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_workspace FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE, CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, CONSTRAINT fk_granted_by FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE RESTRICT, UNIQUE(user_id, workspace_id, role_id));user_permission_overrides
Section titled “user_permission_overrides”Direct permission grants/denies that override role-based permissions.
CREATE TABLE user_permission_overrides ( id UUID PRIMARY KEY NOT NULL, user_id UUID NOT NULL, permission_id UUID NOT NULL, resource_type VARCHAR(100) NOT NULL, resource_id UUID NOT NULL, grant_type VARCHAR(20) NOT NULL, granted_by UUID NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, reason TEXT,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_permission FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE, CONSTRAINT fk_granted_by FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE RESTRICT, CONSTRAINT chk_grant_type CHECK (grant_type IN ('allow', 'deny')), UNIQUE(user_id, permission_id, resource_type, resource_id));Use Cases:
- Grant special permission to one user without creating custom role
- Temporarily revoke specific permission from user
- Handle edge cases
Database Constraints
Section titled “Database Constraints”Super Admin Limit Trigger
Section titled “Super Admin Limit Trigger”CREATE OR REPLACE FUNCTION check_super_admin_limit()RETURNS TRIGGER AS $$DECLARE super_admin_role_id UUID; current_count INTEGER;BEGIN SELECT id INTO super_admin_role_id FROM roles WHERE name = 'Super Admin' AND scope = 'app' AND is_system = true;
IF NEW.role_id = super_admin_role_id THEN SELECT COUNT(*) INTO current_count FROM user_app_roles WHERE role_id = super_admin_role_id AND (expires_at IS NULL OR expires_at > NOW()) AND id != NEW.id;
IF current_count >= 2 THEN RAISE EXCEPTION 'Maximum number of Super Admins (2) already reached'; END IF; END IF;
RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_super_admin_limit BEFORE INSERT OR UPDATE ON user_app_roles FOR EACH ROW EXECUTE FUNCTION check_super_admin_limit();Indexes
Section titled “Indexes”-- PermissionsCREATE INDEX idx_permissions_code ON permissions(code);CREATE INDEX idx_permissions_scope ON permissions(scope);CREATE INDEX idx_permissions_resource ON permissions(resource_type, action);
-- RolesCREATE INDEX idx_roles_tenant ON roles(tenant_id);CREATE INDEX idx_roles_scope ON roles(scope);CREATE INDEX idx_roles_system ON roles(is_system) WHERE is_system = true;
-- Role PermissionsCREATE INDEX idx_role_permissions_role ON role_permissions(role_id);CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id);
-- User App RolesCREATE INDEX idx_user_app_roles_user ON user_app_roles(user_id);CREATE INDEX idx_user_app_roles_role ON user_app_roles(role_id);
-- User Tenant RolesCREATE INDEX idx_user_tenant_roles_user ON user_tenant_roles(user_id);CREATE INDEX idx_user_tenant_roles_tenant ON user_tenant_roles(tenant_id);CREATE INDEX idx_user_tenant_roles_role ON user_tenant_roles(role_id);
-- User Workspace RolesCREATE INDEX idx_user_workspace_roles_user ON user_workspace_roles(user_id);CREATE INDEX idx_user_workspace_roles_workspace ON user_workspace_roles(workspace_id);CREATE INDEX idx_user_workspace_roles_role ON user_workspace_roles(role_id);
-- User Permission OverridesCREATE INDEX idx_user_permission_overrides_user ON user_permission_overrides(user_id);CREATE INDEX idx_user_permission_overrides_resource ON user_permission_overrides(resource_type, resource_id);Permission Checking Logic
Section titled “Permission Checking Logic”Basic Permission Check
Section titled “Basic Permission Check”pub async fn user_has_permission( &self, user_id: Uuid, permission_code: &str,) -> Result<bool, sqlx::Error> { // 1. Check permission overrides (highest priority) // 2. Check app-level roles // 3. Check tenant-level roles // 4. Check workspace-level roles // 5. Return false if no permission found}Workspace Access with Inheritance
Section titled “Workspace Access with Inheritance”pub async fn user_can_access_workspace( &self, user_id: Uuid, workspace_id: Uuid, permission_code: &str,) -> Result<bool, sqlx::Error> { // User can access workspace through: // 1. App-level role (super admin) // 2. Tenant-level role (tenant admin sees all workspaces) // 3. Workspace-level role (direct workspace access)}Permission Resolution Priority
Section titled “Permission Resolution Priority”-
Permission Overrides (highest)
- If
ALLOWoverride exists → GRANT - If
DENYoverride exists → DENY
- If
-
App-Level Roles
- If user has app-level role with permission → GRANT
-
Tenant-Level Roles
- If checking tenant resource AND user has tenant-level role with permission → GRANT
-
Workspace-Level Roles
- If checking workspace resource AND user has workspace-level role with permission → GRANT
-
Default (lowest)
- No permission found → DENY
Custom Roles
Section titled “Custom Roles”Creating Custom Roles
Section titled “Creating Custom Roles”Users with tenant.roles.manage permission can create custom roles within their tenant.
Constraints:
- Custom roles cannot be created at app-level (only system roles)
- Role names must be unique within tenant + scope
- System roles (
is_system = true) cannot be modified - Must select valid permissions for the role’s scope
Example: Create “Content Reviewer” Role
-- 1. Create the roleINSERT INTO roles (id, tenant_id, name, description, scope, is_system, created_by)VALUES ( uuid_generate_v7(), 'tenant-uuid-here', 'Content Reviewer', 'Can review and approve content', 'workspace', false, 'creator-user-uuid');
-- 2. Assign permissionsINSERT INTO role_permissions (id, role_id, permission_id)SELECT uuid_generate_v7(), 'new-role-uuid', p.idFROM permissions pWHERE p.code IN ( 'workspace.view', 'project.read', 'page.read', 'page.update', 'page.publish');Modifying Custom Roles
Section titled “Modifying Custom Roles”- System roles cannot be modified
- Only tenant admins can modify custom roles in their tenant
- Modifications are tracked via
updated_byandupdated_at
Deleting Custom Roles
Section titled “Deleting Custom Roles”- System roles cannot be deleted
- Deleting a role automatically removes all user assignments (CASCADE)
- Role permissions are also removed (CASCADE)
Implementation Guide
Section titled “Implementation Guide”Step 1: Run Migrations
Section titled “Step 1: Run Migrations”# Create permissions tablesqlx migrate add create_permissions_table
# Create roles tablesqlx migrate add create_roles_table
# Create role assignments tablessqlx migrate add create_role_assignments_tables
# Seed system permissions and rolessqlx migrate add seed_system_permissions_and_roles
# Run all migrationssqlx migrate runStep 2: Implement Permission Service
Section titled “Step 2: Implement Permission Service”pub struct PermissionService { pool: PgPool,}
impl PermissionService { pub fn new(pool: PgPool) -> Self { Self { pool } }
pub async fn user_has_permission( &self, user_id: Uuid, permission_code: &str, resource_type: Option<&str>, resource_id: Option<Uuid>, ) -> Result<bool, sqlx::Error> { // Implementation }
pub async fn get_user_permissions( &self, user_id: Uuid, ) -> Result<UserPermissions, sqlx::Error> { // Implementation }}Step 3: Implement Middleware
Section titled “Step 3: Implement Middleware”pub async fn require_permission( permission: String,) -> impl Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send>> { // Implementation}Step 4: Protect Routes
Section titled “Step 4: Protect Routes”pub fn workspace_routes() -> Router<AppState> { Router::new() .route("/workspaces", post(create_workspace)) .route("/workspaces/:id", get(get_workspace)) .route("/workspaces/:id", put(update_workspace)) .route("/workspaces/:id", delete(delete_workspace))}Security Best Practices
Section titled “Security Best Practices”1. Principle of Least Privilege
Section titled “1. Principle of Least Privilege”✅ DO: Give users minimum permissions needed
❌ DON’T: Grant app.admin.full to everyone
2. Time-Limited Access
Section titled “2. Time-Limited Access”// Grant temporary god mode for 2 hoursgrant_temporary_god_mode( user_id, granted_by, "Emergency production fix", 2 // hours).await?;3. Audit All Privileged Actions
Section titled “3. Audit All Privileged Actions”tracing::warn!( "CRITICAL: User {} performed action {} on resource {}", user_id, action, resource_id);4. Separate Read from Write
Section titled “4. Separate Read from Write”app.tenants.view≠app.tenants.manageworkspace.view≠workspace.settings.manage
5. Regular Permission Reviews
Section titled “5. Regular Permission Reviews”- Audit who has app-level roles monthly
- Review tenant admin assignments quarterly
- Remove expired role assignments
6. Use Permission Overrides Sparingly
Section titled “6. Use Permission Overrides Sparingly”- Document why override was needed
- Set expiration dates
- Review regularly
API Reference
Section titled “API Reference”Permission Checking
Section titled “Permission Checking”user_has_permission
Section titled “user_has_permission”Check if user has a specific permission.
pub async fn user_has_permission( user_id: Uuid, permission_code: &str, resource_type: Option<&str>, resource_id: Option<Uuid>,) -> Result<bool, sqlx::Error>Example:
let can_delete = perm_service .user_has_permission(user_id, "project.delete", None, None) .await?;user_can_access_workspace
Section titled “user_can_access_workspace”Check if user can access a workspace with specific permission.
pub async fn user_can_access_workspace( user_id: Uuid, workspace_id: Uuid, permission_code: &str,) -> Result<bool, sqlx::Error>Example:
let can_edit = perm_service .user_can_access_workspace(user_id, workspace_id, "workspace.settings.manage") .await?;get_user_permissions
Section titled “get_user_permissions”Get all permissions for a user (for caching).
pub async fn get_user_permissions( user_id: Uuid,) -> Result<UserPermissions, sqlx::Error>Role Management
Section titled “Role Management”create_role
Section titled “create_role”Create a custom role.
pub async fn create_role( tenant_id: Option<Uuid>, created_by: Uuid, role: CreateRole,) -> Result<Role, AppError>assign_workspace_role
Section titled “assign_workspace_role”Assign a role to a user for a workspace.
pub async fn assign_workspace_role( user_id: Uuid, workspace_id: Uuid, role_id: Uuid, granted_by: Uuid, expires_at: Option<DateTime<Utc>>,) -> Result<(), AppError>list_roles
Section titled “list_roles”List available roles.
pub async fn list_roles( tenant_id: Option<Uuid>, scope: Option<PermissionScope>,) -> Result<Vec<Role>, AppError>Examples
Section titled “Examples”Example 1: Check if User Can Delete Project
Section titled “Example 1: Check if User Can Delete Project”use axum::{extract::Path, http::StatusCode, Extension};
pub async fn delete_project( State(app): State<AppState>, Extension(user): Extension<CurrentUser>, Path((workspace_id, project_id)): Path<(Uuid, Uuid)>,) -> Result<StatusCode, StatusCode> { // Check permission let can_delete = app.permission_service .user_can_access_workspace(user.id, workspace_id, "project.delete") .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !can_delete { return Err(StatusCode::FORBIDDEN); }
// Delete project sqlx::query!("DELETE FROM projects WHERE id = $1", project_id) .execute(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::NO_CONTENT)}Example 2: Create Custom “QA Tester” Role
Section titled “Example 2: Create Custom “QA Tester” Role”pub async fn create_qa_role( State(app): State<AppState>, Extension(user): Extension<CurrentUser>, tenant_id: Uuid,) -> Result<Json<Role>, StatusCode> { // Check if user can manage roles let can_manage = app.permission_service .user_has_permission(user.id, "tenant.roles.manage", None, None) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !can_manage { return Err(StatusCode::FORBIDDEN); }
// Get permission IDs for QA role let permission_codes = vec![ "workspace.view", "project.read", "page.read", "project.publish", "page.publish", ];
let permission_ids = sqlx::query_scalar!( "SELECT id FROM permissions WHERE code = ANY($1)", &permission_codes[..] ) .fetch_all(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Create role let role = app.role_service .create_role( Some(tenant_id), user.id, CreateRole { name: "QA Tester".to_string(), description: Some("Can test and approve releases".to_string()), scope: PermissionScope::Workspace, permission_ids, }, ) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(role))}Example 3: Grant Temporary Support Access
Section titled “Example 3: Grant Temporary Support Access”pub async fn grant_support_access( State(app): State<AppState>, Extension(admin): Extension<CurrentUser>, Path(support_user_id): Path<Uuid>,) -> Result<Json<SuccessResponse>, StatusCode> { // Only Super Admins can grant support access let is_admin = app.permission_service .user_has_permission(admin.id, "app.admin.full", None, None) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !is_admin { return Err(StatusCode::FORBIDDEN); }
// Get Support Agent role let support_role_id = sqlx::query_scalar!( "SELECT id FROM roles WHERE name = 'Support Agent' AND scope = 'app'" ) .fetch_one(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Grant role for 8 hours let expires_at = Utc::now() + chrono::Duration::hours(8);
sqlx::query!( r#" INSERT INTO user_app_roles (id, user_id, role_id, granted_by, granted_reason, expires_at) VALUES ($1, $2, $3, $4, $5, $6) "#, Uuid::now_v7(), support_user_id, support_role_id, admin.id, "Temporary support access for customer issue", expires_at ) .execute(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(SuccessResponse { message: format!("Support access granted until {}", expires_at), }))}Example 4: List User’s Roles Across All Scopes
Section titled “Example 4: List User’s Roles Across All Scopes”pub async fn get_my_roles( State(app): State<AppState>, Extension(user): Extension<CurrentUser>,) -> Result<Json<UserRolesSummary>, StatusCode> { // Get app-level roles let app_roles = sqlx::query_as!( RoleInfo, r#" SELECT r.id, r.name, r.scope as "scope: PermissionScope", uar.expires_at FROM user_app_roles uar JOIN roles r ON uar.role_id = r.id WHERE uar.user_id = $1 AND (uar.expires_at IS NULL OR uar.expires_at > NOW()) "#, user.id ) .fetch_all(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Get tenant-level roles let tenant_roles = sqlx::query_as!( TenantRoleInfo, r#" SELECT r.id, r.name, r.scope as "scope: PermissionScope", utr.tenant_id, t.name as tenant_name, utr.expires_at FROM user_tenant_roles utr JOIN roles r ON utr.role_id = r.id JOIN tenants t ON utr.tenant_id = t.id WHERE utr.user_id = $1 AND (utr.expires_at IS NULL OR utr.expires_at > NOW()) "#, user.id ) .fetch_all(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Get workspace-level roles let workspace_roles = sqlx::query_as!( WorkspaceRoleInfo, r#" SELECT r.id, r.name, r.scope as "scope: PermissionScope", uwr.workspace_id, w.name as workspace_name, uwr.expires_at FROM user_workspace_roles uwr JOIN roles r ON uwr.role_id = r.id JOIN workspaces w ON uwr.workspace_id = w.id WHERE uwr.user_id = $1 AND (uwr.expires_at IS NULL OR uwr.expires_at > NOW()) "#, user.id ) .fetch_all(&app.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(UserRolesSummary { app_roles, tenant_roles, workspace_roles, }))}Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues””Permission denied” but user has role
Section titled “”Permission denied” but user has role”Possible causes:
- Role assignment expired (
expires_atin past) - Permission not added to role
- Checking wrong scope (e.g., tenant permission on workspace resource)
- Permission override denying access
Debug:
-- Check user's rolesSELECT r.name, r.scope, uar.expires_atFROM user_app_roles uarJOIN roles r ON uar.role_id = r.idWHERE uar.user_id = 'user-uuid';
-- Check role's permissionsSELECT p.code, p.nameFROM role_permissions rpJOIN permissions p ON rp.permission_id = p.idWHERE rp.role_id = 'role-uuid';
-- Check for overridesSELECT * FROM user_permission_overridesWHERE user_id = 'user-uuid';Cannot create Super Admin
Section titled “Cannot create Super Admin”Cause: Limit of 2 Super Admins reached
Solution:
-- Check current Super AdminsSELECT u.display_name, uar.granted_atFROM user_app_roles uarJOIN users u ON uar.user_id = u.idJOIN roles r ON uar.role_id = r.idWHERE r.name = 'Super Admin'AND (uar.expires_at IS NULL OR uar.expires_at > NOW());
-- Revoke if neededDELETE FROM user_app_rolesWHERE id = 'role-assignment-uuid';Custom role not working
Section titled “Custom role not working”Possible causes:
- Role created with wrong scope
- Permissions not assigned to role
- User not assigned to role
Debug:
-- Check role detailsSELECT * FROM roles WHERE id = 'role-uuid';
-- Check role permissionsSELECT p.code FROM role_permissions rpJOIN permissions p ON rp.permission_id = p.idWHERE rp.role_id = 'role-uuid';
-- Check user assignmentSELECT * FROM user_workspace_rolesWHERE user_id = 'user-uuid' AND role_id = 'role-uuid';Appendix
Section titled “Appendix”Permission Matrix Reference
Section titled “Permission Matrix Reference”See Matrix 3: Permission Structure for complete permission list.
Migration Scripts
Section titled “Migration Scripts”All migration scripts are in /migrations directory:
001_create_permissions.sql002_create_roles.sql003_create_role_assignments.sql004_seed_system_data.sql005_add_super_admin_constraint.sql
Performance Optimization
Section titled “Performance Optimization”- Cache user permissions in Redis (5-minute TTL)
- Use materialized views for complex permission queries
- Index properly on foreign keys and frequently queried columns
- Batch permission checks when checking multiple permissions
Future Enhancements
Section titled “Future Enhancements”- Resource-level permissions (per-project, per-page)
- Permission groups/categories for easier management
- Permission request/approval workflow
- Role templates for common use cases
- Time-based role activation (scheduled grants)
- IP-based access restrictions
- MFA requirements for sensitive permissions
Last Updated: 2024-11-14
Maintained By: Surkyl Platform Team