/** * 纯权限 reducer(契约 P6 / D11 / D8)。 * * 设计约束(Metis 评审):本文件是纯函数层 —— 输入是"已解析好的" grant、祖先链 * 与用户组集合,不碰 DB / 网络。数据获取在 treeService。这样权限代数可以脱离 * 存储做密集单测与随机化不变量测试。 */ import type { FileLibRole } from "./model.js"; import { ROLE_RANK } from "./model.js"; export interface FileLibGrantFact { readonly nodeId: string; readonly principalType: "USER" | "GROUP"; readonly principalId: string; readonly role: FileLibRole; readonly isCreatorGrant: boolean; } export interface EffectiveRoleInput { /** 目标节点(self)。 */ readonly nodeId: string; readonly nodeKind: "FOLDER" | "PROJECT"; /** 目标的全部祖先 id(不含 self,顺序无关)。 */ readonly ancestorIds: readonly string[]; /** 项目独立权限开关(D11/P5);文件夹忽略此值。 */ readonly independentPermissionsEnabled: boolean; readonly userId: string; /** C2 resolve 结果:用户直接所属 + 全部祖先 group 的 id 集合。 */ readonly groupIds: readonly string[]; /** self ∪ ancestors 上的全部活跃 grant(revokedAt 已由获取层过滤)。 */ readonly grants: readonly FileLibGrantFact[]; } /** * 契约 P6:effective(user, R) = max { grant.role | s ∈ {user} ∪ groups*(user), * r ∈ {R} ∪ ancestors(R) };无匹配 → null(无任何权限)。 * "个人权限不能降权"在 max 语义下天然成立 —— 只取最高,不做减法。 * * D11:目标为 PROJECT 且独立权限关闭时,项目级(挂在 self 上)非创建者 grant * 冻结不参与计算;创建者的自动 grant(isCreatorGrant)始终生效。祖先链上的 * grant 不受开关影响。 */ export function effectiveRole(input: EffectiveRoleInput): FileLibRole | null { const onChain = new Set([input.nodeId, ...input.ancestorIds]); const groups = new Set(input.groupIds); const freezeProjectGrants = input.nodeKind === "PROJECT" && !input.independentPermissionsEnabled; let best: FileLibRole | null = null; for (const grant of input.grants) { if (!onChain.has(grant.nodeId)) continue; if (freezeProjectGrants && grant.nodeId === input.nodeId && !grant.isCreatorGrant) continue; if (grant.principalType === "USER" && grant.principalId !== input.userId) continue; if (grant.principalType === "GROUP" && !groups.has(grant.principalId)) continue; if (best === null || ROLE_RANK[grant.role] > ROLE_RANK[best]) best = grant.role; } return best; } /** * D8 可见性语义: * - 完全无权限(effective === null)→ "not_found"(路由层映射 404,不泄露存在性); * - 有权限但不足 → "forbidden"(路由层映射 403)。 */ export type AccessVerdict = | { readonly allowed: true; readonly role: FileLibRole } | { readonly allowed: false; readonly reason: "not_found" | "forbidden" }; export function checkAccess(effective: FileLibRole | null, min: FileLibRole): AccessVerdict { if (effective === null) return { allowed: false, reason: "not_found" }; if (ROLE_RANK[effective] < ROLE_RANK[min]) return { allowed: false, reason: "forbidden" }; return { allowed: true, role: effective }; }