forked from wangziqi/gongxue-base
181 lines
5.3 KiB
TypeScript
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;
|
|
}
|