/** * 权限内核的可执行校验(不引测试框架,纯 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>, ): PermissionRowLike { return { PermissionName: permission, ViewIndc: flags.view, CreateIndc: flags.create, AmendIndc: flags.amend, DeleteIndc: flags.delete, }; } function depRow( permission: string, dependent: string, flags: Partial>, ): 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"); }