Skip to main content

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

FieldDescription
idUnique identifier (CUID)
nameDisplay name (e.g., "Office Manager")
keyUnique key for code reference (e.g., "office_manager", "OWNER")
descriptionOptional description
isSystemBoolean flag (true only for OWNER role, prevents modification/deletion)

RolePermission​

Defines granular permissions for each role.

FieldDescription
roleIdForeign key to Role
resourceResourceType enum (which resource this permission applies to)
actionsArray of PermissionAction enum values
scopePermissionScope enum (ALL or SELF)
fieldAccessJSON 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 tracking
  • COMPANY_DOCUMENTS — shared company documentation
  • COMPANY_EQUIPMENT — company equipment inventory
  • COMPANY_BENEFITS — company benefits programs
  • COMPANY_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 data
  • EMPLOYEE_DOCUMENTS — employee personal documents
  • EMPLOYEE_EQUIPMENT — equipment assigned to employees
  • EMPLOYEE_ABSENCES — employee absence requests
  • EMPLOYEE_FEEDBACK — performance feedback and reviews
  • EMPLOYEE_EARNINGS — salary and compensation data

PermissionAction​

ActionDescription
VIEWRead access to resource data
CREATECreate new resource instances
EDITModify existing resources
DELETERemove resources
ASSIGNAssign resources to entities
EXPORTExport resource data

PermissionScope​

ScopeDescription
ALLCompany-wide access to all data within the resource
SELFAccess 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: ALL takes precedence over SELF when a user has multiple roles
  • OWNER role bypasses all checks (returns true for 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​

ResourceVIEWCREATEEDITDELETEASSIGNEXPORTScope
EMPLOYEESyesyesyesyesyesyesALL/SELF
COMPANY_ABSENCESyesyesyesyes-yesALL only
COMPANY_DOCUMENTSyesyesyesyes--ALL only
COMPANY_EQUIPMENTyesyesyesyesyes-ALL only
COMPANY_BENEFITSyesyesyesyesyes-ALL only
COMPANY_SETTINGSyesyesyesyes--ALL only
EMPLOYEE_PROFILEyes-yes---ALL/SELF
EMPLOYEE_DOCUMENTSyesyes-yes--ALL/SELF
EMPLOYEE_EQUIPMENTyes---yes-ALL/SELF
EMPLOYEE_ABSENCESyesyesyesyes--ALL/SELF
EMPLOYEE_FEEDBACKyesyesyes---ALL/SELF
EMPLOYEE_EARNINGSyesyesyesyes--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)