Skip to content

Guardian Architecture

Surkyl Access Control System Documentation

Section titled “Surkyl Access Control System Documentation”
  1. Overview
  2. Architecture
  3. Permission Scopes
  4. System Roles
  5. Permission Structure
  6. Database Schema
  7. Permission Checking Logic
  8. Custom Roles
  9. Implementation Guide
  10. Security Best Practices
  11. API Reference
  12. Examples

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
ConceptDescription
PermissionAtomic unit of access (e.g., project.create, page.delete)
RoleCollection of permissions at a specific scope
ScopeLevel at which permissions apply (app/tenant/workspace)
User AssignmentMapping of users to roles at specific scopes
OverrideDirect permission grant/deny that supersedes role-based permissions

┌─────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────┘
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 ACCESS

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 access
  • app.tenants.view - View any tenant
  • app.support.read_only - Read-only access for support

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 control
  • tenant.members.manage - Manage team members
  • tenant.billing.manage - Manage billing

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 control
  • project.create - Create projects
  • page.publish - Publish pages

  • 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
  • Scope: App
  • Description: Infrastructure and tenant management
  • Permissions:
    • app.tenants.view
    • app.tenants.create
    • app.tenants.update
    • app.tenants.manage
    • app.users.view
    • app.infrastructure.view
    • app.infrastructure.manage
    • app.analytics.view
  • Typical Users: DevOps team
  • Scope: App
  • Description: Customer support - read-only access
  • Permissions:
    • app.support.read_only
    • app.tenants.view
    • app.users.view
    • app.workspaces.view
    • app.support.create_tickets
  • Typical Users: Customer support team
  • Scope: App
  • Description: Senior support with limited edit access
  • Permissions: All Support Agent permissions plus:
    • app.tenants.update
    • app.users.update
    • app.users.impersonate
  • Typical Users: Senior support staff
  • Scope: App
  • Description: Manage billing across all tenants
  • Permissions:
    • app.tenants.view
    • app.billing.view_all
    • app.billing.manage_all
    • app.analytics.view
  • Typical Users: Finance team
  • Scope: App
  • Description: View platform analytics and metrics
  • Permissions:
    • app.analytics.view
    • app.tenants.view
  • Typical Users: Business analysts
  • Scope: Tenant
  • Description: Full tenant control
  • Permissions: ALL tenant-level permissions
  • Typical Users: Company founder/CEO
  • Scope: Tenant
  • Description: Admin without billing access
  • Permissions:
    • tenant.settings.manage
    • tenant.members.manage
    • tenant.members.invite
    • tenant.members.view
    • tenant.workspaces.create
    • tenant.workspaces.view
    • tenant.workspaces.manage
    • tenant.roles.manage
  • Typical Users: Company CTO/admin
  • Scope: Tenant
  • Description: Basic tenant access
  • Permissions:
    • tenant.workspaces.view
    • workspace.view
  • Typical Users: Regular employees
  • Scope: Tenant
  • Description: Billing management only
  • Permissions:
    • tenant.billing.manage
    • tenant.billing.view
  • Typical Users: Finance team
  • Scope: Workspace
  • Description: Full workspace control
  • Permissions: ALL workspace-level permissions
  • Typical Users: Project lead
  • Scope: Workspace
  • Description: Edit all content
  • Permissions:
    • workspace.view
    • project.create
    • project.read
    • project.update
    • project.delete
    • page.create
    • page.read
    • page.update
    • page.delete
  • Typical Users: Content team, developers
  • Scope: Workspace
  • Description: Read-only access
  • Permissions:
    • workspace.view
    • project.read
    • page.read
  • Typical Users: Stakeholders, clients
  • Scope: Workspace
  • Description: Create and edit own content
  • Permissions:
    • workspace.view
    • project.create
    • project.read
    • project.update (own content)
    • page.create
    • page.read
    • page.update (own content)
  • Typical Users: Freelancers, contractors
  • Scope: Workspace
  • Description: Can publish to production
  • Permissions:
    • workspace.view
    • project.read
    • project.publish
    • page.read
    • page.publish
  • Typical Users: QA team, senior editors

Format: {scope}.{resource}.{action}

Examples:

  • app.admin.full - Complete app access
  • tenant.settings.manage - Manage tenant settings
  • workspace.view - View workspace
  • project.create - Create projects
  • page.publish - Publish pages
