Files
gongxue-base/apps/server/src/authorization/casl.constants.ts

181 lines
5.3 KiB
TypeScript

/**
* CASL authorization constants.
*
* Maps our existing `{resource}:{action}` permission codes into CASL
* `Action` + `Subject` pairs.
*
* ## Two-layer permission model
*
* 1. **Exact code** — `Access PermissionCode:<code>` 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;
}