/** * CASL authorization constants. * * Maps our existing `{resource}:{action}` permission codes into CASL * `Action` + `Subject` pairs. * * ## Two-layer permission model * * 1. **Exact code** — `Access PermissionCode:` grants the specific * `resource:action` code. Every code a user holds (preset or custom) * gets an exact-code ability. PermissionGuard checks exact codes. * * 2. **Domain level** — only strictly equivalent CRUD codes * (`view|read|create|edit|update|delete`) create broad Subject abilities. * Workflow-specific operations remain exact-code-only. * * Unknown/custom codes (e.g. "student:nuke") get only layer 1, never * layer 2 — no domain ability is inferred. */ /** CASL action strings. */ export const CaslAction = { Manage: 'manage', Create: 'create', Read: 'read', Update: 'update', Delete: 'delete', /** Check exact permission code (e.g. "bill:export-excel"). * Used by PermissionGuard so workflow-specific operations remain distinct. */ Access: 'access', } as const; export type CaslAction = (typeof CaslAction)[keyof typeof CaslAction]; /** Subject names for every entity we protect. */ export const SubjectName = { all: 'all', Student: 'Student', Room: 'Room', Occupancy: 'Occupancy', Expense: 'Expense', Bill: 'Bill', Deposit: 'Deposit', Classroom: 'Classroom', Organization: 'Organization', ClassRental: 'ClassRental', Class: 'Class', Schedule: 'Schedule', Attendance: 'Attendance', Dashboard: 'Dashboard', Profile: 'Profile', Notification: 'Notification', OperationLog: 'OperationLog', User: 'User', Role: 'Role', Learning: 'Learning', Exam: 'Exam', Sync: 'Sync', Integration: 'Integration', Department: 'Department', AiConfig: 'AiConfig', } as const; export type SubjectName = (typeof SubjectName)[keyof typeof SubjectName]; /** Build the exact-code CASL subject string for a permission code. */ export function permissionCodeSubject(code: string): string { return `PermissionCode:${code}`; } // --------------------------------------------------------------------------- // Domain-level action mapping: permission code → CASL action // Used ONLY for the domain layer — not for exact-code access checks. // --------------------------------------------------------------------------- function permissionToAction(permission: string): CaslAction | null { const actionSegment = permission.split(':')[1] ?? permission; // Only strictly equivalent CRUD/read permission codes create broad domain // abilities. Workflow-specific operations remain exact-code-only so that, // for example, export cannot satisfy read and approve cannot satisfy update. switch (actionSegment) { case 'create': return CaslAction.Create; case 'view': case 'read': return CaslAction.Read; case 'edit': case 'update': return CaslAction.Update; case 'delete': return CaslAction.Delete; default: return null; } } function permissionToSubject(resource: string): SubjectName | null { switch (resource) { case 'dashboard': return SubjectName.Dashboard; case 'profile': return SubjectName.Profile; case 'notification': return SubjectName.Notification; case 'student': return SubjectName.Student; case 'room': return SubjectName.Room; case 'occupancy': return SubjectName.Occupancy; case 'expense': return SubjectName.Expense; case 'bill': return SubjectName.Bill; case 'deposit': return SubjectName.Deposit; case 'classroom': return SubjectName.Classroom; case 'organization': return SubjectName.Organization; case 'rental': return SubjectName.ClassRental; case 'log': return SubjectName.OperationLog; case 'user': return SubjectName.User; case 'role': return SubjectName.Role; case 'class': return SubjectName.Class; case 'schedule': return SubjectName.Schedule; case 'attendance': return SubjectName.Attendance; case 'learning': return SubjectName.Learning; case 'exam': return SubjectName.Exam; case 'sync': return SubjectName.Sync; case 'integration': return SubjectName.Integration; case 'department': return SubjectName.Department; case 'ai': return SubjectName.AiConfig; default: return null; } } export interface AbilityPermissionRule { action: CaslAction; subject: SubjectName; } /** * Map a known `resource:action` permission code to a domain-level * CASL rule, or `null` if the resource segment is unrecognised. * * Domain-level rules are used by services for data-scoping checks. * They are NOT used for exact-code access control — use * {@link permissionCodeSubject} for that. */ export function mapPermissionCode(code: string): AbilityPermissionRule | null { const [resource] = code.split(':'); const subject = permissionToSubject(resource ?? ''); if (!subject) return null; const action = permissionToAction(code); if (!action) return null; return { action, subject }; } /** * Whether the permission code is "known" — i.e. the resource maps to a * recognised subject. */ export function isKnownPermissionCode(code: string): boolean { const [resource] = code.split(':'); return permissionToSubject(resource ?? '') !== null; }