Stop persisting roles to localStorage as authority; AdminSettingsModal reads and writes role matrices via /api/v1/admin/roles; hasPermission denies when capabilities are not loaded from /auth/me. Co-authored-by: Cursor <cursoragent@cursor.com>
213 lines
6.3 KiB
TypeScript
213 lines
6.3 KiB
TypeScript
import { RoleDefinition, PermissionKey } from '../types/rbac';
|
||
import { UserProfile } from '../types/docgov';
|
||
import { DeliverableItem } from '../types/workbench';
|
||
|
||
/**
|
||
* 系统内置与预置全局角色库
|
||
*/
|
||
export const DEFAULT_ROLES: RoleDefinition[] = [
|
||
{
|
||
id: 'role-admin',
|
||
name: '系统管理员',
|
||
code: 'ADMIN',
|
||
description: '拥有系统所有功能与数据的全量管理权限(防死锁兜底)。',
|
||
isSystem: true,
|
||
permissions: [
|
||
'project:create',
|
||
'project:assign_lead',
|
||
'perm:manage',
|
||
'workflow:config_visibility',
|
||
'doc:override_visibility',
|
||
'doc:export',
|
||
'workflow:view_all',
|
||
],
|
||
createdAt: '2026-09-08 00:00',
|
||
updatedAt: '2026-09-08 00:00',
|
||
},
|
||
{
|
||
id: 'role-director',
|
||
name: '研发总监',
|
||
code: 'DIRECTOR',
|
||
description: '负责研发中心整体项目决策、立项审查、权限监督与终审批准。',
|
||
isSystem: false,
|
||
permissions: [
|
||
'project:create',
|
||
'project:assign_lead',
|
||
'workflow:config_visibility',
|
||
'doc:override_visibility',
|
||
'doc:export',
|
||
'workflow:view_all',
|
||
],
|
||
createdAt: '2026-09-08 00:00',
|
||
updatedAt: '2026-09-08 00:00',
|
||
},
|
||
{
|
||
id: 'role-tech-lead',
|
||
name: '研发技术主管',
|
||
code: 'TECH_LEAD',
|
||
description: '负责具体研发管线的进度把控、文档责任人分配与技术审核。',
|
||
isSystem: false,
|
||
permissions: [
|
||
'project:create',
|
||
'project:assign_lead',
|
||
'doc:override_visibility',
|
||
'doc:export',
|
||
'workflow:view_all',
|
||
],
|
||
createdAt: '2026-09-08 00:00',
|
||
updatedAt: '2026-09-08 00:00',
|
||
},
|
||
{
|
||
id: 'role-engineer',
|
||
name: '方案工程师',
|
||
code: 'ENGINEER',
|
||
description: '负责医疗器械规格设计、受控正文编制、技术测算与推演。',
|
||
isSystem: false,
|
||
permissions: ['doc:export', 'workflow:view_all'],
|
||
createdAt: '2026-09-08 00:00',
|
||
updatedAt: '2026-09-08 00:00',
|
||
},
|
||
{
|
||
id: 'role-guest',
|
||
name: '外部审计/访客',
|
||
code: 'GUEST',
|
||
description: '仅允许通过单文档覆盖或公开授权查阅特定报告,默认不可见内部工作流。',
|
||
isSystem: false,
|
||
permissions: ['doc:export'],
|
||
createdAt: '2026-09-08 00:00',
|
||
updatedAt: '2026-09-08 00:00',
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 判定用户是否具备指定全局功能权限。
|
||
* capabilities 来自服务端 /auth/me 时为唯一权威来源(fail closed)。
|
||
*/
|
||
export function hasPermission(
|
||
_user: UserProfile,
|
||
_roles: RoleDefinition[],
|
||
permKey: PermissionKey,
|
||
capabilities?: string[]
|
||
): boolean {
|
||
if (capabilities === undefined) {
|
||
return false;
|
||
}
|
||
if (capabilities.includes('perm:manage')) {
|
||
return true;
|
||
}
|
||
return capabilities.includes(permKey);
|
||
}
|
||
|
||
/**
|
||
* 交付物可见性 5 步决策流水线
|
||
* 1. private 个人私密文档:严格隔离,仅创建者本人可见(绝对红线)
|
||
* 2. shared 项目协作资料:项目全员可见
|
||
* 3. 管理员特权:ADMIN / perm:manage 全员可见
|
||
* 4. 三权法定责任人保底:编制人、审核人、批准人必定可见
|
||
* 5. 单文档覆盖规则 (Override Priority):
|
||
* - designated_only: 仅责任人可见 (非责任人已在步骤4排除,直接拒绝)
|
||
* - role_whitelist: 仅白名单内角色可见
|
||
* - public: 全员可见
|
||
* 6. 回退继承工作流基线:用户角色需具备 workflow:view_all 权限
|
||
*/
|
||
export function isDeliverableVisible(
|
||
user: UserProfile,
|
||
roles: RoleDefinition[],
|
||
doc: DeliverableItem
|
||
): boolean {
|
||
// 步骤 1:个人私密文档 (private) 绝对隔离
|
||
if (doc.category === 'private') {
|
||
return doc.creatorId === user.id;
|
||
}
|
||
|
||
// 步骤 2:项目协作参考库 (shared) 对项目成员全员可见
|
||
if (doc.category === 'shared') {
|
||
return true;
|
||
}
|
||
|
||
// 步骤 3:管理员超级特权
|
||
const role = roles.find((r) => r.id === user.roleId);
|
||
if (role?.code === 'ADMIN' || role?.permissions.includes('perm:manage')) {
|
||
return true;
|
||
}
|
||
|
||
// 步骤 4:法定三权责任人保底 (防止指派后本人看不见无法履责)
|
||
const isAssignee =
|
||
doc.author === user.name ||
|
||
doc.reviewer === user.name ||
|
||
doc.approver === user.name;
|
||
if (isAssignee) {
|
||
return true;
|
||
}
|
||
|
||
// 步骤 5:单文档覆盖策略判定
|
||
const rule = doc.visibilityRule;
|
||
if (rule && rule.mode !== 'inherit') {
|
||
// 5.1 特例人员白名单 (特聘临床专家等)
|
||
if (rule.allowedUserIds?.includes(user.id)) {
|
||
return true;
|
||
}
|
||
|
||
switch (rule.mode) {
|
||
case 'designated_only':
|
||
// 步骤 4 已经排除了非责任人,因此此处直接拒绝
|
||
return false;
|
||
|
||
case 'role_whitelist':
|
||
// 判定当前用户全局角色是否在白名单中
|
||
return Boolean(user.roleId && rule.allowedRoleIds?.includes(user.roleId));
|
||
|
||
case 'public':
|
||
// 放宽为全员公开可见
|
||
return true;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 步骤 6:回退继承工作流默认基线规则(文档可见性仍用角色矩阵作 UI 展示兜底)
|
||
return hasPermission(user, roles, 'workflow:view_all', capabilitiesFromRole(roles, user.roleId));
|
||
}
|
||
|
||
/**
|
||
* 批量过滤大盘交付物
|
||
*/
|
||
export function filterVisibleDeliverablesWithRBAC(
|
||
deliverables: DeliverableItem[],
|
||
user: UserProfile,
|
||
roles: RoleDefinition[]
|
||
): DeliverableItem[] {
|
||
return deliverables.filter((doc) => isDeliverableVisible(user, roles, doc));
|
||
}
|
||
|
||
/**
|
||
* 核心业务动作权限拦截判定
|
||
*/
|
||
export function canPerformDocAction(
|
||
action: 'assign_lead' | 'override_visibility' | 'create_project',
|
||
user: UserProfile,
|
||
roles: RoleDefinition[],
|
||
capabilities?: string[]
|
||
): boolean {
|
||
switch (action) {
|
||
case 'create_project':
|
||
return hasPermission(user, roles, 'project:create', capabilities);
|
||
case 'assign_lead':
|
||
return hasPermission(user, roles, 'project:assign_lead', capabilities);
|
||
case 'override_visibility':
|
||
return (
|
||
hasPermission(user, roles, 'doc:override_visibility', capabilities) ||
|
||
hasPermission(user, roles, 'perm:manage', capabilities)
|
||
);
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** 从角色定义提取 capabilities(测试与文档可见性兜底用)。 */
|
||
export function capabilitiesFromRole(roles: RoleDefinition[], roleId?: string): string[] {
|
||
if (!roleId) return [];
|
||
return roles.find((r) => r.id === roleId)?.permissions ?? [];
|
||
}
|