Skip to main content

Overview

Risk Legion implements Role-Based Access Control (RBAC) at multiple layers: API middleware, database Row-Level Security (RLS), and frontend route protection. This multi-layer approach ensures comprehensive security.

Role Hierarchy

User Roles

Super Admin

AspectDescription
ScopePlatform-wide
PurposePlatform operations and enterprise management
Data AccessEnterprise metadata only, no business data
Stored Insuper_admins table

Client Admin

AspectDescription
ScopeSingle enterprise
PurposeEnterprise administration and BRA approval
Data AccessAll data within their enterprise
Stored Inenterprise_users (role = ‘admin’)

Assessor

AspectDescription
ScopeAssigned entities within enterprise
PurposeCreate and conduct BRAs
Data AccessAssigned Legal Entities/Business Units only
Stored Inenterprise_users (role = ‘assessor’) + user_entity_roles

Reviewer

AspectDescription
ScopeAssigned entities within enterprise
PurposeRead-only review and audit
Data AccessAssigned Legal Entities/Business Units only (read-only)
Stored Inenterprise_users (role = ‘reviewer’) + user_entity_roles

Permission Matrix

BRA Operations

OperationSuper AdminClient AdminAssessorReviewer
List BRAs✅ All✅ Assigned✅ Assigned
View BRA✅ Assigned✅ Assigned
Create BRA
Edit BRA✅ Own
Approve BRA
Cancel BRA

Governance Operations

OperationSuper AdminClient AdminAssessorReviewer
View Legal Entities✅ Assigned✅ Assigned
Create Legal Entities
Manage Risk Library
Configure Risk Appetite

Admin Operations

OperationSuper AdminClient AdminAssessorReviewer
Create Enterprise
View All Enterprises
Create Client Admin
Create Assessor/Reviewer
View Audit Logs

RBAC Implementation Layers

Layer 1: API Middleware

# backend/app/middleware/auth.py

from functools import wraps
from fastapi import HTTPException

def require_role(*allowed_roles):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            user = kwargs.get('current_user')
            
            if user.role not in allowed_roles:
                raise HTTPException(
                    status_code=403,
                    detail=f"Role '{user.role}' not authorized for this action"
                )
            
            return await func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
@app.post("/api/v1/bras/{bra_id}/approve")
@require_role("admin")
async def approve_bra(bra_id: str, current_user: User = Depends(get_current_user)):
    # Only Client Admins can reach here
    pass

Layer 2: Database RLS

-- Enterprise isolation
CREATE POLICY "enterprise_isolation" ON business_risk_assessments
FOR ALL
USING (
    enterprise_id = (
        SELECT enterprise_id FROM enterprise_users
        WHERE user_id = auth.uid()
        LIMIT 1
    )
);

-- Entity-level access for Assessors
CREATE POLICY "assessor_entity_access" ON business_risk_assessments
FOR SELECT
USING (
    -- Admins see all
    EXISTS (
        SELECT 1 FROM enterprise_users
        WHERE user_id = auth.uid() AND role = 'admin'
    )
    OR
    -- Assessors/Reviewers see only assigned entities
    legal_entity_id IN (
        SELECT legal_entity_id FROM user_entity_roles
        WHERE user_id = auth.uid()
    )
);

-- Write restrictions
CREATE POLICY "assessor_write_access" ON business_risk_assessments
FOR INSERT
WITH CHECK (
    EXISTS (
        SELECT 1 FROM enterprise_users
        WHERE user_id = auth.uid()
        AND role IN ('admin', 'assessor')
    )
);

Layer 3: Frontend Route Protection

// ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredRoles?: string[];
}

export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) {
  const { user, role, isLoading } = useAuth();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  if (requiredRoles && !requiredRoles.includes(role)) {
    return <Navigate to="/unauthorized" />;
  }
  
  return <>{children}</>;
}

// Usage in routes
<Route 
  path="/admin" 
  element={
    <ProtectedRoute requiredRoles={['super_admin']}>
      <AdminDashboard />
    </ProtectedRoute>
  } 
/>

Entity Assignment

Assigning Entities to Users

-- user_entity_roles table
CREATE TABLE user_entity_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES profiles(id),
    enterprise_id UUID NOT NULL REFERENCES enterprises(id),
    legal_entity_id UUID REFERENCES legal_entities(id),
    business_unit_id UUID REFERENCES business_units(id),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Assign user to specific Legal Entity
INSERT INTO user_entity_roles (user_id, enterprise_id, legal_entity_id)
VALUES ('user-uuid', 'enterprise-uuid', 'legal-entity-uuid');

-- Assign user to specific Business Unit
INSERT INTO user_entity_roles (user_id, enterprise_id, business_unit_id)
VALUES ('user-uuid', 'enterprise-uuid', 'business-unit-uuid');

Checking User Access

async def check_entity_access(user_id: str, legal_entity_id: str) -> bool:
    # Check if user is admin (has access to all)
    user = await get_enterprise_user(user_id)
    if user.role == 'admin':
        return True
    
    # Check specific entity assignment
    assignments = await get_user_entity_roles(user_id)
    return any(
        a.legal_entity_id == legal_entity_id 
        for a in assignments
    )

Role Resolution

Getting User Role

async def get_user_role(user_id: str) -> dict:
    # Check super admin first
    super_admin = await db.super_admins.get(user_id=user_id)
    if super_admin:
        return {"role": "super_admin", "scope": "platform"}
    
    # Get enterprise role
    enterprise_user = await db.enterprise_users.get(user_id=user_id)
    if not enterprise_user:
        raise HTTPException(status_code=403, detail="No role assigned")
    
    return {
        "role": enterprise_user.role,
        "enterprise_id": enterprise_user.enterprise_id,
        "scope": "enterprise" if enterprise_user.role == "admin" else "entity"
    }

Applying Role to Queries

async def get_bras(current_user: User):
    base_query = db.bras.select()
    
    # Apply enterprise filter
    base_query = base_query.filter(
        enterprise_id=current_user.enterprise_id
    )
    
    # Apply entity filter for non-admins
    if current_user.role != 'admin':
        entity_ids = await get_user_assigned_entities(current_user.id)
        base_query = base_query.filter(
            legal_entity_id__in=entity_ids
        )
    
    return await base_query.execute()

Audit Logging

All permission checks are logged:
async def log_access_attempt(
    user_id: str,
    action: str,
    resource: str,
    resource_id: str,
    allowed: bool
):
    await db.audit_log.insert({
        "user_id": user_id,
        "action_type": "access_check",
        "entity_type": resource,
        "entity_id": resource_id,
        "action_details": {
            "action": action,
            "allowed": allowed
        },
        "status": "success" if allowed else "denied"
    })

Best Practices

  • Grant minimum necessary permissions
  • Use specific entity assignments
  • Regularly review access
  • Remove access promptly when roles change
  • Implement RBAC at all layers
  • Don’t trust frontend-only validation
  • Use database RLS as final guard
  • Log all access attempts
  • Document role responsibilities
  • Train users on their permissions
  • Audit role assignments regularly
  • Use clear role naming
  • Test each role’s access paths
  • Verify denial cases
  • Test entity-level isolation
  • Regular security reviews