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
@@ -1,14 +1,30 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
findSodConflicts,
|
||||
isPermissionCode,
|
||||
isSpecialPermission,
|
||||
parsePermission,
|
||||
Permission,
|
||||
PERMISSION_CATALOG,
|
||||
PERMISSION_CODES,
|
||||
PERMISSION_MODULES,
|
||||
permissionMetaOf,
|
||||
permissionsByModule,
|
||||
ROLE_SOFT_LIMIT,
|
||||
SOD_PAIRS,
|
||||
SPECIAL_PERMISSIONS,
|
||||
splitPermissions,
|
||||
SUPER_ADMIN_SOFT_LIMIT,
|
||||
} from './permissions.js';
|
||||
|
||||
describe('权限点常量', () => {
|
||||
describe('权限点目录', () => {
|
||||
it('H1 口径:共 30 个权限点,编码全局唯一', () => {
|
||||
expect(PERMISSION_CODES).toHaveLength(30);
|
||||
expect(PERMISSION_CATALOG).toHaveLength(30);
|
||||
expect(new Set(PERMISSION_CODES).size).toBe(30);
|
||||
});
|
||||
|
||||
it('全部符合 模块:动作 命名且模块为小写', () => {
|
||||
for (const code of PERMISSION_CODES) {
|
||||
expect(code).toMatch(/^[a-z][a-z_]*:[a-z][a-z_]*$/);
|
||||
@@ -16,15 +32,175 @@ describe('权限点常量', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('特殊权限必须是已登记的权限点', () => {
|
||||
it('保留既有 read / write 编码,并补齐 H1 新增的 5 个权限点', () => {
|
||||
for (const code of [
|
||||
'script:read',
|
||||
'script:write',
|
||||
'material:read',
|
||||
'material:write',
|
||||
'asset:read',
|
||||
'asset:write',
|
||||
'delivery:read',
|
||||
'customer:read',
|
||||
]) {
|
||||
expect(PERMISSION_CODES).toContain(code);
|
||||
}
|
||||
expect(Permission.ROLE_VIEW).toBe('role:view');
|
||||
expect(Permission.CUSTOMER_FIELD_APPLY).toBe('customer_field:apply');
|
||||
expect(Permission.CUSTOMER_TRANSFER).toBe('customer:transfer');
|
||||
expect(Permission.CUSTOMER_GROUP_MANAGE).toBe('customer_group:manage');
|
||||
expect(Permission.ASSET_DELETE).toBe('asset:delete');
|
||||
});
|
||||
|
||||
it('权限树为 10 个模块,顺序与名称对齐 design/a2-ui-v1/brief.md', () => {
|
||||
const groups = permissionsByModule();
|
||||
expect(groups).toHaveLength(10);
|
||||
expect(PERMISSION_MODULES).toHaveLength(10);
|
||||
expect(groups.map((group) => group.module)).toEqual([
|
||||
'script',
|
||||
'material',
|
||||
'asset',
|
||||
'delivery',
|
||||
'customer',
|
||||
'customer_group',
|
||||
'identity',
|
||||
'industry',
|
||||
'platform',
|
||||
'audit',
|
||||
]);
|
||||
expect(groups.map((group) => group.moduleLabel)).toEqual([
|
||||
'脚本',
|
||||
'素材',
|
||||
'资产',
|
||||
'导出交付',
|
||||
'客户',
|
||||
'客户组',
|
||||
'组织与角色',
|
||||
'行业包',
|
||||
'平台配置',
|
||||
'审计',
|
||||
]);
|
||||
expect(groups.map((group) => group.sortOrder)).toEqual([
|
||||
10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
|
||||
]);
|
||||
expect(groups.flatMap((group) => group.permissions)).toHaveLength(30);
|
||||
});
|
||||
|
||||
it('权限点显示名与说明与 UI 原型一致(抽样关键项)', () => {
|
||||
const nameOf = (code: string): { name: string; description: string } => {
|
||||
const meta = permissionMetaOf(code);
|
||||
if (meta === undefined) {
|
||||
throw new Error(`权限点未登记:${code}`);
|
||||
}
|
||||
return { name: meta.name, description: meta.description };
|
||||
};
|
||||
expect(nameOf('script:read')).toEqual({ name: '查看脚本', description: '脚本库、模板与版本' });
|
||||
expect(nameOf('delivery:export')).toEqual({
|
||||
name: '发起导出',
|
||||
description: '数据离开平台的唯一出口(特殊)',
|
||||
});
|
||||
expect(nameOf('user:manage')).toEqual({ name: '管理用户', description: '新建、停用、转交' });
|
||||
expect(nameOf('customer_group:manage')).toEqual({
|
||||
name: '管理客户组',
|
||||
description: '客户组增改与成员',
|
||||
});
|
||||
expect(nameOf('audit:export')).toEqual({
|
||||
name: '导出审计',
|
||||
description: '审计导出并留痕(特殊)',
|
||||
});
|
||||
});
|
||||
|
||||
it('目录元数据完整(编码 / 模块 / 名称 / 说明 / 特殊标记 / 排序)', () => {
|
||||
for (const item of PERMISSION_CATALOG) {
|
||||
expect(permissionMetaOf(item.code)).toEqual(item);
|
||||
expect(item.name.length).toBeGreaterThan(0);
|
||||
expect(item.description.length).toBeGreaterThan(0);
|
||||
expect(item.moduleLabel.length).toBeGreaterThan(0);
|
||||
expect(item.isSpecial).toBe(isSpecialPermission(item.code));
|
||||
}
|
||||
const sortOrders = PERMISSION_CATALOG.map((item) => item.sortOrder);
|
||||
expect(sortOrders).toEqual([...sortOrders].sort((a, b) => a - b));
|
||||
});
|
||||
|
||||
it('特殊权限为 6 个编码(5 类),均已在目录登记', () => {
|
||||
expect(SPECIAL_PERMISSIONS).toHaveLength(6);
|
||||
expect([...SPECIAL_PERMISSIONS].sort()).toEqual(
|
||||
[
|
||||
'delivery:export',
|
||||
'customer_field:approve',
|
||||
'industry:publish',
|
||||
'ai:configure',
|
||||
'audit:read',
|
||||
'audit:export',
|
||||
].sort(),
|
||||
);
|
||||
for (const code of SPECIAL_PERMISSIONS) {
|
||||
expect(PERMISSION_CODES).toContain(code);
|
||||
expect(isSpecialPermission(code)).toBe(true);
|
||||
}
|
||||
expect(isSpecialPermission('script:write')).toBe(false);
|
||||
});
|
||||
|
||||
it('软上限取自 PRD 建议值', () => {
|
||||
expect(ROLE_SOFT_LIMIT).toBe(20);
|
||||
expect(SUPER_ADMIN_SOFT_LIMIT).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SoD 三对', () => {
|
||||
it('互斥对为三对,且两侧都是已登记权限点', () => {
|
||||
expect(SOD_PAIRS).toHaveLength(3);
|
||||
expect(SOD_PAIRS.map((pair) => pair.id)).toEqual([
|
||||
'script-write-review',
|
||||
'industry-manage-publish',
|
||||
'customer-field-apply-approve',
|
||||
]);
|
||||
for (const pair of SOD_PAIRS) {
|
||||
for (const code of pair.permissions) {
|
||||
expect(PERMISSION_CODES).toContain(code);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('isPermissionCode 只认可登记值', () => {
|
||||
expect(isPermissionCode(Permission.SCRIPT_READ)).toBe(true);
|
||||
expect(isPermissionCode('script:delete_all')).toBe(false);
|
||||
it('同时拥有互斥两侧即命中;只拥有单侧不命中', () => {
|
||||
expect(findSodConflicts(['script:write', 'script:review'])).toEqual([
|
||||
{
|
||||
pairId: 'script-write-review',
|
||||
label: '脚本生成 × 脚本审核',
|
||||
permissions: ['script:write', 'script:review'],
|
||||
},
|
||||
]);
|
||||
expect(findSodConflicts(['industry:manage', 'industry:publish']).map((c) => c.pairId)).toEqual([
|
||||
'industry-manage-publish',
|
||||
]);
|
||||
expect(
|
||||
findSodConflicts(['customer_field:apply', 'customer_field:approve']).map((c) => c.pairId),
|
||||
).toEqual(['customer-field-apply-approve']);
|
||||
expect(findSodConflicts(['script:write', 'script:read'])).toEqual([]);
|
||||
expect(findSodConflicts([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitPermissions', () => {
|
||||
it('拆分普通权限与特殊权限并去重排序', () => {
|
||||
expect(
|
||||
splitPermissions([
|
||||
'script:write',
|
||||
'delivery:export',
|
||||
'script:read',
|
||||
'delivery:export',
|
||||
'audit:read',
|
||||
]),
|
||||
).toEqual({
|
||||
permissions: ['script:read', 'script:write'],
|
||||
specialPermissions: ['audit:read', 'delivery:export'],
|
||||
});
|
||||
});
|
||||
|
||||
it('未登记编码抛错(调用方映射 40001)', () => {
|
||||
expect(() => splitPermissions(['script:delete_all'])).toThrow(
|
||||
'INVALID_PERMISSION_CODE:script:delete_all',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,3 +216,10 @@ describe('parsePermission', () => {
|
||||
expect(parsePermission('a:b:c')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPermissionCode', () => {
|
||||
it('只认可登记值', () => {
|
||||
expect(isPermissionCode(Permission.SCRIPT_READ)).toBe(true);
|
||||
expect(isPermissionCode('script:delete_all')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user