fix: 用专用权限收窄经理 AP 访问(09-07-manager-ap-access-scope)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-08 07:21:37 +08:00
父节点 99167127b5
当前提交 58dcee08f7
共修改 18 个文件,包含 410 行新增和 8 行删除
+3
查看文件
@@ -68,6 +68,7 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"payment.batch.review",
],
ceo: [
"finance.ledger.view",
...EMPLOYEE_BASE_PERMISSIONS,
...PROCUREMENT_SELF_SERVICE_PERMISSIONS,
...MASTERDATA_VIEW_PERMISSIONS,
@@ -86,6 +87,7 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"payment.batch.review",
],
finance: [
"finance.ledger.view",
...EMPLOYEE_BASE_PERMISSIONS,
...MASTERDATA_VIEW_PERMISSIONS,
"invoice.manage",
@@ -106,6 +108,7 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
"payroll.manage",
],
finance_mgr: [
"finance.ledger.view",
...EMPLOYEE_BASE_PERMISSIONS,
...MASTERDATA_MANAGE_PERMISSIONS,
"invoice.manage",
@@ -38,7 +38,7 @@ export const NAV_ITEMS: NavItem[] = [
{ to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
{ to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] },
{ to: "/payment-batches", label: "付款", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage", "payment.batch.review"] },
{ to: "/ap-operations", label: "应付账款", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage", "payment.batch.review"] },
{ to: "/ap-operations", label: "应付账款", icon: "account_balance_wallet", anyOf: ["finance.ledger.view"] },
{ to: "/invoice-pool", label: "票据", icon: "receipt_long", anyOf: ["invoice.manage"] },
{ to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] },
{ to: "/dashboard", label: "数据看板", icon: "dashboard", anyOf: ["report.dashboard.view"] },
+1 -1
查看文件
@@ -315,7 +315,7 @@ const routeElements = (
<Route
path="ap-operations"
element={
<RequirePermission anyOf={["payment.batch.manage", "risk.review.manage", "payment.batch.review"]}>
<RequirePermission anyOf={["finance.ledger.view"]}>
{withRouteFallback(<ApOperationsPage />)}
</RequirePermission>
}
@@ -346,19 +346,40 @@ async function login(page: Page, actor: SyntheticActor) {
await expect(page).toHaveURL(/\/(?:portal|mobile)$/);
}
test("只有付款复核职责的经理可看应付账款但看不到导出", async ({ page }) => {
test("只有付款复核职责的经理不能直连应付账款且不请求台账", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
await mockSyntheticApis(page, REVIEWER_ACTOR);
await login(page, REVIEWER_ACTOR);
let apRequests = 0;
page.on("request", (request) => {
if (new URL(request.url()).pathname.startsWith("/api/ap-operations")) apRequests += 1;
});
await page.goto("/ap-operations");
await expect(page).toHaveURL(/\/ap-operations$/);
await expect(page.getByRole("heading", { level: 1, name: "应付账款", exact: true })).toBeVisible();
await expect(page).toHaveURL(/\/403$/);
await expect(page.getByRole("link", { name: "应付账款", exact: true })).toHaveCount(0);
expect(apRequests).toBe(0);
await expect(page.getByTestId("ap-export")).toHaveCount(0);
expectNoPageErrors(pageErrors);
});
for (const roleCodes of [["ceo"], ["finance"], ["finance_mgr"], ["manager", "finance"]]) {
test(`AP 专用读取权限允许 ${roleCodes.join("+")} 进入台账且不提升导出权限`, async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const actor = { ...FINANCE_ACTOR, roleCodes };
await mockSyntheticApis(page, actor);
await login(page, actor);
await page.goto("/ap-operations");
await expect(page).toHaveURL(/\/ap-operations$/);
await expect(page.getByRole("heading", { level: 1, name: "应付账款", exact: true })).toBeVisible();
await page.getByRole("tab", { name: /^财务台账/ }).click();
await expect(page.getByTestId("finance-ledger-table")).toBeVisible();
if (roleCodes.includes("ceo")) await expect(page.getByTestId("ap-export")).toHaveCount(0);
else await expect(page.getByTestId("ap-export")).toBeVisible();
});
}
test("只有付款复核职责的经理看不到新建批次", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);