Skip to main content

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:

  1. Frappe is the source of truth for all permission configurations
  2. Frontend dynamically loads permissions via API
  3. Static fallback is used only when API is unavailable
  4. 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 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
DocTypePurposeExample Values
CTMS RoleDefine user rolesPlatform Administrator, Study Designer, Study Coordinator
CTMS ResourceDefine protected resourcesstudy, subject, crf, personnel, demographics
CTMS ActionDefine available actionscreate, read, update, delete, bulk_delete, export
CTMS PermissionMap role+resource+action to enabled/disabledStudy 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​

FilePurpose
src/contexts/permission-context.tsxProvides permission state to entire app
src/hooks/useActionPermissions.tsHook for checking permissions
src/components/guards/ActionGuard.tsxComponent wrapper for protected UI
src/lib/auth/rbac/actions.tsType definitions and static fallback
src/lib/api/rbac-api.tsAPI 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​

BenefitDescription
No code changesNew resources in Frappe work automatically
TypeScript supportAutocomplete for common values
Backward compatibleExisting code continues to work
Single source of truthFrappe 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
};
note

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:

  1. No frontend code changes required - Types are flexible
  2. Create CTMS Resource record in Frappe
  3. Create CTMS Permission records for each role+action combination
  4. 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​

ConsiderationImplementation
CachingPermissions loaded once on app start
Refreshrefresh() method available for manual reload
Parallel fetchPermissions and nav permissions fetched in parallel
MemoizationPermission checks memoized in hooks

Security Notes​

  1. Frontend permissions are for UX only - Backend must also validate
  2. API endpoints validate permissions - Frappe handles authorization
  3. Tokens include role claims - Used for API-level authorization
  4. Audit logging - All permission changes logged in Frappe