forked from wangziqi/gongxue-base
feat: add CASL authorization and AI configuration
This commit is contained in:
180
apps/server/src/authorization/casl.constants.ts
Normal file
180
apps/server/src/authorization/casl.constants.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user