CodeNameResourceActionDescription
app.admin.fullFull App AdminALLadminComplete system access
app.users.manageManage All UsersusermanageCRUD any user
app.users.viewView All UsersuserreadView any user
app.users.createCreate UsersusercreateCreate user accounts
app.users.updateUpdate UsersuserupdateEdit user information
app.users.deleteDelete UsersuserdeleteDelete user accounts
app.users.impersonateImpersonate UsersuserimpersonateLogin as any user
app.tenants.manageManage All TenantstenantmanageCRUD any tenant
app.tenants.viewView All TenantstenantreadView any tenant
app.tenants.createCreate TenantstenantcreateCreate new tenants
app.tenants.updateUpdate TenantstenantupdateEdit tenant settings
app.tenants.deleteDelete TenantstenantdeleteDelete tenant accounts
app.workspaces.viewView All WorkspacesworkspacereadView any workspace
app.support.viewSupport ViewALLviewView all data for support
app.support.read_onlySupport Read AccessALLreadRead-only support access
app.support.create_ticketsCreate Support TicketsticketcreateCreate internal tickets
app.billing.view_allView All BillingbillingreadView all tenant billing
app.billing.manage_allManage All BillingbillingmanageManage any tenant billing
app.analytics.viewView AnalyticsanalyticsreadView platform analytics
app.infrastructure.viewView InfrastructureinfrastructurereadView server status
app.infrastructure.manageManage InfrastructureinfrastructuremanageManage infrastructure
CodeNameResourceActionDescription
tenant.admin.fullTenant AdminALLadminFull tenant access
tenant.settings.manageManage SettingstenantmanageEdit tenant settings
tenant.members.inviteInvite MembersmemberinviteInvite users to tenant
tenant.members.manageManage MembersmembermanageEdit member roles
tenant.members.viewView MembersmemberreadView team members
tenant.workspaces.createCreate WorkspacesworkspacecreateCreate new workspaces
tenant.workspaces.viewView All WorkspacesworkspacereadView all workspaces
tenant.workspaces.manageManage All WorkspacesworkspacemanageEdit/delete any workspace
tenant.billing.viewView BillingbillingreadView billing info
tenant.billing.manageManage BillingbillingmanageUpdate payment methods
tenant.roles.manageManage Custom RolesrolemanageCreate/edit custom roles
CodeNameResourceActionDescription
workspace.admin.fullWorkspace AdminALLadminFull workspace access
workspace.settings.manageManage SettingsworkspacemanageEdit workspace settings
workspace.viewView WorkspaceworkspacereadView workspace
workspace.members.inviteInvite MembersmemberinviteInvite to workspace
workspace.members.manageManage MembersmembermanageEdit member roles
project.createCreate ProjectsprojectcreateCreate new projects
project.readView ProjectsprojectreadView projects
project.updateEdit ProjectsprojectupdateEdit project details
project.deleteDelete ProjectsprojectdeleteDelete projects
project.publishPublish ProjectsprojectpublishPublish to production
page.createCreate PagespagecreateCreate new pages
page.readView PagespagereadView pages
page.updateEdit PagespageupdateEdit page content
page.deleteDelete PagespagedeleteDelete pages
page.publishPublish PagespagepublishPublish pages

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 role
  • tenant_id IS NOT NULL = tenant-specific role
  • is_system = true = cannot be modified/deleted

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)
);

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_reason is required for audit purposes
  • expires_at allows time-limited access
  • last_used_at tracks role usage

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)
);

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)
);

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
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();
-- Permissions
CREATE 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);
-- Roles
CREATE 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 Permissions
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id);
CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id);
-- User App Roles
CREATE 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 Roles
CREATE 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 Roles
CREATE 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 Overrides
CREATE 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);

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
}
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)
}
  1. Permission Overrides (highest)

    • If ALLOW override exists → GRANT
    • If DENY override exists → DENY
  2. App-Level Roles

    • If user has app-level role with permission → GRANT
  3. Tenant-Level Roles

    • If checking tenant resource AND user has tenant-level role with permission → GRANT
  4. Workspace-Level Roles

    • If checking workspace resource AND user has workspace-level role with permission → GRANT
  5. Default (lowest)

    • No permission found → DENY

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 role
INSERT 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 permissions
INSERT INTO role_permissions (id, role_id, permission_id)
SELECT
uuid_generate_v7(),
'new-role-uuid',
p.id
FROM permissions p
WHERE p.code IN (
'workspace.view',
'project.read',
'page.read',
'page.update',
'page.publish'
);
  • System roles cannot be modified
  • Only tenant admins can modify custom roles in their tenant
  • Modifications are tracked via updated_by and updated_at
  • System roles cannot be deleted
  • Deleting a role automatically removes all user assignments (CASCADE)
  • Role permissions are also removed (CASCADE)

