forked from EduCraft/curriculum-project-hub
feat(database): init database folder frontend and permission
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 纯权限 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<string>([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 };
|
||||
}
|
||||
Reference in New Issue
Block a user