CASL 同构权限系统:当你的 AI 也需要访问控制——15×26 权限矩阵的工程实践

> 前端菜单是否显示某按钮、后端 API 是否允许访问、AI Tool Registry 是否暴露某操作——所有这些依赖同一套 CASL 能力定义。EAMX 的权限系统用 132 行 ability.ts 实现了前后端共享的 AppAbility,通过 buildAbilityFromPermissions + permissionConditions 做到了三维数据范围(all/department/self)控制。本文详解 ability.ts 的设计和 CASL 条件权限的实现。

1. 问题:多端权限的一致性

传统系统权限的实现分散在三个地方:

三个地方的判断逻辑各自独立,容易出现"前端隐藏了按钮但 API 没保护"或"API 返回 403 但 AI 仍建议该操作"的安全漏洞。

EAMX 的解法:全部依赖 packages/core/src/ability.ts 导出的同一套 AppAbility

2. Actions 和 Subjects 定义

```typescript<br>// ability.ts — 15 个 Actions<br>type Actions =<br> | "read" | "create" | "update" | "delete" // CRUD<br> | "export" | "manage" // 通用<br> | "grab" | "dispatch" | "start" // 工单生命周期<br> | "complete" | "accept" | "close" // 完工/验收/关闭<br> | "assign" | "approve" | "use"; // 指派/审批/AI

// 26 个 Subjects<br>type Subjects =<br> | "WorkOrder" | "FaultReport" | "Equipment"<br> | "SparePart" | "Inventory" | "PurchaseRequest"<br> | "TechStandard" | "PmPlan" | "AiChat"<br> | "Department" | "User" | "Role" | "Tenant"<br> // ... 共 26 个<br>```

Action 不是简单的 CRUD——包含工单的生命周期动作(grab/start/complete/accept/close),这让权限粒度足够细:一个角色可以"查看所有工单"但不能"抢单"。

3. 权限字符串格式

``<br>"manage:equipment" → can("manage", "Equipment")<br>"read:workorder" → can("read", "WorkOrder")<br>"use:aichat" → can("use", "AiChat")<br>"grab:workorder" → can("grab", "WorkOrder") // 抢单权限<br>``

resolvePermission 通过 SUBJECT_KEY_MAP 将小写 subject → PascalCase Subject 类型:

```typescript<br>const SUBJECT_KEY_MAP: Record<string, Subjects> = {<br> "workorder": "WorkOrder",<br> "equipment": "Equipment",<br> // ...<br>};

function resolvePermission(code: string): [Actions, Subjects] | null {<br> const [action, subjectKey] = code.split(":");<br> return SUBJECT_KEY_MAP[subjectKey] ? [action, subject] : null;<br>}<br>```

4. 能力构建:从权限列表到 AppAbility

```typescript<br>export function buildAbilityFromPermissions(<br> permissions: string[],<br> options?: {<br> isAdmin?: boolean;<br> permissionConditions?: Record<string, Record<string, any>>;<br> },<br>): AppAbility {<br> const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);

// 平台超管直接拥有所有权限<br> if (options?.isAdmin) {<br> can("manage", "all");<br> return build();<br> }

for (const code of permissions) {<br> const [action, subject] = resolvePermission(code);<br> const conditions = options?.permissionConditions?.[code];<br> if (conditions) {<br> can(action, subject, conditions); // 带条件的权限<br> } else {<br> can(action, subject); // 无条件权限<br> }<br> }<br> return build();<br>}<br>```

5. 条件权限:三维数据范围

```typescript<br>// 部门管理员只能看自己部门的工单<br>can("read", "WorkOrder", { departmentId: user.departmentId });

// 普通维修工只能看自己的工单<br>can("read", "WorkOrder", { responsibleUserId: user.id });<br>```

这依赖 @casl/ability 的 MongoDB 风格条件查询:

```typescript<br>// 前端检查<br>ability.can("read", "WorkOrder")<br>// → 带条件的 can 在纯前端返回 false(因为没有 DB 上下文)

// 后端 Drizzle 注入条件<br>const rules = ability.rulesFor("read", "WorkOrder");<br>// → [{ action: "read", subject: "WorkOrder", conditions: { departmentId: "dept-1" } }]<br>// → SQL: WHERE department_id = 'dept-1'<br>```

三维数据范围:

6. 前后端 + AI 三层消费

| 层级 | 消费方式 | 实现位置 |<br>|---|---|---|<br>| 前端 | ability.can(action, subject) → 控制按钮/菜单显隐 | 共享 AppAbility 实例 |<br>| 后端 API | 中间件 ability.can(action, subject) + Drizzle 条件注入 | API Server |<br>| AI Tool Registry | toolRegistry.getAuthorized(ability) 过滤 Tool 列表 | Tool Factory |

``typescript<br>// AI Tool Registry 消费<br>getAuthorized(ability: AppAbility): McpTool[] {<br> return this.tools.filter(t => {<br> const casl = t.annotations?.casl;<br> if (!casl) return true; // 无 CASL 注解的 Tool 对所有人开放<br> return ability.can(casl.action, casl.subject);<br> });<br>}<br>``

7. 核心文件索引

| 文件 | 行数 | 职责 |<br>|---|---|---|<br>| packages/core/src/ability.ts | 132 | CASL 类型 + buildAbilityFromPermissions |<br>| packages/core/src/types.ts | — | PluginManifest、NavItem、PermissionDefinition |<br>| 各个插件的 permissions 字段 | — | 插件自有权限定义 |

下一篇:审批工作流引擎——LifecycleBus 的拦截模式如何实现零侵入审批流。

作者:白杨,十余年设备资产管理从业经验,EAMX 产品负责人。