Terminal window
# Create permissions table
sqlx migrate add create_permissions_table
# Create roles table
sqlx migrate add create_roles_table
# Create role assignments tables
sqlx migrate add create_role_assignments_tables
# Seed system permissions and roles
sqlx migrate add seed_system_permissions_and_roles
# Run all migrations
sqlx migrate run
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
}
}
pub async fn require_permission(
permission: String,
) -> impl Fn(Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send>> {
// Implementation
}
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))
}

DO: Give users minimum permissions needed ❌ DON’T: Grant app.admin.full to everyone

// Grant temporary god mode for 2 hours
grant_temporary_god_mode(
user_id,
granted_by,
"Emergency production fix",
2 // hours
).await?;
tracing::warn!(
"CRITICAL: User {} performed action {} on resource {}",
user_id, action, resource_id
);
  • app.tenants.viewapp.tenants.manage
  • workspace.viewworkspace.settings.manage
  • Audit who has app-level roles monthly
  • Review tenant admin assignments quarterly
  • Remove expired role assignments
  • Document why override was needed
  • Set expiration dates
  • Review regularly

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?;

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 all permissions for a user (for caching).

pub async fn get_user_permissions(
user_id: Uuid,
) -> Result<UserPermissions, sqlx::Error>

Create a custom role.

pub async fn create_role(
tenant_id: Option<Uuid>,
created_by: Uuid,
role: CreateRole,
) -> Result<Role, AppError>

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 available roles.

pub async fn list_roles(
tenant_id: Option<Uuid>,
scope: Option<PermissionScope>,
) -> Result<Vec<Role>, AppError>

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))
}
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,
}))
}

Possible causes:

  1. Role assignment expired (expires_at in past)
  2. Permission not added to role
  3. Checking wrong scope (e.g., tenant permission on workspace resource)
  4. Permission override denying access

Debug:

-- Check user's roles
SELECT r.name, r.scope, uar.expires_at
FROM user_app_roles uar
JOIN roles r ON uar.role_id = r.id
WHERE uar.user_id = 'user-uuid';
-- Check role's permissions
SELECT p.code, p.name
FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.id
WHERE rp.role_id = 'role-uuid';
-- Check for overrides
SELECT * FROM user_permission_overrides
WHERE user_id = 'user-uuid';

Cause: Limit of 2 Super Admins reached

Solution:

-- Check current Super Admins
SELECT u.display_name, uar.granted_at
FROM user_app_roles uar
JOIN users u ON uar.user_id = u.id
JOIN roles r ON uar.role_id = r.id
WHERE r.name = 'Super Admin'
AND (uar.expires_at IS NULL OR uar.expires_at > NOW());
-- Revoke if needed
DELETE FROM user_app_roles
WHERE id = 'role-assignment-uuid';

Possible causes:

  1. Role created with wrong scope
  2. Permissions not assigned to role
  3. User not assigned to role

Debug:

-- Check role details
SELECT * FROM roles WHERE id = 'role-uuid';
-- Check role permissions
SELECT p.code FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.id
WHERE rp.role_id = 'role-uuid';
-- Check user assignment
SELECT * FROM user_workspace_roles
WHERE user_id = 'user-uuid' AND role_id = 'role-uuid';

See Matrix 3: Permission Structure for complete permission list.

All migration scripts are in /migrations directory:

  • 001_create_permissions.sql
  • 002_create_roles.sql
  • 003_create_role_assignments.sql
  • 004_seed_system_data.sql
  • 005_add_super_admin_constraint.sql
  1. Cache user permissions in Redis (5-minute TTL)
  2. Use materialized views for complex permission queries
  3. Index properly on foreign keys and frequently queried columns
  4. Batch permission checks when checking multiple permissions
  • 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