This commit is contained in:
owenchen committed 2026-10-13 10:47:39 +08:00
1 parent 6a8258ac4b
commit a7b25d49c4
29 files changed
+1960 -649

No files matched your search

@@ -0,0 +1,396 @@
/**
* 权限内核的可执行校验(不引测试框架,纯 TS + 断言函数)
*
* 跑法(PowerShell,在项目根目录):
* & "C:\Program Files\nodejs\node.exe" ".\node_modules\esbuild\bin\esbuild" `
* src\permission\permission-kernel.spec.ts --bundle --platform=node --format=cjs `
* --alias:@=./src --outfile="$env:TEMP\permission-kernel.spec.cjs" --log-level=warning
* & "C:\Program Files\nodejs\node.exe" "$env:TEMP\permission-kernel.spec.cjs"
*
* 为什么要它:判定语义的正确性只能靠"跑一遍"来钉住,尤其 canView 是否检 ViewIndc
* 这条——它是本次移植相对旧 WebVue 唯一的行为变更,靠读代码很容易自我说服"应该没问题"。
* 项目目前还没接测试运行器,所以先写成"能被 esbuild 打包直接用 node 跑"的形态:
* 将来引入 vitest,只需把 check() 换成 expect(),用例本身不用动。
*
* 约定:所有用例都对照后端 CloudBuilder.Security.Service/Authorization/PermissionContext.cs
* 的既有行为写,断言的是"前端与后端一致",不是"前端自己的偏好"。
*/
import type {
DependentRowLike,
PermissionInfoLike,
PermissionRowLike,
} from "./permission-kernel";
import {
actionsOf,
buildPermissionIndex,
can,
canAny,
canOperate,
grantedPermissions,
} from "./permission-kernel";
let failed = 0;
let total = 0;
/** 断言:失败不中断,好让一次跑完能看清全部问题 */
function check(label: string, actual: unknown, expected: unknown): void {
total++;
if (actual !== expected) {
failed++;
// 本项目的 no-console 只放行 warn/error(页面代码里不该打印),
// 这是个只在 node 下跑的校验脚本,就沿用放行的那一个
console.warn(
` FAIL ${label} => expected ${String(expected)}, got ${String(actual)}`,
);
}
}
function row(
permission: string,
flags: Partial<Record<"amend" | "create" | "delete" | "view", string>>,
): PermissionRowLike {
return {
PermissionName: permission,
ViewIndc: flags.view,
CreateIndc: flags.create,
AmendIndc: flags.amend,
DeleteIndc: flags.delete,
};
}
function depRow(
permission: string,
dependent: string,
flags: Partial<Record<"amend" | "create" | "delete" | "view", string>>,
): DependentRowLike {
return { ...row(permission, flags), DependentDetailName: dependent };
}
function info(
permissions: PermissionRowLike[] = [],
dependents: DependentRowLike[] = [],
): PermissionInfoLike {
return { Permissions: permissions, PermissionDependents: dependents };
}
// ---------------------------------------------------------------- 1. view 必须检 ViewIndc
{
// view_indc='N' 而 create_indc='Y':后端 CanView 为 false。
// 旧前端只看"权限名在不在",会误放行,页面进来后列表接口一片 403。
const index = buildPermissionIndex(
info([row("ActorEnquiry", { view: "N", create: "Y" })]),
);
check(
"view=N 时 canView 为 false(修复旧前端漏检)",
can(index, "view", "ActorEnquiry"),
false,
);
check(
"view=N 时 canNone 仍为 true(只要求挂过权限)",
can(index, "none", "ActorEnquiry"),
true,
);
check(
"create=Y 时 canCreate 为 true",
can(index, "create", "ActorEnquiry"),
true,
);
}
// ---------------------------------------------------------------- 2. 四种动作各自独立
{
const index = buildPermissionIndex(
info([row("DmsFileEnquiry", { view: "Y" })]),
);
check("只给 view:view 通过", can(index, "view", "DmsFileEnquiry"), true);
check(
"只给 view:create 不通过",
can(index, "create", "DmsFileEnquiry"),
false,
);
check(
"只给 view:amend 不通过",
can(index, "amend", "DmsFileEnquiry"),
false,
);
check(
"只给 view:delete 不通过",
can(index, "delete", "DmsFileEnquiry"),
false,
);
const full = buildPermissionIndex(
info([
row("DmsFileEnquiry", {
view: "Y",
create: "Y",
amend: "Y",
delete: "Y",
}),
]),
);
check("四标全 Y:delete 通过", can(full, "delete", "DmsFileEnquiry"), true);
}
// ---------------------------------------------------------------- 3. 多用户组授权做 OR 合并
{
// 同一权限名由两个用户组分别授予 view 与 delete,合并后两者都应有
const index = buildPermissionIndex(
info([
row("FamilyGroupEnquiry", { view: "Y" }),
row("FamilyGroupEnquiry", { delete: "Y" }),
]),
);
check("多组合并:view 命中", can(index, "view", "FamilyGroupEnquiry"), true);
check(
"多组合并:delete 命中",
can(index, "delete", "FamilyGroupEnquiry"),
true,
);
check(
"多组合并:amend 仍未命中",
can(index, "amend", "FamilyGroupEnquiry"),
false,
);
const flags = actionsOf(index, "FamilyGroupEnquiry");
check(
"actionsOf 一次取全",
`${flags.view}/${flags.create}/${flags.amend}/${flags.delete}`,
"true/false/false/true",
);
}
// ---------------------------------------------------------------- 4. 数据级只查 dependents,不回退
{
const index = buildPermissionIndex(
info(
[row("PublishPhotoEnquiry", { view: "Y" })],
[depRow("PublishPhotoEnquiry", "F1001", { view: "N" })],
),
);
check("功能级命中", can(index, "view", "PublishPhotoEnquiry"), true);
// 后端 CanView(permission, dependent) 只查 PermissionDependents;
// 前端若"数据级没查到就退回功能级",就比后端宽,会出现前端放行后端 403
check(
"数据级 N:不回退到功能级",
can(index, "view", "PublishPhotoEnquiry", "F1001"),
false,
);
check(
"数据级整条不存在:false",
can(index, "view", "PublishPhotoEnquiry", "F9999"),
false,
);
check(
"数据级 Y:命中",
can(
buildPermissionIndex(
info([], [depRow("PublishPhotoEnquiry", "F1", { view: "Y" })]),
),
"view",
"PublishPhotoEnquiry",
"F1",
),
true,
);
}
// ---------------------------------------------------------------- 5. 严格比较(与后端一致)
{
const index = buildPermissionIndex(
info([
row("SecurityGroupEnquiry", { view: "y" }),
row(" security ", { view: "Y" }),
]),
);
check(
"小写 y 不算 YES(后端按 'Y' 精确比较)",
can(index, "view", "SecurityGroupEnquiry"),
false,
);
check("权限名区分大小写", can(index, "view", "securitygroupenquiry"), false);
check("权限名不做 trim", can(index, "view", "security"), false);
check("原样存在的名字可命中", can(index, "view", " security "), true);
}
// ---------------------------------------------------------------- 6. 档案缺失或损坏
{
const broken: PermissionInfoLike[] = [
{},
{ Permissions: null },
{ Permissions: undefined, PermissionDependents: undefined },
{ Permissions: [] },
{ Permissions: [null as unknown as PermissionRowLike] },
{ Permissions: [{ PermissionName: "" } as PermissionRowLike] },
{
PermissionDependents: [
{ PermissionName: "A", DependentDetailName: "" } as DependentRowLike,
],
},
];
broken.forEach((item, i) => {
const index = buildPermissionIndex(item);
check(
`损坏档案[${i}]:不抛异常且判定为 false`,
can(index, "view", "A"),
false,
);
check(`损坏档案[${i}]:none 也为 false`, can(index, "none", "A"), false);
});
check(
"传 null 得到空索引",
can(buildPermissionIndex(null), "none", "A"),
false,
);
check(
"不传参得到空索引",
grantedPermissions(buildPermissionIndex(), "none").length,
0,
);
}
// ---------------------------------------------------------------- 7. canAny 批量判定
{
const index = buildPermissionIndex(
info([row("PreviewAudio", { view: "Y" })]),
);
check("数组任一命中", canAny(index, "view", ["Nope", "PreviewAudio"]), true);
check("数组全不命中", canAny(index, "view", ["Nope", "Nothing"]), false);
check("空数组为 false(而非 undefined)", canAny(index, "view", []), false);
check(
"数组里的空串被跳过",
canAny(index, "view", ["", "PreviewAudio"]),
true,
);
check("单串仍可用", canAny(index, "view", "PreviewAudio"), true);
check(
"批量判定带 dependent",
canAny(index, "view", ["PreviewAudio"], "F1"),
false,
);
}
// ---------------------------------------------------------------- 8. 未登记动作与未知权限
{
const index = buildPermissionIndex(
info([row("ActorEnquiry", { view: "Y" })]),
);
check("未授予的权限名:false", can(index, "view", "NeverGranted"), false);
const flags = actionsOf(index, "NeverGranted");
check(
"未授予权限的 actionsOf 返回全 false 而非 undefined",
`${flags.none}/${flags.view}`,
"false/false",
);
}
// ---------------------------------------------------------------- 9. 已授权清单
{
const index = buildPermissionIndex(
info([
row("A_Enquiry", { view: "Y" }),
row("B_Enquiry", { view: "N", create: "Y" }),
row("C_Enquiry", { view: "Y", delete: "Y" }),
]),
);
check(
"none 清单含全部挂过的名字",
grantedPermissions(index, "none").join(","),
"A_Enquiry,B_Enquiry,C_Enquiry",
);
check(
"view 清单只含 Y",
grantedPermissions(index, "view").join(","),
"A_Enquiry,C_Enquiry",
);
check(
"create 清单",
grantedPermissions(index, "create").join(","),
"B_Enquiry",
);
check(
"delete 清单",
grantedPermissions(index, "delete").join(","),
"C_Enquiry",
);
check("amend 清单为空", grantedPermissions(index, "amend").length, 0);
}
// ---------------------------------------------------------------- 10. 操作型判定(对齐后端 PermissionHandler 的 switch)
{
const index = buildPermissionIndex(
info(
[
row("PublishPhotoEnquiry", {
view: "Y",
create: "Y",
amend: "Y",
delete: "Y",
}),
],
// Submit/Approval 这类操作在后端是"数据级授权行",靠 DependentDetailName 命中
[depRow("PublishPhotoEnquiry", "Submit", { view: "N" })],
),
);
check("Find -> view", canOperate(index, "Find", "PublishPhotoEnquiry"), true);
check(
"Print -> view",
canOperate(index, "Print", "PublishPhotoEnquiry"),
true,
);
check(
"Insert -> create",
canOperate(index, "Insert", "PublishPhotoEnquiry"),
true,
);
check(
"Update -> amend",
canOperate(index, "Update", "PublishPhotoEnquiry"),
true,
);
check(
"Delete -> delete",
canOperate(index, "Delete", "PublishPhotoEnquiry"),
true,
);
check(
"None 只要求挂过",
canOperate(index, "None", "PublishPhotoEnquiry"),
true,
);
check(
"Submit 走数据级,命中",
canOperate(index, "Submit", "PublishPhotoEnquiry"),
true,
);
check(
"Approval 未挂 → false(即使四个 Indc 全 Y)",
canOperate(index, "Approval", "PublishPhotoEnquiry"),
false,
);
// view_indc='N' 时 Find 必须为 false:这条正是旧前端会误判成 true 的场景
const noView = buildPermissionIndex(
info([row("ActorEnquiry", { view: "N", create: "Y" })]),
);
check(
"view=N 时 Find 为 false",
canOperate(noView, "Find", "ActorEnquiry"),
false,
);
check(
"view=N 时 Insert 仍为 true",
canOperate(noView, "Insert", "ActorEnquiry"),
true,
);
}
console.warn(
failed === 0
? `permission-kernel: ${total} assertions passed`
: `permission-kernel: ${failed}/${total} assertions FAILED`,
);
if (failed > 0) {
// 让非零退出可被脚本捕获,避免"跑了但没人看输出"
throw new Error("permission-kernel assertions failed");
}