ow
This commit is contained in:
1 parent
6a8258ac4b
commit
a7b25d49c4
29 files changed
+1960
-649
No files matched your search
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 权限判定内核:纯函数 + 预建索引,不碰 uni / pinia / 请求层
|
||||
*
|
||||
* 语义以服务端为准,逐条对齐 CloudBuilder.Security.Service/Authorization/PermissionContext.cs:
|
||||
* - view / create / amend / delete 各自要求对应的 Indc 为 'Y';
|
||||
* - none 只要求权限名挂过(后端 CanNone 同款,用于"有授权行就允许,具体动作交给接口判");
|
||||
* - 传 dependentId 时只在 PermissionDependents 里找,不回退到 Permissions——回退等于比后端宽;
|
||||
* - 不做 '*' 通配、不做大小写宽松匹配:后端是精确字符串比较,前端放宽只会带来
|
||||
* "前端放行、后端 403" 的错觉,那正是本层要消除的东西。
|
||||
*
|
||||
* 与旧 WebVue 的一处行为差异(有意的修复):
|
||||
* 旧 hooks/PermissionContext.ts 的 canView 只判断权限名存不存在,漏检 ViewIndc。
|
||||
* 于是 view_indc='N'、create_indc='Y' 的授权行会让用户进得来页面,
|
||||
* 但页面的列表接口走 Find -> 后端 CanView -> 403,表现为"打开就一片红"。
|
||||
* 这里按后端补齐后,进不来就直接跳 403 页,原因写在页面上。
|
||||
*
|
||||
* 之所以做成纯内核:判定逻辑要能被脚本直接跑起来验证(不依赖设备、不依赖后端),
|
||||
* 也方便日后单测;pinia/uni 的取值与响应式都留在 permission-context.ts 那层。
|
||||
*/
|
||||
import { YesNoPolicy } from "@/data/custom/yesNoPolicy";
|
||||
|
||||
/** 四种可判定的动作,none 表示"不细分动作,只要求有授权行" */
|
||||
export type PermissionAction = "none" | "view" | "create" | "amend" | "delete";
|
||||
|
||||
/**
|
||||
* 权限名入参:单个或数组(数组为 OR,任一命中即通过)。
|
||||
* 放在内核里是因为 routes.ts 的 PAGE_PERMISSIONS 也要用同一份定义,
|
||||
* 而内核不 import 任何模块,不会形成 routes <-> permission 的循环依赖。
|
||||
*/
|
||||
export type PermissionNames = string | string[];
|
||||
|
||||
/**
|
||||
* 权限行的结构快照:只声明内核要用的字段,不 import 生成的实体类。
|
||||
* 生成器(data/auto/*)改字段名时不该牵连判定逻辑,只要后端返回的 JSON 形状不变就照样工作。
|
||||
*/
|
||||
export interface PermissionRowLike {
|
||||
PermissionName: string;
|
||||
ViewIndc?: string;
|
||||
CreateIndc?: string;
|
||||
AmendIndc?: string;
|
||||
DeleteIndc?: string;
|
||||
}
|
||||
|
||||
/** 数据级(dependent)权限行:比功能级多一个 DependentDetailName,如"某个家族/某个机构" */
|
||||
export interface DependentRowLike extends PermissionRowLike {
|
||||
DependentDetailName: string;
|
||||
}
|
||||
|
||||
/** 内核需要的登录档案权限信息子集,与 SecurityUserPermissionInfo 结构兼容 */
|
||||
export interface PermissionInfoLike {
|
||||
// 功能级授权行;DependentRowLike 是其超集,故生成实体可直接传入
|
||||
Permissions?: PermissionRowLike[] | null;
|
||||
// 数据级授权行
|
||||
PermissionDependents?: DependentRowLike[] | null;
|
||||
}
|
||||
|
||||
/** 某个权限名上四种动作的合并结果(多组授权会 OR) */
|
||||
export interface ActionFlags {
|
||||
none: boolean;
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
amend: boolean;
|
||||
delete: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预建索引:把旧实现"每次判定都 Array.some 线性扫一遍"换成一次构建、多次 O(1) 查询。
|
||||
* 旧版一个页面要调 canView/canCreate/canAmend/canDelete 四次,扫四遍;
|
||||
* 相册页权限行可能上百,加上小程序 setData 频率敏感,索引这层不亏。
|
||||
*/
|
||||
export interface PermissionIndex {
|
||||
/** key = PermissionName */
|
||||
byName: Map<string, ActionFlags>;
|
||||
/** key = dependentKey(permission, dependent) */
|
||||
byDependent: Map<string, ActionFlags>;
|
||||
}
|
||||
|
||||
/** 空索引:登录档案缺失时用,配合 context 层的"清单为空即放行"策略 */
|
||||
const EMPTY_INDEX: PermissionIndex = {
|
||||
byName: new Map(),
|
||||
byDependent: new Map(),
|
||||
};
|
||||
|
||||
/** 数据级权限的复合键;用 \u0000 分隔,权限名与明细名都不可能出现控制字符 */
|
||||
export function dependentKey(permission: string, dependent: string): string {
|
||||
return `${permission}\u0000${dependent}`;
|
||||
}
|
||||
|
||||
function emptyFlags(): ActionFlags {
|
||||
return {
|
||||
none: false,
|
||||
view: false,
|
||||
create: false,
|
||||
amend: false,
|
||||
delete: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isYes(value?: string): boolean {
|
||||
return value === YesNoPolicy.YES;
|
||||
}
|
||||
|
||||
/** 同一权限名可能被多个用户组授予,合并成 OR:任一组给了某个动作就算有 */
|
||||
function merge(
|
||||
target: Map<string, ActionFlags>,
|
||||
key: string,
|
||||
row: PermissionRowLike,
|
||||
): void {
|
||||
let flags = target.get(key);
|
||||
if (!flags) {
|
||||
flags = emptyFlags();
|
||||
target.set(key, flags);
|
||||
}
|
||||
flags.none = true;
|
||||
flags.view ||= isYes(row.ViewIndc);
|
||||
flags.create ||= isYes(row.CreateIndc);
|
||||
flags.amend ||= isYes(row.AmendIndc);
|
||||
flags.delete ||= isYes(row.DeleteIndc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建索引。传 null/undefined/空清单会得到空索引,不抛异常——
|
||||
* 登录档案是从本地缓存水合的,损坏或半截是正常状态,判定层不该让页面崩。
|
||||
*/
|
||||
export function buildPermissionIndex(
|
||||
info?: PermissionInfoLike | null,
|
||||
): PermissionIndex {
|
||||
if (!info) {
|
||||
return EMPTY_INDEX;
|
||||
}
|
||||
const byName = new Map<string, ActionFlags>();
|
||||
const byDependent = new Map<string, ActionFlags>();
|
||||
for (const row of info.Permissions ?? []) {
|
||||
if (row?.PermissionName) {
|
||||
merge(byName, row.PermissionName, row);
|
||||
}
|
||||
}
|
||||
for (const row of info.PermissionDependents ?? []) {
|
||||
if (row?.PermissionName && row.DependentDetailName) {
|
||||
merge(
|
||||
byDependent,
|
||||
dependentKey(row.PermissionName, row.DependentDetailName),
|
||||
row,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { byName, byDependent };
|
||||
}
|
||||
|
||||
function lookup(
|
||||
index: PermissionIndex,
|
||||
permission: string,
|
||||
dependent?: string,
|
||||
): ActionFlags | undefined {
|
||||
if (dependent) {
|
||||
return index.byDependent.get(dependentKey(permission, dependent));
|
||||
}
|
||||
return index.byName.get(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条判定。
|
||||
* @param index 预建索引
|
||||
* @param action 要判定的动作
|
||||
* @param permission 权限名,须与后端 security_function.permission_name 逐字一致
|
||||
* @param dependent 数据级授权明细名;给了它就只查 dependent 集合,与后端一致
|
||||
*/
|
||||
export function can(
|
||||
index: PermissionIndex,
|
||||
action: PermissionAction,
|
||||
permission: string,
|
||||
dependent?: string,
|
||||
): boolean {
|
||||
const flags = lookup(index, permission, dependent);
|
||||
if (!flags) {
|
||||
return false;
|
||||
}
|
||||
return action === "none" ? flags.none : flags[action];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量判定:任一权限名通过即通过(OR)。
|
||||
* 一个页面被多个功能权限共享时用得上,例如相册浏览页同时挂在
|
||||
* PublishPhotoEnquiry 与 FamilyGroupShare 之下。
|
||||
*/
|
||||
export function canAny(
|
||||
index: PermissionIndex,
|
||||
action: PermissionAction,
|
||||
permissions: PermissionNames,
|
||||
dependent?: string,
|
||||
): boolean {
|
||||
const list = Array.isArray(permissions) ? permissions : [permissions];
|
||||
// 刻意不写成 list.some(name => ...):单参数箭头的括号,prettier 要加、
|
||||
// ESLint 要去,改回去就来回拉锯;for-of 在两个工具下都稳定
|
||||
for (const name of list) {
|
||||
if (name && can(index, action, name, dependent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端 [PermissionOperate(name, Operate.X)] 的枚举值,逐字照抄
|
||||
* CloudBuilder.Core/Authorization/PermissionOperateAttribute.cs 的 Operate。
|
||||
* 用 as const 数组而不是多行字面量联合:联合类型的换行位置 prettier 与
|
||||
* operator-linebreak 各执一词,只有这种写法在两个工具下都不被改写
|
||||
*/
|
||||
export const PERMISSION_OPERATES = [
|
||||
"None",
|
||||
"Find",
|
||||
"Print",
|
||||
"Insert",
|
||||
"Update",
|
||||
"Delete",
|
||||
"Submit",
|
||||
"Approval",
|
||||
] as const;
|
||||
|
||||
/** 后端 Operate 枚举的字符串取值 */
|
||||
export type PermissionOperate = (typeof PERMISSION_OPERATES)[number];
|
||||
|
||||
/**
|
||||
* 按后端的鉴权口径判一个操作:与 PermissionHandler.HandleRequirementAsync 的 switch 一一对应。
|
||||
* Find/Print -> view,Insert -> create,Update -> amend,Delete -> delete;
|
||||
* 其余(Submit/Approval)走 default 分支——后端把 operate 字符串当成 DependentDetailName
|
||||
* 去数据级授权里查"有没有挂过这个操作",所以这里同样是 none + dependent。
|
||||
*
|
||||
* 用意:前端按钮的可用性判定和接口能不能调,用的是同一份映射,不会出现
|
||||
* "提交按钮亮着、请求过去被 403"。新增操作类型时只改这一处。
|
||||
*/
|
||||
export function canOperate(
|
||||
index: PermissionIndex,
|
||||
operate: PermissionOperate,
|
||||
permission: string,
|
||||
): boolean {
|
||||
switch (operate) {
|
||||
case "None": {
|
||||
return can(index, "none", permission);
|
||||
}
|
||||
case "Find":
|
||||
case "Print": {
|
||||
return can(index, "view", permission);
|
||||
}
|
||||
case "Insert": {
|
||||
return can(index, "create", permission);
|
||||
}
|
||||
case "Update": {
|
||||
return can(index, "amend", permission);
|
||||
}
|
||||
case "Delete": {
|
||||
return can(index, "delete", permission);
|
||||
}
|
||||
default: {
|
||||
return can(index, "none", permission, operate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 取某权限名(或某条数据级授权)的全部动作,供 UI 一次性渲染按钮可用性 */
|
||||
export function actionsOf(
|
||||
index: PermissionIndex,
|
||||
permission: string,
|
||||
dependent?: string,
|
||||
): ActionFlags {
|
||||
return lookup(index, permission, dependent) ?? emptyFlags();
|
||||
}
|
||||
|
||||
/** 已授权的动作清单,调试面板与"我到底有什么权限"排查用 */
|
||||
export function grantedPermissions(
|
||||
index: PermissionIndex,
|
||||
action: PermissionAction,
|
||||
): string[] {
|
||||
const names: string[] = [];
|
||||
index.byName.forEach((flags, name) => {
|
||||
if (action === "none" ? flags.none : flags[action]) {
|
||||
names.push(name);
|
||||
}
|
||||
});
|
||||
return names;
|
||||
}
|
||||
Reference in new issue
Block a user