OAPERM1 收紧启用前权限冻结

这个提交包含在:
2026-05-24 00:57:11 +08:00
父节点 a3f13eab57
当前提交 65513a5291
共修改 27 个文件,包含 231 行新增和 149 行删除
+12 -10
查看文件
@@ -51,21 +51,23 @@ describe("Permission Constants", () => {
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true);
});
it("grants travel permissions to configured roles", () => {
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "travel.create")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "travel.submit")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "travel.approve")).toBe(true);
it("freezes travel permissions for non-admin launch roles", () => {
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "travel.create")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "travel.submit")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "travel.approve")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["finance"]), "travel.approve")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "travel.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "travel.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "travel.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "travel.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["admin"]), "travel.manage")).toBe(true);
});
it("grants travel.payment.manage only to configured finance and administration roles", () => {
expect(hasPermission(resolvePermissionsForRoles(["finance"]), "travel.payment.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "travel.payment.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "travel.payment.manage")).toBe(true);
it("freezes travel.payment.manage before travel launch", () => {
expect(hasPermission(resolvePermissionsForRoles(["finance"]), "travel.payment.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "travel.payment.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "travel.payment.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "travel.payment.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "travel.payment.manage")).toBe(false);
expect(hasPermission(resolvePermissionsForRoles(["admin"]), "travel.payment.manage")).toBe(true);
});
it("grants ai.assist only to finance manager and admin", () => {
@@ -143,7 +143,7 @@ async function buildApp({ permissionCodes = ["system.users"], stats } = {}) {
}
describe("employees stats route", () => {
it("returns aggregated employee stats for read-only viewers without salary data", async () => {
it("returns aggregated employee stats for read-only viewers without salary or performance data", async () => {
const {
app,
employeeGroupBy,
@@ -182,27 +182,10 @@ describe("employees stats route", () => {
expiringSoonCount: 4,
});
expect(payload.data.performanceSummary).toEqual({
cycle: {
id: "cycle-active",
name: "2026 Q2",
cycleType: "quarterly",
status: "active",
startDate: "2026-04-01",
endDate: "2026-06-30",
},
averageScore: 4.2,
reviewedCount: 5,
distribution: [
{ score: 1, count: 0 },
{ score: 1.5, count: 0 },
{ score: 2, count: 0 },
{ score: 2.5, count: 0 },
{ score: 3, count: 0 },
{ score: 3.5, count: 0 },
{ score: 4, count: 3 },
{ score: 4.5, count: 2 },
{ score: 5, count: 0 },
],
cycle: null,
averageScore: null,
reviewedCount: 0,
distribution: [],
});
expect(payload.data.salarySummary).toBeNull();
expect(employeeGroupBy).toHaveBeenCalledTimes(2);
@@ -211,17 +194,17 @@ describe("employees stats route", () => {
expect(contractCount).toHaveBeenCalledTimes(1);
expect(certificateGroupBy).toHaveBeenCalledTimes(1);
expect(certificateCount).toHaveBeenCalledTimes(1);
expect(performanceCycleFindFirst).toHaveBeenCalledTimes(2);
expect(performanceReviewAggregate).toHaveBeenCalledTimes(1);
expect(performanceReviewGroupBy).toHaveBeenCalledTimes(1);
expect(performanceCycleFindFirst).not.toHaveBeenCalled();
expect(performanceReviewAggregate).not.toHaveBeenCalled();
expect(performanceReviewGroupBy).not.toHaveBeenCalled();
expect(payslipAggregate).not.toHaveBeenCalled();
await app.close();
});
it("returns salary summary for system.users viewers", async () => {
it("returns salary and performance summary for system settings viewers", async () => {
const { app, payslipAggregate } = await buildApp({
permissionCodes: ["system.users"],
permissionCodes: ["system.users", "system.settings"],
});
const response = await app.inject({
@@ -244,6 +227,10 @@ describe("employees stats route", () => {
issuedCount: 2,
},
});
expect(response.json().data.performanceSummary.cycle).toMatchObject({
id: "cycle-active",
name: "2026 Q2",
});
expect(payslipAggregate).toHaveBeenCalledTimes(2);
await app.close();
@@ -24,7 +24,7 @@ function buildPayslipRow() {
}
async function buildApp({
actor = { id: ADMIN_ID, permissionCodes: ["system.users"] },
actor = { id: ADMIN_ID, permissionCodes: ["system.settings"] },
} = {}) {
const app = Fastify();
const salaryItems = [
@@ -21,7 +21,7 @@ function buildSalaryItem(overrides = {}) {
}
async function buildApp({
actor = { id: ADMIN_ID, permissionCodes: ["system.users"] },
actor = { id: ADMIN_ID, permissionCodes: ["system.settings"] },
} = {}) {
const app = Fastify();
const state = {
@@ -48,7 +48,7 @@ const PAYSLIP_IDS = [
];
async function buildApp({
actor = { id: ADMIN_ID, permissionCodes: ["system.users"] },
actor = { id: ADMIN_ID, permissionCodes: ["system.settings"] },
omitProfileFor = [],
} = {}) {
const app = Fastify();
@@ -50,7 +50,7 @@ function buildSalaryRow(overrides = {}) {
}
async function buildApp({
actor = { id: ADMIN_ID, permissionCodes: ["system.users"] },
actor = { id: ADMIN_ID, permissionCodes: ["system.settings"] },
} = {}) {
const app = Fastify();
const state = {
@@ -41,7 +41,7 @@ function buildCycleRow(overrides = {}) {
};
}
async function buildApp({ actor = { id: ADMIN_ID, department_id: DEPARTMENT_ID, permissionCodes: ["system.users"] } } = {}) {
async function buildApp({ actor = { id: ADMIN_ID, department_id: DEPARTMENT_ID, permissionCodes: ["system.settings"] } } = {}) {
const app = Fastify();
const state = {
cycles: [],
@@ -276,7 +276,7 @@ async function buildApp({
}
describe("performance review routes", () => {
it("allows direct managers to score direct reports in active cycles", async () => {
it("rejects direct managers while performance management is frozen before launch", async () => {
const { app, state, performanceReviewDimension } = await buildApp();
const response = await app.inject({
@@ -292,23 +292,14 @@ describe("performance review routes", () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toMatchObject({
employeeId: EMPLOYEE_ID,
overallScore: 4.5,
comment: "表现稳定",
dimensions: [
{ dimensionType: "work_quality", score: 4.5 },
{ dimensionType: "teamwork", score: 4 },
],
});
expect(state.reviews.find((item) => item.cycle_id === ACTIVE_CYCLE_ID && item.employee_id === EMPLOYEE_ID)).toBeTruthy();
expect(performanceReviewDimension.createMany).toHaveBeenCalledTimes(1);
expect(response.statusCode).toBe(403);
expect(state.reviews.find((item) => item.cycle_id === ACTIVE_CYCLE_ID && item.employee_id === EMPLOYEE_ID)).toBeFalsy();
expect(performanceReviewDimension.createMany).not.toHaveBeenCalled();
await app.close();
});
it("returns completed results only for self viewers", async () => {
it("rejects self viewers while performance management is frozen before launch", async () => {
const { app } = await buildApp({
actor: {
id: EMPLOYEE_ID,
@@ -322,11 +313,7 @@ describe("performance review routes", () => {
url: `/api/performance/employees/${EMPLOYEE_ID}/reviews`,
});
expect(response.statusCode).toBe(200);
expect(response.json().data.viewerRole).toBe("self");
expect(response.json().data.items).toHaveLength(1);
expect(response.json().data.items[0].cycle.status).toBe("completed");
expect(response.json().data.items[0].review.overallScore).toBe(4);
expect(response.statusCode).toBe(403);
await app.close();
});
@@ -359,7 +346,7 @@ describe("performance review routes", () => {
actor: {
id: ADMIN_ID,
department_id: DEPARTMENT_ID,
permissionCodes: ["system.users"],
permissionCodes: ["system.settings"],
},
});
+21
查看文件
@@ -109,6 +109,27 @@ async function buildApp({ employee, prisma }) {
}
describe("global search R23 confidential boundary", () => {
it("does not expose contract registry or confidential document refs to project managers through search", async () => {
const prisma = createPrisma();
const app = await buildApp({
employee: { id: "manager-1", department_id: null, permissionCodes: ["project.view_all", "project.manage"] },
prisma,
});
const response = await app.inject({
method: "GET",
url: "/api/search?q=HT-001",
headers: { authorization: "Bearer test-token" },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.items).toEqual([]);
expect(prisma.contractRegistry.findMany).not.toHaveBeenCalled();
expect(prisma.confidentialDocumentRef.findMany).not.toHaveBeenCalled();
await app.close();
});
it("does not match sensitive summary text but still matches minimal identifiers", async () => {
const prisma = createPrisma();
const app = await buildApp({
@@ -1,7 +1,12 @@
const workflowRoutes = require("../../../src/routes/workflow");
describe("Workflow Route Permissions", () => {
const { listAllowedActionsForTask, parseTaskListQuery } = workflowRoutes.__testables;
const {
buildWorkflowEntityVisibilityWhere,
canViewWorkflowEntityType,
listAllowedActionsForTask,
parseTaskListQuery,
} = workflowRoutes.__testables;
it("allows leave approvers to act on leave tasks", () => {
const actions = listAllowedActionsForTask(
@@ -133,4 +138,26 @@ describe("Workflow Route Permissions", () => {
it("rejects unsupported workflow entityType filters", () => {
expect(() => parseTaskListQuery({ entityType: "invoice_pool" })).toThrow("参数校验失败");
});
it("hides travel workflow entities when travel is frozen for the actor", () => {
const request = { employee: { permissionCodes: [] } };
expect(canViewWorkflowEntityType(request, "travel_request")).toBe(false);
expect(buildWorkflowEntityVisibilityWhere(request)).toEqual({
entity_type: { notIn: ["travel_request"] },
});
expect(buildWorkflowEntityVisibilityWhere(request, "travel_request")).toEqual({
entity_type: "__forbidden__",
});
});
it("keeps travel workflow entities visible only for system settings holders", () => {
const request = { employee: { permissionCodes: ["system.settings"] } };
expect(canViewWorkflowEntityType(request, "travel_request")).toBe(true);
expect(buildWorkflowEntityVisibilityWhere(request)).toEqual({});
expect(buildWorkflowEntityVisibilityWhere(request, "travel_request")).toEqual({
entity_type: "travel_request",
});
});
});
@@ -159,7 +159,7 @@ async function buildApp({ prisma, employee }) {
}
describe("workflow transfer route", () => {
it.each(["leave", "purchase_request", "travel_request"])(
it.each(["leave", "purchase_request"])(
"allows transfer for %s when target has matching approve permission",
async (entityType) => {
const tx = buildTx({ entityType });
@@ -217,6 +217,33 @@ describe("workflow transfer route", () => {
},
);
it("rejects transfer for frozen travel_request tasks", async () => {
const tx = buildTx({ entityType: "travel_request" });
const app = await buildApp({
prisma: buildPrisma(tx),
employee: {
id: "actor-1",
roleCodes: ["manager"],
permissionCodes: ["expense.transfer"],
},
});
const response = await app.inject({
method: "POST",
url: `/api/workflow/tasks/${buildTaskRow().id}/transfer`,
headers: { authorization: "Bearer test" },
payload: {
toApproverId: "44444444-4444-4444-8444-444444444444",
reason: "当前事项需转交对应审批人继续处理,请及时跟进。",
},
});
expect(response.statusCode).toBe(403);
expect(tx.workflowTask.update).not.toHaveBeenCalled();
await app.close();
});
it("rejects target approver without matching entity approve permission", async () => {
const tx = buildTx({ entityType: "leave", targetRoleCode: "finance" });
const app = await buildApp({