PA6 补齐权限与审计回归
这个提交包含在:
@@ -2645,6 +2645,18 @@ async function reimbursementRoutes(fastify) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: request.employee.id,
|
||||
action: "REIMBURSEMENT_ATTACHMENT_DOWNLOADED",
|
||||
targetType: "reimbursement",
|
||||
targetId: reimbursementId,
|
||||
extra: {
|
||||
attachmentId: attachment.id,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
reply.type(attachment.mime_type || "application/octet-stream");
|
||||
reply.header(
|
||||
"content-disposition",
|
||||
|
||||
@@ -5,6 +5,16 @@ const WORKSPACE_PROFILES = Object.freeze({
|
||||
});
|
||||
|
||||
const MODULE_VISIBILITY_MODES = Object.freeze(["main", "admin", "hidden"]);
|
||||
const BUSINESS_APPROVE_PERMISSIONS = Object.freeze([
|
||||
"expense.approve",
|
||||
"leave.approve",
|
||||
"procurement.approve",
|
||||
"travel.approve",
|
||||
]);
|
||||
const BUSINESS_APPROVER_ROLE_CODES = new Set(["manager", "ceo", "finance", "finance_mgr", "hr"]);
|
||||
const FINANCE_ROLE_CODES = new Set(["finance", "finance_mgr"]);
|
||||
const FINANCE_PERMISSION_CODES = Object.freeze(["payment.batch.manage", "risk.review.manage", "expense.finance.pay"]);
|
||||
const FINANCE_NAVIGATION_MODULES = new Set(["paymentBatches", "apOperations", "invoicePool"]);
|
||||
|
||||
const NAVIGATION_REGISTRY = Object.freeze({
|
||||
portal: { key: "portal", label: "Portal", icon: "home_app_logo", href: "/portal", anyOf: [] },
|
||||
@@ -23,7 +33,7 @@ const NAVIGATION_REGISTRY = Object.freeze({
|
||||
organization: { key: "organization", label: "组织架构", icon: "account_tree", href: "/organization", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
|
||||
contractsRegistry: { key: "contractsRegistry", label: "合同台账", icon: "description", href: "/contracts-registry", anyOf: ["project.manage", "project.view_all", "system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.projectLinking },
|
||||
workflowDefinitions: { key: "workflowDefinitions", label: "流程定义", icon: "schema", href: "/system/workflow-definitions", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.workflowDefinitionAdmin },
|
||||
systemSettings: { key: "systemSettings", label: "系统设置", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.view", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
|
||||
systemSettings: { key: "systemSettings", label: "系统设置", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
|
||||
featureFlags: { key: "featureFlags", label: "功能开关", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
|
||||
auditLogs: { key: "auditLogs", label: "审计日志", icon: "manage_search", href: "/audit-logs", anyOf: ["system.audit.view"] },
|
||||
|
||||
@@ -176,6 +186,22 @@ function hasAnyPermission(hasPermission, employee, anyOf = []) {
|
||||
return anyOf.some((permissionCode) => hasPermission(employee, permissionCode));
|
||||
}
|
||||
|
||||
function hasBusinessApproverRole(hasPermission, employee) {
|
||||
const roleCodes = employee?.roleCodes ?? [];
|
||||
return (
|
||||
roleCodes.some((roleCode) => BUSINESS_APPROVER_ROLE_CODES.has(roleCode)) &&
|
||||
hasAnyPermission(hasPermission, employee, BUSINESS_APPROVE_PERMISSIONS)
|
||||
);
|
||||
}
|
||||
|
||||
function hasFinanceRole(hasPermission, employee) {
|
||||
const roleCodes = employee?.roleCodes ?? [];
|
||||
return (
|
||||
roleCodes.some((roleCode) => FINANCE_ROLE_CODES.has(roleCode)) &&
|
||||
hasAnyPermission(hasPermission, employee, FINANCE_PERMISSION_CODES)
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveModuleGate({ prisma, config, employee, moduleKey, registryItem }) {
|
||||
if (!registryItem?.featureFlagKey) {
|
||||
return { enabled: true, reason: "no_feature_gate" };
|
||||
@@ -204,6 +230,14 @@ async function getSystemNavigation({ prisma, config, employee, hasPermission })
|
||||
continue;
|
||||
}
|
||||
|
||||
if (moduleKey === "workflowTasks" && !hasBusinessApproverRole(hasPermission, employee)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FINANCE_NAVIGATION_MODULES.has(moduleKey) && !hasFinanceRole(hasPermission, employee)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gate = await resolveModuleGate({ prisma, config, employee, moduleKey, registryItem });
|
||||
if (!gate.enabled) {
|
||||
continue;
|
||||
|
||||
@@ -81,6 +81,7 @@ describe("workspace small-team navigation defaults", () => {
|
||||
const employee = {
|
||||
id: "employee-1",
|
||||
department_id: "dept-1",
|
||||
roleCodes: ["employee"],
|
||||
permissionCodes: resolvePermissionsForRoles(["employee"]),
|
||||
};
|
||||
|
||||
@@ -102,4 +103,29 @@ describe("workspace small-team navigation defaults", () => {
|
||||
expect(backupKeys).not.toContain("erpIntegrations");
|
||||
expect(backupKeys).not.toContain("dashboard");
|
||||
});
|
||||
|
||||
it("does not expose workflow task entry to admin-only navigation", async () => {
|
||||
const adminOnly = {
|
||||
id: "admin-1",
|
||||
department_id: "dept-1",
|
||||
roleCodes: ["admin"],
|
||||
permissionCodes: resolvePermissionsForRoles(["admin"]),
|
||||
};
|
||||
|
||||
const navigation = await getSystemNavigation({
|
||||
prisma: buildPrisma(),
|
||||
config: { ...buildConfig(), workflowDefinitionAdminEnabled: true },
|
||||
employee: adminOnly,
|
||||
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
||||
});
|
||||
|
||||
const allKeys = [
|
||||
...navigation.sections.main,
|
||||
...navigation.sections.admin,
|
||||
...navigation.sections.backup,
|
||||
].map((item) => item.key);
|
||||
|
||||
expect(allKeys).toContain("auditLogs");
|
||||
expect(allKeys).not.toContain("workflowTasks");
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户