Role-Based Access Control (RBAC)
DutyDuke implements a comprehensive RBAC system with granular permissions at the resource, action, and scope level.
Overview
The system supports:
- Multiple user roles with different permission levels
- Company-wide vs. self-only access scopes
- Action-level permissions (VIEW, CREATE, EDIT, DELETE, ASSIGN, EXPORT)
- Flexible role assignment to users
- System roles (OWNER) with immutable permissions
- Custom roles created and managed by administrators
Database Schema
The RBAC system uses four main tables:
Role
Stores role definitions (both system and custom roles).
| Field | Description |
|---|---|
id | Unique identifier (CUID) |
name | Display name (e.g., "Office Manager") |
key | Unique key for code reference (e.g., "office_manager", "OWNER") |
description | Optional description |
isSystem | Boolean flag (true only for OWNER role, prevents modification/deletion) |
RolePermission
Defines granular permissions for each role.
| Field | Description |
|---|---|
roleId | Foreign key to Role |
resource | ResourceType enum (which resource this permission applies to) |
actions | Array of PermissionAction enum values |
scope | PermissionScope enum (ALL or SELF) |
fieldAccess | JSON object for field-level permissions (future extension) |
Unique constraint on (roleId, resource) — one permission record per role per resource.
IdentityRole
Junction table linking identities to roles (many-to-many). Composite primary key on (identityId, roleId) with cascade deletes.
Permission Type System
ResourceType
Company-Level Resources (no SELF scope):
COMPANY_ABSENCES— company-wide absence policies and trackingCOMPANY_DOCUMENTS— shared company documentationCOMPANY_EQUIPMENT— company equipment inventoryCOMPANY_BENEFITS— company benefits programsCOMPANY_SETTINGS— system-wide settings
Employee Management (supports ALL/SELF scope):
EMPLOYEES— employee records and profiles
Employee-Specific Resources (supports ALL/SELF scope):
EMPLOYEE_PROFILE— individual employee profile dataEMPLOYEE_DOCUMENTS— employee personal documentsEMPLOYEE_EQUIPMENT— equipment assigned to employeesEMPLOYEE_ABSENCES— employee absence requestsEMPLOYEE_FEEDBACK— performance feedback and reviewsEMPLOYEE_EARNINGS— salary and compensation data
PermissionAction
| Action | Description |
|---|---|
VIEW | Read access to resource data |
CREATE | Create new resource instances |
EDIT | Modify existing resources |
DELETE | Remove resources |
ASSIGN | Assign resources to entities |
EXPORT | Export resource data |
PermissionScope
| Scope | Description |
|---|---|
ALL | Company-wide access to all data within the resource |
SELF | Access only to own data (requires employee record linked to identity) |
API-Level Implementation
Permission Checker
Core permission checking interface:
type PermissionChecker = {
can: (resource: ResourceType, action: PermissionAction) => boolean;
canAny: (resource: ResourceType, actions: PermissionAction[]) => boolean;
canAll: (resource: ResourceType, actions: PermissionAction[]) => boolean;
getScope: (resource: ResourceType) => PermissionScope | null;
isOwner: () => boolean;
serialize: () => SerializedPermissions;
};
Key behaviors:
- Aggregates permissions from all assigned roles (union of permissions)
- Scope priority:
ALLtakes precedence overSELFwhen a user has multiple roles - OWNER role bypasses all checks (returns
truefor all permissions) - Serialization support for passing permissions to client components
Authorization Wrappers
Three authorization patterns used in controllers:
requirePermission(resource, action, callback) — checks specific resource + action permission. Throws 403 if denied.
privateRoute(callback) — verifies authentication only. No specific permission check.
isOwnerRoute(callback) — requires OWNER role. Used for administrative operations.
Scope Enforcement
Controllers enforce scope by checking if the target employee matches the logged-in user:
const scope = checker.getScope(ResourceType.EMPLOYEE_EARNINGS);
if (scope === PermissionScope.SELF) {
const currentEmployee = await employeeQueries.getEmployeeByIdentityId(
checker.getIdentityId()
);
if (!currentEmployee || currentEmployee.id !== employeeId) {
throw new ApiError(403, 'Forbidden: Can only manage own earnings');
}
}
UI-Level Implementation
Server Components
import { getPermissionChecker } from '@/api/hris/authorization';
export default async function EmployeesPage() {
const checker = await getPermissionChecker();
const canCreate = checker.can(ResourceType.EMPLOYEES, PermissionAction.CREATE);
const permissions = checker.serialize();
return <EmployeesTable permissions={permissions} />;
}
Client Components
'use client';
import { canAccess, type SerializedPermissions } from '@/api/hris/authorization/client';
function EmployeeActions({ permissions }: { permissions: SerializedPermissions }) {
const canEdit = canAccess(permissions, ResourceType.EMPLOYEES, PermissionAction.EDIT);
return canEdit ? <EditButton /> : null;
}
Module separation:
@/api/hris/authorization— server-only exports (includes database access)@/api/hris/authorization/client— client-safe exports (enums, types, helpers)
Permission Matrix
| Resource | VIEW | CREATE | EDIT | DELETE | ASSIGN | EXPORT | Scope |
|---|---|---|---|---|---|---|---|
| EMPLOYEES | yes | yes | yes | yes | yes | yes | ALL/SELF |
| COMPANY_ABSENCES | yes | yes | yes | yes | - | yes | ALL only |
| COMPANY_DOCUMENTS | yes | yes | yes | yes | - | - | ALL only |
| COMPANY_EQUIPMENT | yes | yes | yes | yes | yes | - | ALL only |
| COMPANY_BENEFITS | yes | yes | yes | yes | yes | - | ALL only |
| COMPANY_SETTINGS | yes | yes | yes | yes | - | - | ALL only |
| EMPLOYEE_PROFILE | yes | - | yes | - | - | - | ALL/SELF |
| EMPLOYEE_DOCUMENTS | yes | yes | - | yes | - | - | ALL/SELF |
| EMPLOYEE_EQUIPMENT | yes | - | - | - | yes | - | ALL/SELF |
| EMPLOYEE_ABSENCES | yes | yes | yes | yes | - | - | ALL/SELF |
| EMPLOYEE_FEEDBACK | yes | yes | yes | - | - | - | ALL/SELF |
| EMPLOYEE_EARNINGS | yes | yes | yes | yes | - | - | ALL/SELF |
Common Role Examples
OWNER (System Role)
- All resources, all actions, ALL scope
- Cannot be modified or deleted
- Bypasses all permission checks in code
HR Manager (Custom Role)
- EMPLOYEES: VIEW, CREATE, EDIT, EXPORT (ALL scope)
- EMPLOYEE_ABSENCES: VIEW, CREATE, EDIT, DELETE (ALL scope)
- EMPLOYEE_DOCUMENTS: VIEW, CREATE, DELETE (ALL scope)
- COMPANY_BENEFITS: VIEW, ASSIGN (ALL scope)
- COMPANY_ABSENCES: VIEW, CREATE, EDIT (ALL scope)
Department Manager (Custom Role)
- EMPLOYEES: VIEW (ALL scope)
- EMPLOYEE_FEEDBACK: VIEW, CREATE, EDIT (ALL scope)
- EMPLOYEE_DOCUMENTS: VIEW (ALL scope)
- EMPLOYEE_ABSENCES: VIEW, CREATE, EDIT (SELF scope)
Employee (Self-Service Role)
- EMPLOYEE_PROFILE: VIEW, EDIT (SELF scope)
- EMPLOYEE_DOCUMENTS: VIEW (SELF scope)
- EMPLOYEE_ABSENCES: VIEW, CREATE (SELF scope)
- EMPLOYEE_EQUIPMENT: VIEW (SELF scope)