From 32568ed13e01d36d897955c87315e573c9b012df Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 28 Sep 2026 15:36:39 +0000 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E9=A1=B9=E7=9B=AE=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=8F=AF=E8=A7=81=E6=9C=AC=E9=A1=B9=E7=9B=AE=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=8F=97=E6=8E=A7=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已打开项目的流程受控文档按成员基线展示。单文档指定人、角色白名单和私密隔离仍然优先。 --- web/src/App.tsx | 4 +++- web/src/engine/rbac.test.ts | 21 ++++++++++++++++++++- web/src/engine/rbac.ts | 19 ++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 1fa2b7dd..4b91b4a5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -472,7 +472,9 @@ export default function App() { // 基于当前登录用户与 RBAC 规则过滤可见交付物 (私密文档严格按 creatorId 隔离,单文档覆盖优先于工作流基线) const visibleDeliverables = useMemo(() => { - return filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles); + return filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles, { + projectMember: true, + }); }, [currentProjectDeliverables, currentUser, roles]); // 单文档可见性覆盖保存处理 diff --git a/web/src/engine/rbac.test.ts b/web/src/engine/rbac.test.ts index cdb9a49f..9152be5f 100644 --- a/web/src/engine/rbac.test.ts +++ b/web/src/engine/rbac.test.ts @@ -112,8 +112,21 @@ test('用例 3: 常规工作流基线可见性 (workflow:view_all) 判定', () = // 工程师有 workflow:view_all -> 可见 assert.equal(isDeliverableVisible(sampleUsers.engineer, DEFAULT_ROLES, normalDoc), true); - // 外部审计员无 workflow:view_all 且不是责任人 -> 不可见 + // 外部审计员无 workflow:view_all 且不是项目成员 -> 不可见 assert.equal(isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, normalDoc), false); + + // 同一访客作为项目成员打开本项目 -> 可见流程受控文档 + assert.equal( + isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, normalDoc, { projectMember: true }), + true, + ); + const memberVisible = filterVisibleDeliverablesWithRBAC( + [normalDoc], + sampleUsers.guest, + DEFAULT_ROLES, + { projectMember: true }, + ); + assert.equal(memberVisible.length, 1); }); test('用例 4: 单文档覆盖 - designated_only (仅编制/审核/批准人可见,其他用户被精确拦截)', () => { @@ -138,6 +151,12 @@ test('用例 4: 单文档覆盖 - designated_only (仅编制/审核/批准人可 // 王总监虽为研发总监,但非三权责任人 -> 被单文档覆盖拦截 (不可见) assert.equal(isDeliverableVisible(sampleUsers.director, DEFAULT_ROLES, restrictedDoc), false); + // 项目成员身份不覆盖「仅责任人」收紧 + assert.equal( + isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, restrictedDoc, { projectMember: true }), + false, + ); + // 系统管理员超级权限仍可见 assert.equal(isDeliverableVisible(sampleUsers.admin, DEFAULT_ROLES, restrictedDoc), true); }); diff --git a/web/src/engine/rbac.ts b/web/src/engine/rbac.ts index a3aab7f0..17d98da9 100644 --- a/web/src/engine/rbac.ts +++ b/web/src/engine/rbac.ts @@ -108,12 +108,14 @@ export function hasPermission( * - designated_only: 仅责任人可见 (非责任人已在步骤4排除,直接拒绝) * - role_whitelist: 仅白名单内角色可见 * - public: 全员可见 - * 6. 回退继承工作流基线:用户角色需具备 workflow:view_all 权限 + * 6. 项目成员:已打开的项目内,流程受控文档对 active 成员可见(非成员不传 projectMember) + * 7. 回退继承工作流基线:用户角色需具备 workflow:view_all 权限 */ export function isDeliverableVisible( user: UserProfile, roles: RoleDefinition[], - doc: DeliverableItem + doc: DeliverableItem, + options?: { projectMember?: boolean }, ): boolean { // 步骤 1:个人私密文档 (private) 绝对隔离 if (doc.category === 'private') { @@ -166,7 +168,13 @@ export function isDeliverableVisible( } } - // 步骤 6:回退继承工作流默认基线规则(文档可见性仍用角色矩阵作 UI 展示兜底) + // 步骤 6:项目成员可见本项目流程受控文档。 + // 非成员仍回退到 workflow:view_all;单文档覆盖(步骤 5)优先于成员基线。 + if (doc.category === 'workflow' && options?.projectMember) { + return true; + } + + // 步骤 7:回退继承工作流默认基线规则(文档可见性仍用角色矩阵作 UI 展示兜底) return hasPermission(user, roles, 'workflow:view_all', capabilitiesFromRole(roles, user.roleId)); } @@ -176,9 +184,10 @@ export function isDeliverableVisible( export function filterVisibleDeliverablesWithRBAC( deliverables: DeliverableItem[], user: UserProfile, - roles: RoleDefinition[] + roles: RoleDefinition[], + options?: { projectMember?: boolean }, ): DeliverableItem[] { - return deliverables.filter((doc) => isDeliverableVisible(user, roles, doc)); + return deliverables.filter((doc) => isDeliverableVisible(user, roles, doc, options)); } /**