feat(identity): 完成 A2 角色权限与数据范围全栈落地
背景:A2 开卡前完成 H1 权限门,确认权限点目录、特殊权限、SoD 三对、六类数据范围与超管隐式全集口径(ADR-001)。 改动: - shared:30 权限点 / 10 模块目录与元数据、特殊权限 6 编码、SoD 三对、角色与权限树契约;/me 改 dataScopes 多角色并集。 - prisma:expand 迁移新增 permissions / data_scopes;种子按原型口径重写角色权限、数据范围与 8 项特殊授予。 - api:角色 CRUD / 复制 / 停用 / 删除、权限点启动同步、SoD 40304、系统角色保护、六类范围 ScopeContext 与 CustomerDirectory 端口、变更审计与 outbox。 - web:方案 B 主从分栏三角色页面(列表 / 编辑器 / 新建),权限树、范围选择、特殊权限二次确认、保存影响、SoD 冲突、只读态与模板预填。 - 测试:角色集成 13 条 + A2 e2e 1 条;A1 全量回归。 验证:format:check / lint / typecheck / test(306)/ test:int(27)/ A2 e2e(1,复跑通过)/ openapi:export 全绿。
This commit is contained in:
1 parent
f97f97b0cd
commit
61894df401
80 files changed
+14380
-237
No files matched your search
@@ -7,6 +7,7 @@ import type { AuthContext, AuthenticatedRequest } from './auth.types';
|
||||
import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from './decorators';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { RequestContextService } from './request-context';
|
||||
import { ScopeResolver } from './scope/scope-resolver.service';
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
const MUST_CHANGE_ALLOWED_PATHS: readonly string[] = [
|
||||
@@ -123,9 +124,9 @@ export class PermissionGuard implements CanActivate {
|
||||
return true;
|
||||
}
|
||||
const auth = requireAuth(context);
|
||||
const missing = normal.filter((code) => !auth.permissions.includes(code));
|
||||
if (missing.length > 0) {
|
||||
throw new AppException({ code: ErrorCode.PERMISSION_DENIED, details: { missing } });
|
||||
// 多个权限点之间为 OR(任一满足即通过,见 @RequirePermissions 注释)
|
||||
if (!normal.some((code) => auth.permissions.includes(code))) {
|
||||
throw new AppException({ code: ErrorCode.PERMISSION_DENIED, details: { required: normal } });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -147,11 +148,10 @@ export class SpecialPermissionGuard implements CanActivate {
|
||||
return true;
|
||||
}
|
||||
const auth = requireAuth(context);
|
||||
const missing = special.filter((code) => !auth.permissions.includes(code));
|
||||
if (missing.length > 0) {
|
||||
if (!special.some((code) => auth.permissions.includes(code))) {
|
||||
throw new AppException({
|
||||
code: ErrorCode.SPECIAL_PERMISSION_REQUIRED,
|
||||
details: { missing },
|
||||
details: { required: special },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
@@ -159,16 +159,32 @@ export class SpecialPermissionGuard implements CanActivate {
|
||||
}
|
||||
|
||||
/**
|
||||
* 4) 数据范围守卫骨架:A2 按 dataScope 注入 ScopeContext(部门 / 客户集合)。
|
||||
* A1 identity 接口不返回客户数据,非全局范围暂直接放行。
|
||||
* 4) 数据范围守卫:解析 `ScopeContext`(六类范围 → 可见客户集合),注入 `request.scope`
|
||||
* 与请求上下文;Repository 必须通过 `customerScopeWhere(ctx)` 强制客户维度过滤。
|
||||
*
|
||||
* 口径(技术架构 §5.5 / §7.1「失败处理」):解析失败一律 403(40302),不降级为「全部客户」;
|
||||
* 公开接口(登录 / 健康检查)不解析。
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScopeGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const auth = authOf(context);
|
||||
if (auth !== null && auth.dataScope.type !== 'all') {
|
||||
// TODO(A2-BE-04):按 dataScope 注入 ScopeContext,并在 Repository 强制带 customer_id 过滤
|
||||
constructor(
|
||||
@Inject(Reflector) private readonly reflector: Reflector,
|
||||
@Inject(ScopeResolver) private readonly resolver: ScopeResolver,
|
||||
@Inject(RequestContextService) private readonly requestContext: RequestContextService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (isPublic(this.reflector, context)) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const auth = authOf(context);
|
||||
if (auth === null) {
|
||||
return true;
|
||||
}
|
||||
const scope = await this.resolver.resolve(auth);
|
||||
request.scope = scope;
|
||||
this.requestContext.patch({ scope });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user