收紧报销草稿读取边界 [09-04-reimbursement-state-gap-finance-view][R2]

这个提交包含在:
2026-09-05 09:13:53 +08:00
父节点 1b120e0829
当前提交 8e0af8235e
共修改 14 个文件,包含 704 行新增和 120 行删除
+40 -39
查看文件
@@ -26,6 +26,11 @@ const { mapInvoiceDocumentMeta } = require("../services/invoiceDocumentMeta");
const { dispatchNotificationBatch, flushNotificationDispatchTasks } = require("../services/notificationDispatcher");
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
const { ensureCutoverAllowed } = require("../services/cutover");
const {
buildReadableReimbursementWhere,
canReadReimbursementBusinessFact,
isGlobalReimbursementViewRole,
} = require("../services/reimbursementVisibility");
const { resolveApplicableBudgetPoolByContext } = require("../services/budgetPool");
const {
buildPersistedRiskFlag,
@@ -245,9 +250,12 @@ const ATTACHMENT_MIME_ALLOWLIST = new Set([
"image/webp",
]);
function ensureOwnOrCanViewAll(fastify, request, reimbursement) {
async function ensureOwnOrCanViewAll(fastify, request, reimbursement) {
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
if (canViewAll) {
if (
canViewAll &&
await canReadReimbursementBusinessFact(fastify.prisma, reimbursement, request.employee.id)
) {
return;
}
@@ -275,7 +283,11 @@ async function ensureOwnCanViewAllOrWorkflowReviewer(fastify, request, reimburse
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
const isOwn = reimbursement.owner_id === request.employee.id || reimbursement.submitter_id === request.employee.id;
const canViewOwn = fastify.hasPermission(request.employee, "expense.view_own") && isOwn;
if (canViewAll || canViewOwn) return;
if (canViewOwn) return;
if (
canViewAll &&
await canReadReimbursementBusinessFact(fastify.prisma, reimbursement, request.employee.id)
) return;
const delegatedPrincipalIds = await listActiveDelegationPrincipals(fastify.prisma, {
delegateEmployeeId: request.employee.id,
@@ -852,7 +864,7 @@ function buildReimbursementStatsConditions({
conditions.push(Prisma.sql`${sqlIdentifier(formAlias, "department_id")} = ${departmentId}`);
}
if (!isGlobalViewRole(request.employee.roleCodes ?? [])) {
if (!isGlobalReimbursementViewRole(request.employee.roleCodes ?? [])) {
conditions.push(
Prisma.sql`(
${sqlIdentifier(formAlias, "owner_id")} = ${request.employee.id}
@@ -862,6 +874,20 @@ function buildReimbursementStatsConditions({
);
}
conditions.push(
Prisma.sql`(
${sqlIdentifier(formAlias, "status")}::text <> 'draft'
OR ${sqlIdentifier(formAlias, "owner_id")} = ${request.employee.id}
OR ${sqlIdentifier(formAlias, "submitter_id")} = ${request.employee.id}
OR EXISTS (
SELECT 1
FROM "workflow_instances" visibility_wi
WHERE visibility_wi.entity_type = 'reimbursement'
AND visibility_wi.entity_id = ${sqlIdentifier(formAlias, "id")}
)
)`,
);
return conditions;
}
@@ -1030,32 +1056,6 @@ function parseReimbursementListQuery(query) {
};
}
function buildOwnScopeWhere(employeeId) {
return {
OR: [{ owner_id: employeeId }, { submitter_id: employeeId }],
};
}
function isGlobalViewRole(roleCodes) {
return roleCodes.some((code) =>
["admin", "ceo", "finance", "finance_mgr", "hr"].includes(code),
);
}
function buildViewAllScopeWhere(request) {
if (isGlobalViewRole(request.employee.roleCodes ?? [])) {
return {};
}
return {
OR: [
{ owner_id: request.employee.id },
{ submitter_id: request.employee.id },
{ owner: { direct_manager_id: request.employee.id } },
],
};
}
function mapReimbursementSummary(item) {
return {
id: item.id,
@@ -2042,12 +2042,11 @@ async function reimbursementRoutes(fastify) {
delegateEmployeeId: request.employee.id,
allowedScopes: ["all"],
});
const where = {
...(query.status ? { status: query.status } : {}),
...(scope === "all"
? buildViewAllScopeWhere(request)
: buildOwnScopeWhere(request.employee.id)),
};
const where = buildReadableReimbursementWhere({
employee: request.employee,
scope,
filters: query.status ? { status: query.status } : {},
});
const [items, total] = await Promise.all([
fastify.prisma.reimbursementForm.findMany({
@@ -3335,6 +3334,7 @@ async function reimbursementRoutes(fastify) {
select: {
owner_id: true,
submitter_id: true,
status: true,
},
},
},
@@ -3435,7 +3435,7 @@ async function reimbursementRoutes(fastify) {
sensitivity_level: true,
attachment_type: true,
download_policy: true,
reimbursement_form: { select: { owner_id: true, submitter_id: true } },
reimbursement_form: { select: { owner_id: true, submitter_id: true, status: true } },
},
});
if (!attachment) {
@@ -3508,6 +3508,7 @@ async function reimbursementRoutes(fastify) {
id: true,
owner_id: true,
submitter_id: true,
status: true,
invoice_pool_items: {
orderBy: [{ created_at: "asc" }, { id: "asc" }],
select: {
@@ -4132,12 +4133,12 @@ async function reimbursementRoutes(fastify) {
const reimbursementId = parseReimbursementId(request.params?.id);
const reimbursement = await fastify.prisma.reimbursementForm.findUnique({
where: { id: reimbursementId },
select: { id: true, owner_id: true, submitter_id: true },
select: { id: true, owner_id: true, submitter_id: true, status: true },
});
if (!reimbursement) {
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
}
ensureOwnOrCanViewAll(fastify, request, reimbursement);
await ensureOwnOrCanViewAll(fastify, request, reimbursement);
const precheck = await resolveEntityPrecheck({
prisma: fastify.prisma,
config: fastify.config,