RBAC System Architecture
This document describes the technical architecture of the Role-Based Access Control (RBAC) system in the HB Life Science platform. It covers the data model, permission resolution flow, and integration points.
System Overviewβ
ποΈ RBAC Architecture Diagram
Frappe Backend β API Layer β Permission Context β UI Components
The RBAC system follows a Frappe-first architecture where:
- Frappe is the source of truth for all permission configurations
- Frontend dynamically loads permissions via API
- Static fallback is used only when API is unavailable
- UI components automatically adapt to user permissions
Data Modelβ
Frappe DocTypesβ
The permission system is built on four interconnected DocTypes in Frappe:
βββββββββββββββββββ βββββββββββββββββββ
β CTMS Role β β CTMS Resource β
βββββββββββββββββββ βββββββββββββββββββ
β β’ name β β β’ name β
β β’ description β β β’ description β
ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β
β βββββββββββββββββββ β
β β CTMS Action β β
β βββββββββββββββββββ β
β β β’ name β β
β β β’ description β β
β ββββββββββ¬βββββββββ β
β β β
ββββββββββββββΌβββββββββββ
β
βββββββββΌββββββββ
βCTMS Permissionβ
βββββββββββββββββ
β β’ role β
β β’ resource β
β β’ action β
β β’ is_enabled β
βββββββββββββββββ
| DocType | Purpose | Example Values |
|---|---|---|
| CTMS Role | Define user roles | Platform Administrator, Study Designer, Study Coordinator |
| CTMS Resource | Define protected resources | study, subject, crf, personnel, demographics |
| CTMS Action | Define available actions | create, read, update, delete, bulk_delete, export |
| CTMS Permission | Map role+resource+action to enabled/disabled | Study Designer + personnel + create = enabled |
Permission Record Structureβ
{
"name": "Study Coordinator-personnel-create",
"role": "Study Coordinator",
"resource": "personnel",
"action": "create",
"is_enabled": 1
}
Frontend Architectureβ
Component Hierarchyβ
πΌοΈ Frontend Component Hierarchy
PermissionProvider β useActionPermissions β ActionGuard β Button
App
βββ PermissionProvider // Loads permissions from Frappe
βββ Layout
βββ Page
βββ ActionGuard // Wraps protected buttons
βββ Button // Shown/hidden based on permission
Key Filesβ
| File | Purpose |
|---|---|
src/contexts/permission-context.tsx | Provides permission state to entire app |
src/hooks/useActionPermissions.ts | Hook for checking permissions |
src/components/guards/ActionGuard.tsx | Component wrapper for protected UI |
src/lib/auth/rbac/actions.ts | Type definitions and static fallback |
src/lib/api/rbac-api.ts | API calls to fetch permissions from Frappe |
Permission Resolution Flowβ
1. Initial Loadβ
When the application starts:
1. PermissionProvider mounts
2. Calls fetchPermissions() and fetchNavigationPermissions()
3. Builds PermissionMatrix from API response
4. Sets isLoaded = true, isUsingFallback = false
2. Permission Checkβ
When a component checks permission:
1. Component calls can('personnel', 'create')
2. useActionPermissions hook receives the call
3. Hook checks: isUsingFallback?
- If NO (API loaded): Check dynamic PermissionMatrix
- If YES (API failed): Check static ACTION_PERMISSIONS
4. Returns true/false
5. ActionGuard shows/hides child component
3. Matrix Structureβ
// PermissionMatrix structure
{
"Study Coordinator": {
"personnel": {
"create": true,
"read": true,
"update": true,
"delete": false
},
"subject": {
"create": true,
"read": true,
"update": true,
"delete": true
}
},
"Study Designer": {
// ... permissions
}
}
Flexible Type Systemβ
The system uses a flexible type approach that accepts any string value while providing TypeScript autocomplete for known values:
// Known values for autocomplete
export type KnownResource = 'study' | 'subject' | 'crf' | 'personnel' | ...;
export type KnownAction = 'create' | 'read' | 'update' | 'delete' | ...;
// Accept any string from Frappe
export type Resource = KnownResource | (string & {});
export type Action = KnownAction | (string & {});
Benefitsβ
| Benefit | Description |
|---|---|
| No code changes | New resources in Frappe work automatically |
| TypeScript support | Autocomplete for common values |
| Backward compatible | Existing code continues to work |
| Single source of truth | Frappe manages all permissions |
API Endpointsβ
Fetch Permissionsβ
GET /api/v1/doctype/CTMS Permission
Query: ?limit_page_length=0&fields=["*"]
Response:
{
"data": [
{
"name": "Study Coordinator-personnel-create",
"role": "Study Coordinator",
"resource": "personnel",
"action": "create",
"is_enabled": 1
},
// ... more permissions
]
}
Fetch Navigation Permissionsβ
GET /api/v1/doctype/CTMS Navigation Permission
Query: ?limit_page_length=0&fields=["*"]
Static Fallbackβ
When the API is unavailable, the system falls back to ACTION_PERMISSIONS:
export const ACTION_PERMISSIONS: Record<string, Partial<Record<string, Role[]>>> = {
study: {
create: [ROLES.ADMIN, ROLES.STUDY_DESIGNER],
read: [], // Empty = all authenticated users
update: [ROLES.ADMIN, ROLES.STUDY_DESIGNER],
delete: [ROLES.ADMIN],
},
personnel: {
create: [ROLES.ADMIN, ROLES.STUDY_DESIGNER, ROLES.STUDY_COORDINATOR],
read: [],
update: [ROLES.ADMIN, ROLES.STUDY_DESIGNER, ROLES.STUDY_COORDINATOR],
delete: [ROLES.ADMIN, ROLES.STUDY_DESIGNER],
},
// ... more resources
};
The static fallback only needs to cover common scenarios. It's not required to match Frappe exactly since it's only used when the API fails.
Integration Pointsβ
1. ActionGuard Componentβ
<ActionGuard resource="personnel" action="create">
<Button>Add Personnel</Button>
</ActionGuard>
2. Direct Hook Usageβ
const { can } = useActionPermissions();
// Check single permission
if (can('personnel', 'create')) {
// Show create button
}
// Multiple checks
const canEdit = can('study', 'update');
const canDelete = can('study', 'delete');
3. Conditional Renderingβ
{can('subject', 'create') && (
<Button onClick={handleCreate}>Add Subject</Button>
)}
Adding New Resourcesβ
When a new resource is added in Frappe:
- No frontend code changes required - Types are flexible
- Create CTMS Resource record in Frappe
- Create CTMS Permission records for each role+action combination
- Frontend automatically picks up new permissions on next load
Example: Adding "report" resourceβ
-- In Frappe
INSERT INTO `tabCTMS Resource` (name) VALUES ('report');
-- Add permissions
INSERT INTO `tabCTMS Permission` (role, resource, action, is_enabled) VALUES
('Platform Administrator', 'report', 'create', 1),
('Platform Administrator', 'report', 'read', 1),
('Study Designer', 'report', 'read', 1);
Frontend usage (works immediately):
<ActionGuard resource="report" action="create">
<Button>Generate Report</Button>
</ActionGuard>
Performance Considerationsβ
| Consideration | Implementation |
|---|---|
| Caching | Permissions loaded once on app start |
| Refresh | refresh() method available for manual reload |
| Parallel fetch | Permissions and nav permissions fetched in parallel |
| Memoization | Permission checks memoized in hooks |
Security Notesβ
- Frontend permissions are for UX only - Backend must also validate
- API endpoints validate permissions - Frappe handles authorization
- Tokens include role claims - Used for API-level authorization
- Audit logging - All permission changes logged in Frappe