ec-nest/src/common/data-scope.ts
liuyun 513688a5e6 fix(security): P0 安全越权堵漏(C1/H1-H6/H10/H11/H13/M1/M4/M6/M8-M11/M22)
- 平台管理员模型:super_admin=平台超管、tenant_super_admin=租户顶层(保留码),tenant:* 仅平台可授予
- 软删改名回收唯一键 + P2002→409
- 删除角色级联清理关联 + 聚合过滤已删/停用角色资源
- tokenVersion 会话吊销(重置密码/停用/删除即时踢下线)
- data_scope self 按 userId 过滤 + 详情数据权限校验
- 组织树防环、seed 不重置密码、日志脱敏补 phone/email
- JWT 密钥强校验、pageSize 上限、DTO 枚举校验、Prisma 错误映射、授权事务化

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:10:00 +08:00

74 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { PrismaClient } from '@prisma/client';
import { DataScope } from './enums.js';
/** 数据权限档位(从大到小)的优先级,用于多角色取最高权限。 */
const RANK: Record<DataScope, number> = {
all: 5,
dept_and_child: 4,
dept: 3,
custom: 2,
self: 1,
};
/** 从多个角色的 data_scope 中取最高权限档。 */
export function resolveDataScope(scopes: DataScope[]): DataScope {
if (scopes.length === 0) return 'self';
return scopes.reduce((a, b) => (RANK[a] >= RANK[b] ? a : b));
}
/**
* 计算数据权限可见的部门 code 集合。
* 返回空数组表示「全部可见」(不受数据权限限制)。
*/
export async function resolveVisibleOrgs(opts: {
prisma: PrismaClient;
tenantCode: string;
dataScope: DataScope;
orgCode: string | null;
roleCodes: string[];
}): Promise<string[]> {
const { prisma, tenantCode, dataScope, orgCode, roleCodes } = opts;
switch (dataScope) {
case 'all':
return [];
case 'self':
// 仅本人:不走组织过滤,由调用方按 userId 过滤(返回哨兵避免误放大为全量)
return ['__none__'];
case 'dept':
return orgCode ? [orgCode] : ['__none__'];
case 'dept_and_child':
return orgCode ? collectDescendants(prisma, tenantCode, orgCode) : ['__none__'];
case 'custom': {
const rows = await prisma.roleOrg.findMany({
where: { tenantCode, roleCode: { in: roleCodes } },
select: { orgCode: true },
});
const codes: string[] = [...new Set(rows.map((r) => r.orgCode))];
return codes.length ? codes : ['__none__'];
}
}
}
/** 收集某部门及其全部后代部门的 code(含自身),带 visited 防环。 */
async function collectDescendants(prisma: PrismaClient, tenantCode: string, rootCode: string): Promise<string[]> {
const result: string[] = [];
const visited = new Set<string>();
let frontier = [rootCode];
while (frontier.length > 0) {
const next: string[] = [];
for (const code of frontier) {
if (visited.has(code)) continue;
visited.add(code);
result.push(code);
next.push(code);
}
if (next.length === 0) break;
const children = await prisma.org.findMany({
where: { tenantCode, parentCode: { in: next }, deletedAt: null },
select: { code: true },
});
frontier = children.map((c) => c.code);
}
return result;
}