feat(web): wire rbac permission enforcement into left rail and rights modal
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+132
-12
@@ -11,6 +11,7 @@ import { DocGovActionBar } from './components/doc/DocGovActionBar';
|
||||
import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard';
|
||||
import { UserProjectRightsModal } from './components/user/UserProjectRightsModal';
|
||||
import { SettingsModal } from './components/user/SettingsModal';
|
||||
import { DocVisibilityModal } from './components/doc/DocVisibilityModal';
|
||||
import { markdownToBlockAST } from './engine/markdownToBlock';
|
||||
import { blockASTToMarkdown } from './engine/blockToMarkdown';
|
||||
import { recomputeNumbering } from './engine/autoNumber';
|
||||
@@ -23,6 +24,12 @@ import {
|
||||
InteractiveCardData,
|
||||
} from './types/workbench';
|
||||
import { UserRole, UserProfile, DocPermissionRole } from './types/docgov';
|
||||
import { RoleDefinition, DocVisibilityRule } from './types/rbac';
|
||||
import {
|
||||
DEFAULT_ROLES,
|
||||
hasPermission,
|
||||
filterVisibleDeliverablesWithRBAC,
|
||||
} from './engine/rbac';
|
||||
import { MOCK_DOCUMENTS } from './data/mockDocuments';
|
||||
import { exportDocumentToDocx } from './engine/docxRenderer';
|
||||
import { exportDocumentToXlsx } from './engine/xlsxRenderer';
|
||||
@@ -47,6 +54,10 @@ import {
|
||||
saveMessages,
|
||||
loadDeliverables,
|
||||
saveDeliverables,
|
||||
loadRoles,
|
||||
saveRoles,
|
||||
loadUsers,
|
||||
saveUsers,
|
||||
} from './data/storage';
|
||||
|
||||
export const INITIAL_DELIVERABLES: DeliverableItem[] = [
|
||||
@@ -207,6 +218,7 @@ export const MOCK_USERS: UserProfile[] = [
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '资深医疗器械方案工程师',
|
||||
roleId: 'role-engineer',
|
||||
projectPermissions: {
|
||||
'doc-001': 'author', // 编制人
|
||||
'doc-002': 'reviewer', // 审核人
|
||||
@@ -222,6 +234,7 @@ export const MOCK_USERS: UserProfile[] = [
|
||||
employeeId: 'SMDF-0205',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '研发项目技术主管',
|
||||
roleId: 'role-tech-lead',
|
||||
projectPermissions: {
|
||||
'doc-001': 'reviewer',
|
||||
'doc-002': 'author',
|
||||
@@ -237,6 +250,7 @@ export const MOCK_USERS: UserProfile[] = [
|
||||
employeeId: 'SMDF-0018',
|
||||
department: '医疗器械研发事业部',
|
||||
position: '微创器械研发总监',
|
||||
roleId: 'role-director',
|
||||
projectPermissions: {
|
||||
'doc-001': 'approver',
|
||||
'doc-002': 'approver',
|
||||
@@ -245,6 +259,28 @@ export const MOCK_USERS: UserProfile[] = [
|
||||
'doc-shared-001': 'approver',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '赵管理员',
|
||||
employeeId: 'SMDF-0001',
|
||||
department: '系统安全与技术委员会',
|
||||
position: '系统全域管理总管',
|
||||
roleId: 'role-admin',
|
||||
projectPermissions: {
|
||||
'doc-001': 'approver',
|
||||
'doc-002': 'approver',
|
||||
'doc-003': 'approver',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '钱审计',
|
||||
employeeId: 'SMDF-9901',
|
||||
department: '第三方合规审计与认证机构',
|
||||
position: '外部合规审查专员',
|
||||
roleId: 'role-guest',
|
||||
projectPermissions: {},
|
||||
},
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
@@ -261,8 +297,21 @@ export default function App() {
|
||||
saveActiveSessionId(activeSessionId);
|
||||
}, [activeSessionId]);
|
||||
|
||||
// 当前用户身份 (职位在全公司唯一不变,但在不同文档有不同法定职责)
|
||||
const [currentUser, setCurrentUser] = useState<UserProfile>(MOCK_USERS[0]);
|
||||
// 全局角色库与人员名单状态 (支持动态增删改与持久化)
|
||||
const [roles, setRoles] = useState<RoleDefinition[]>(() => loadRoles(DEFAULT_ROLES));
|
||||
const [allUsers, setAllUsers] = useState<UserProfile[]>(() => loadUsers(MOCK_USERS));
|
||||
|
||||
// 自动持久化 roles 与 allUsers
|
||||
useEffect(() => {
|
||||
saveRoles(roles);
|
||||
}, [roles]);
|
||||
|
||||
useEffect(() => {
|
||||
saveUsers(allUsers);
|
||||
}, [allUsers]);
|
||||
|
||||
// 当前用户身份 (职位在全公司唯一不变,绑定全局角色,但在不同文档有不同法定职责)
|
||||
const [currentUser, setCurrentUser] = useState<UserProfile>(() => allUsers[0] || MOCK_USERS[0]);
|
||||
|
||||
// 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类,支持持久化)
|
||||
const [deliverables, setDeliverables] = useState<DeliverableItem[]>(() =>
|
||||
@@ -281,6 +330,24 @@ export default function App() {
|
||||
// 模态浮层控制
|
||||
const [rightsModalOpen, setRightsModalOpen] = useState(false);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false);
|
||||
const [targetDocForVisibility, setTargetDocForVisibility] = useState<DeliverableItem | null>(null);
|
||||
|
||||
// 核心功能权限计算
|
||||
const canCreateProject = useMemo(() => {
|
||||
return hasPermission(currentUser, roles, 'project:create');
|
||||
}, [currentUser, roles]);
|
||||
|
||||
const canAssignLead = useMemo(() => {
|
||||
return hasPermission(currentUser, roles, 'project:assign_lead');
|
||||
}, [currentUser, roles]);
|
||||
|
||||
const canOverrideVisibility = useMemo(() => {
|
||||
return (
|
||||
hasPermission(currentUser, roles, 'doc:override_visibility') ||
|
||||
hasPermission(currentUser, roles, 'perm:manage')
|
||||
);
|
||||
}, [currentUser, roles]);
|
||||
|
||||
// 获取当前激活会话的项目ID (默认为 1)
|
||||
const currentProjectId = useMemo(() => {
|
||||
@@ -293,15 +360,27 @@ export default function App() {
|
||||
return deliverables.filter((doc) => doc.projectId === currentProjectId);
|
||||
}, [deliverables, currentProjectId]);
|
||||
|
||||
// 基于当前登录用户过滤可见交付物 (私密文档严格按 creatorId 隔离,流程与共享文档全员可见)
|
||||
// 基于当前登录用户与 RBAC 规则过滤可见交付物 (私密文档严格按 creatorId 隔离,单文档覆盖优先于工作流基线)
|
||||
const visibleDeliverables = useMemo(() => {
|
||||
return currentProjectDeliverables.filter((doc) => {
|
||||
if (doc.category === 'workflow' || doc.category === 'shared') {
|
||||
return true;
|
||||
}
|
||||
return doc.creatorId === currentUser.id;
|
||||
return filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles);
|
||||
}, [currentProjectDeliverables, currentUser, roles]);
|
||||
|
||||
// 单文档可见性覆盖保存处理
|
||||
const handleSaveDocVisibilityRule = (docId: string, rule: DocVisibilityRule | undefined) => {
|
||||
setDeliverables((prev) =>
|
||||
prev.map((d) => (d.docId === docId ? { ...d, visibilityRule: rule } : d))
|
||||
);
|
||||
setDocCache((prev) => {
|
||||
if (!prev[docId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[docId]: {
|
||||
...prev[docId],
|
||||
visibilityRule: rule,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [currentProjectDeliverables, currentUser.id]);
|
||||
};
|
||||
|
||||
// 当用户切换或可见列表变动时,自动校准激活文档
|
||||
useEffect(() => {
|
||||
@@ -1209,6 +1288,7 @@ export default function App() {
|
||||
setActiveDocId(clonedDocs[0].docId);
|
||||
setActiveCategory('workflow');
|
||||
}}
|
||||
canCreateProject={canCreateProject}
|
||||
currentUser={currentUser}
|
||||
currentRole={currentDocRole}
|
||||
currentUserName={currentUser.name}
|
||||
@@ -1266,6 +1346,10 @@ export default function App() {
|
||||
onShareToProject={handleShareToProject}
|
||||
onRevokeShare={handleRevokeShare}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
onOpenDocVisibility={(doc) => {
|
||||
setTargetDocForVisibility(doc);
|
||||
setDocVisibilityModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 块状文档渲染器 */}
|
||||
@@ -1290,6 +1374,12 @@ export default function App() {
|
||||
ast={docAST}
|
||||
currentRole={currentDocRole}
|
||||
currentUserId={currentUser.id}
|
||||
canOverrideVisibility={canOverrideVisibility}
|
||||
onOpenVisibilityModal={() => {
|
||||
const currentDoc = deliverables.find((d) => d.docId === activeDocId) || null;
|
||||
setTargetDocForVisibility(currentDoc);
|
||||
setDocVisibilityModalOpen(true);
|
||||
}}
|
||||
onClaim={handleClaim}
|
||||
onSubmitReview={handleSubmitReview}
|
||||
onPassReview={handlePassReview}
|
||||
@@ -1331,16 +1421,46 @@ export default function App() {
|
||||
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
|
||||
deliverables={visibleDeliverables}
|
||||
activeDocId={activeDocId}
|
||||
allUsers={MOCK_USERS}
|
||||
onSwitchUser={(u) => setCurrentUser(u)}
|
||||
allUsers={allUsers}
|
||||
canAssignLead={canAssignLead}
|
||||
onSwitchUser={(u) => {
|
||||
const fresh = allUsers.find((x) => x.id === u.id) || u;
|
||||
setCurrentUser(fresh);
|
||||
}}
|
||||
onSelectDoc={handleSelectDoc}
|
||||
onRoleChangeForDoc={handleRoleChangeForDoc}
|
||||
/>
|
||||
|
||||
{/* 预留系统设置与管理员后台弹窗 */}
|
||||
{/* 系统权限与角色管理控制台 */}
|
||||
<SettingsModal
|
||||
isOpen={settingsModalOpen}
|
||||
onClose={() => setSettingsModalOpen(false)}
|
||||
roles={roles}
|
||||
onSaveRoles={(newRoles) => setRoles(newRoles)}
|
||||
users={allUsers}
|
||||
currentUser={currentUser}
|
||||
onUpdateUserRole={(userId, roleId) => {
|
||||
setAllUsers((prev) =>
|
||||
prev.map((u) => (u.id === userId ? { ...u, roleId } : u))
|
||||
);
|
||||
if (currentUser.id === userId) {
|
||||
setCurrentUser((prev) => ({ ...prev, roleId }));
|
||||
}
|
||||
}}
|
||||
onSwitchUser={(newUser) => {
|
||||
const fresh = allUsers.find((u) => u.id === newUser.id) || newUser;
|
||||
setCurrentUser(fresh);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 单文档可见性覆盖弹窗 */}
|
||||
<DocVisibilityModal
|
||||
isOpen={docVisibilityModalOpen}
|
||||
onClose={() => setDocVisibilityModalOpen(false)}
|
||||
doc={targetDocForVisibility}
|
||||
roles={roles}
|
||||
currentUserName={currentUser.name}
|
||||
onSaveRule={handleSaveDocVisibilityRule}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ interface AppShellProps {
|
||||
activeSessionId: string;
|
||||
onSelectSession: (id: string) => void;
|
||||
onNewSession: () => void;
|
||||
canCreateProject?: boolean;
|
||||
currentUser: UserProfile;
|
||||
currentRole: UserRole;
|
||||
currentUserName: string;
|
||||
@@ -27,6 +28,7 @@ export const AppShell: React.FC<AppShellProps> = ({
|
||||
activeSessionId,
|
||||
onSelectSession,
|
||||
onNewSession,
|
||||
canCreateProject = true,
|
||||
currentUser,
|
||||
currentRole,
|
||||
currentUserName: _currentUserName,
|
||||
@@ -111,6 +113,7 @@ export const AppShell: React.FC<AppShellProps> = ({
|
||||
activeSessionId={activeSessionId}
|
||||
onSelectSession={onSelectSession}
|
||||
onNewSession={onNewSession}
|
||||
canCreateProject={canCreateProject}
|
||||
currentUser={currentUser}
|
||||
currentRole={currentRole}
|
||||
onOpenRightsModal={onOpenRightsModal}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface LeftRailProps {
|
||||
activeSessionId: string;
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
onNewSession: () => void;
|
||||
canCreateProject?: boolean;
|
||||
currentUser: UserProfile;
|
||||
currentRole: UserRole;
|
||||
onOpenRightsModal: () => void;
|
||||
@@ -23,6 +24,7 @@ export const LeftRail: React.FC<LeftRailProps> = ({
|
||||
activeSessionId,
|
||||
onSelectSession,
|
||||
onNewSession,
|
||||
canCreateProject = true,
|
||||
currentUser,
|
||||
currentRole,
|
||||
onOpenRightsModal,
|
||||
@@ -65,22 +67,31 @@ export const LeftRail: React.FC<LeftRailProps> = ({
|
||||
{/* 顶部操作区 */}
|
||||
<div style={{ padding: '14px 12px 10px', borderBottom: '1px solid var(--border-light)' }}>
|
||||
<button
|
||||
onClick={onNewSession}
|
||||
onClick={canCreateProject ? onNewSession : undefined}
|
||||
disabled={!canCreateProject}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '36px',
|
||||
borderRadius: 'var(--r-md)',
|
||||
background: 'var(--brand-grad)',
|
||||
color: '#fff',
|
||||
background: canCreateProject ? 'var(--brand-grad)' : 'var(--bg-subtle)',
|
||||
color: canCreateProject ? '#fff' : 'var(--text-faint)',
|
||||
border: canCreateProject ? 'none' : '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
fontWeight: 600,
|
||||
fontSize: '13px',
|
||||
boxShadow: '0 2px 8px rgba(79, 107, 255, 0.28)',
|
||||
boxShadow: canCreateProject ? '0 2px 8px rgba(79, 107, 255, 0.28)' : 'none',
|
||||
cursor: canCreateProject ? 'pointer' : 'not-allowed',
|
||||
opacity: canCreateProject ? 1 : 0.6,
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
title={
|
||||
canCreateProject
|
||||
? '发起新研发项目与立项'
|
||||
: `当前角色 [${currentUser.position || '方案工程师'}] 无权发起新研发项目与立项`
|
||||
}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>新建立项会话</span>
|
||||
|
||||
@@ -18,6 +18,7 @@ interface UserProjectRightsModalProps {
|
||||
deliverables: DeliverableItem[];
|
||||
activeDocId: string;
|
||||
allUsers?: UserProfile[];
|
||||
canAssignLead?: boolean;
|
||||
onSwitchUser?: (user: UserProfile) => void;
|
||||
onSelectDoc: (docId: string) => void;
|
||||
onRoleChangeForDoc?: (docId: string, role: DocPermissionRole) => void;
|
||||
@@ -31,6 +32,7 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
deliverables,
|
||||
activeDocId,
|
||||
allUsers,
|
||||
canAssignLead = true,
|
||||
onSwitchUser,
|
||||
onSelectDoc,
|
||||
onRoleChangeForDoc,
|
||||
@@ -457,6 +459,7 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
{onRoleChangeForDoc ? (
|
||||
<select
|
||||
value={role}
|
||||
disabled={!canAssignLead}
|
||||
onChange={(e) => onRoleChangeForDoc(item.docId, e.target.value as DocPermissionRole)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
@@ -466,9 +469,14 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
color: badge.color,
|
||||
fontWeight: 500,
|
||||
fontSize: '11px',
|
||||
cursor: 'pointer',
|
||||
cursor: canAssignLead ? 'pointer' : 'not-allowed',
|
||||
opacity: canAssignLead ? 1 : 0.65,
|
||||
}}
|
||||
title="演练测试:支持切换该文档下的负责角色"
|
||||
title={
|
||||
canAssignLead
|
||||
? '演练测试:支持切换该文档下的负责角色'
|
||||
: '当前角色无权配置文档法定职责'
|
||||
}
|
||||
>
|
||||
<option value="author">✍️ 编制负责人</option>
|
||||
<option value="reviewer">🔍 审核负责人</option>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
DEFAULT_ROLES,
|
||||
hasPermission,
|
||||
isDeliverableVisible,
|
||||
filterVisibleDeliverablesWithRBAC,
|
||||
canPerformDocAction,
|
||||
} from './rbac';
|
||||
import { UserProfile } from '../types/docgov';
|
||||
import { DeliverableItem } from '../types/workbench';
|
||||
import { RoleDefinition } from '../types/rbac';
|
||||
|
||||
test('集成场景 1: 动态新增合规审计员角色,并配置其仅能查阅特定单文档覆盖', () => {
|
||||
// 1. 动态新增一个审计员角色
|
||||
const customAuditorRole: RoleDefinition = {
|
||||
id: 'role-auditor',
|
||||
name: '外部合规审计专家',
|
||||
code: 'AUDITOR',
|
||||
description: '第三方飞行检查专家,默认无内部工作流权限,仅可查阅指定公开或白名单文档',
|
||||
isSystem: false,
|
||||
permissions: ['doc:export'], // 默认无 workflow:view_all, 无 project:create, 无 project:assign_lead
|
||||
createdAt: '2026-09-08 15:00',
|
||||
updatedAt: '2026-09-08 15:00',
|
||||
};
|
||||
|
||||
const activeRoles: RoleDefinition[] = [...DEFAULT_ROLES, customAuditorRole];
|
||||
|
||||
// 2. 创建审计员用户
|
||||
const auditorUser: UserProfile = {
|
||||
id: 99,
|
||||
name: '钱审计',
|
||||
employeeId: 'AUDIT-001',
|
||||
department: '国家器审中心第三方合规组',
|
||||
position: '主任审核员',
|
||||
roleId: 'role-auditor',
|
||||
};
|
||||
|
||||
// 3. 准备一批项目交付物
|
||||
const projectDocs: DeliverableItem[] = [
|
||||
{
|
||||
docId: 'doc-wf-normal',
|
||||
projectId: 1,
|
||||
taskKey: 'feasibility',
|
||||
title: '常规内部立项可行性报告',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '2026-09-08 12:00',
|
||||
},
|
||||
{
|
||||
docId: 'doc-wf-audited',
|
||||
projectId: 1,
|
||||
taskKey: 'regulatory',
|
||||
title: '器械注册与临床评价白皮书 (开放审计)',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
lifecycleState: 'approving',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '2026-09-08 12:00',
|
||||
visibilityRule: {
|
||||
mode: 'role_whitelist',
|
||||
allowedRoleIds: ['role-director', 'role-auditor'],
|
||||
overrideReason: '飞行检查开放白名单',
|
||||
},
|
||||
},
|
||||
{
|
||||
docId: 'doc-private-zhang',
|
||||
projectId: 1,
|
||||
taskKey: 'draft',
|
||||
title: '张工敏感商业测算草案',
|
||||
stage: 'V0.1',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '',
|
||||
approver: '',
|
||||
updatedAt: '2026-09-08 12:00',
|
||||
},
|
||||
];
|
||||
|
||||
// 4. 验证钱审计视角下的可见交付物:
|
||||
// - doc-wf-normal: 无 workflow:view_all -> 不可见
|
||||
// - doc-private-zhang: private 且非创建者 -> 绝对不可见
|
||||
// - doc-wf-audited: 单文档覆盖 role_whitelist 包含 role-auditor -> 可见!
|
||||
const auditorVisible = filterVisibleDeliverablesWithRBAC(projectDocs, auditorUser, activeRoles);
|
||||
assert.equal(auditorVisible.length, 1);
|
||||
assert.equal(auditorVisible[0].docId, 'doc-wf-audited');
|
||||
|
||||
// 5. 验证操作权限拦截
|
||||
assert.equal(canPerformDocAction('create_project', auditorUser, activeRoles), false);
|
||||
assert.equal(canPerformDocAction('assign_lead', auditorUser, activeRoles), false);
|
||||
assert.equal(canPerformDocAction('override_visibility', auditorUser, activeRoles), false);
|
||||
});
|
||||
|
||||
test('集成场景 2: 动态修改角色权限矩阵,权限即刻全量生效', () => {
|
||||
// 1. 复制角色库
|
||||
let roles = [...DEFAULT_ROLES];
|
||||
const engineerRole = roles.find((r) => r.code === 'ENGINEER')!;
|
||||
assert.equal(engineerRole.permissions.includes('project:create'), false);
|
||||
|
||||
const zhangUser: UserProfile = {
|
||||
id: 1,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '微创中心',
|
||||
position: '资深方案工程师',
|
||||
roleId: 'role-engineer',
|
||||
};
|
||||
|
||||
// 初始时张工无权创建项目
|
||||
assert.equal(hasPermission(zhangUser, roles, 'project:create'), false);
|
||||
|
||||
// 2. 模拟管理员在 SettingsModal 中为 ENGINEER 勾选了 project:create 权限
|
||||
roles = roles.map((r) =>
|
||||
r.code === 'ENGINEER'
|
||||
? { ...r, permissions: [...r.permissions, 'project:create' as const] }
|
||||
: r
|
||||
);
|
||||
|
||||
// 3. 再次断言,权限立即响应式生效!
|
||||
assert.equal(hasPermission(zhangUser, roles, 'project:create'), true);
|
||||
});
|
||||
|
||||
test('集成场景 3: 单文档覆盖 designated_only 严格收紧与撤回恢复全流程', () => {
|
||||
const roles = DEFAULT_ROLES;
|
||||
|
||||
const zhangAuthor: UserProfile = {
|
||||
id: 1,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '微创中心',
|
||||
position: '资深方案工程师',
|
||||
roleId: 'role-engineer',
|
||||
};
|
||||
|
||||
const liReviewer: UserProfile = {
|
||||
id: 2,
|
||||
name: '李主管',
|
||||
employeeId: 'SMDF-0205',
|
||||
department: '微创中心',
|
||||
position: '技术主管',
|
||||
roleId: 'role-tech-lead',
|
||||
};
|
||||
|
||||
const sunOtherEngineer: UserProfile = {
|
||||
id: 8,
|
||||
name: '孙工',
|
||||
employeeId: 'SMDF-0999',
|
||||
department: '微创中心',
|
||||
position: '方案工程师',
|
||||
roleId: 'role-engineer',
|
||||
};
|
||||
|
||||
let targetDoc: DeliverableItem = {
|
||||
docId: 'doc-secret-001',
|
||||
projectId: 1,
|
||||
taskKey: 'feasibility',
|
||||
title: '涉密荧光滤光片设计图',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
lifecycleState: 'drafting',
|
||||
isClaimed: true,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '2026-09-08 16:00',
|
||||
};
|
||||
|
||||
// 步骤 A: 未设置覆盖时,同项目的孙工(具备 workflow:view_all)可以查阅
|
||||
assert.equal(isDeliverableVisible(sunOtherEngineer, roles, targetDoc), true);
|
||||
|
||||
// 步骤 B: 李主管配置单文档覆盖为 designated_only
|
||||
targetDoc = {
|
||||
...targetDoc,
|
||||
visibilityRule: {
|
||||
mode: 'designated_only',
|
||||
overrideReason: '核心专利技术交底,严格限制仅编制/审核/批准人可见',
|
||||
configuredBy: '李主管',
|
||||
configuredAt: '2026-09-08 16:05',
|
||||
},
|
||||
};
|
||||
|
||||
// 步骤 C: 重新判定
|
||||
// 张工(编制人)可见
|
||||
assert.equal(isDeliverableVisible(zhangAuthor, roles, targetDoc), true);
|
||||
// 李主管(审核人)可见
|
||||
assert.equal(isDeliverableVisible(liReviewer, roles, targetDoc), true);
|
||||
// 孙工(非责任人)被单文档覆盖精确拦截 -> 不可见!
|
||||
assert.equal(isDeliverableVisible(sunOtherEngineer, roles, targetDoc), false);
|
||||
|
||||
// 步骤 D: 撤回覆盖,恢复为 inherit
|
||||
targetDoc = {
|
||||
...targetDoc,
|
||||
visibilityRule: undefined,
|
||||
};
|
||||
|
||||
// 孙工再次可见
|
||||
assert.equal(isDeliverableVisible(sunOtherEngineer, roles, targetDoc), true);
|
||||
});
|
||||
Reference in New Issue
Block a user