From ef57e05942af32e11807147fd5f5724cc86b1bc8 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 16 Sep 2026 23:52:02 +0800 Subject: [PATCH] Honor view-all off: only own sessions and personal workspace. Shared project and channel sessions were still visible via canViewSystemSessions when the toggle was off; tighten ACL and copy so off means own only. Co-authored-by: Cursor --- uds-auth/lib/client.js | 4 +- uds-auth/lib/dsh-acl.js | 40 +++-------------- uds-auth/lib/i18n.js | 4 +- uds-auth/package.json | 2 +- uds-auth/test/session-access.test.js | 66 ++++++++++++++++++---------- 5 files changed, 55 insertions(+), 61 deletions(-) diff --git a/uds-auth/lib/client.js b/uds-auth/lib/client.js index 76651851..6c27c4cc 100644 --- a/uds-auth/lib/client.js +++ b/uds-auth/lib/client.js @@ -67,7 +67,7 @@ window.__ModuleLoader__.load({ "ui.add": "添加", "ui.department": "部门", "ui.viewAllSessionsTitle": "查看全部会话", - "ui.viewAllSessionsIntro": "超级管理员默认可见全部会话(含渠道)。关闭后仅看自己的;侧栏与 @ 提及规则相同。", + "ui.viewAllSessionsIntro": "默认可见全部会话(含渠道与共享工作区)。关闭后仅看自己名下的会话与个人工作区。", "ui.viewAllSessionsToggle": "显示所有人的会话", "ui.viewAllSessionsOn": "已开启:可见全部会话", "ui.viewAllSessionsOff": "已关闭:仅可见自己的会话", @@ -207,7 +207,7 @@ window.__ModuleLoader__.load({ "ui.add": "Add", "ui.department": "Department", "ui.viewAllSessionsTitle": "View all sessions", - "ui.viewAllSessionsIntro": "Super admins see all sessions by default (including channels). Turn off to only see your own; sidebar and @ mentions share the same rule.", + "ui.viewAllSessionsIntro": "All sessions are visible by default (including channels and shared workspaces). Turn off to only see sessions you own and your personal workspace.", "ui.viewAllSessionsToggle": "Show everyone’s sessions", "ui.viewAllSessionsOn": "On: all sessions visible", "ui.viewAllSessionsOff": "Off: only your own sessions", diff --git a/uds-auth/lib/dsh-acl.js b/uds-auth/lib/dsh-acl.js index 0672688b..bfb8596b 100644 --- a/uds-auth/lib/dsh-acl.js +++ b/uds-auth/lib/dsh-acl.js @@ -465,21 +465,20 @@ export function createSessionAccess({ const root = getWorkspaceRoot() const wid = ws.id ?? ws.workspaceId const path = ws.path + // View-all off: only the caller's provisioned user workspace — not shared + // project folders (chatgpt/harness/…) or channel roots. Those stay under view-all. if (userWorkspaces.isUserPath(empNo, path, root) || (userWorkspaces.get(empNo)?.workspaceId && String(userWorkspaces.get(empNo).workspaceId) === String(wid))) { return true } - // Channel / bot / shared harness workspaces live outside user-workspaces. - if (identity.permissions?.canViewSystemSessions && isOutsideUserWorkspaceRoot(path, root)) { - return true - } return false } /** - * Owner stamp OR cwd/workspace under the caller's provisioned path. - * Settings roles also see unowned system/channel sessions (cwd outside user-workspaces). + * Owner stamp OR cwd under the caller's provisioned path. + * View-all off means only those — no "unowned outside user-workspaces" leak + * (shared project sessions were incorrectly treated as channel/system). */ const canAccessSession = (sessionId, identity, rowHint) => { if (!identity || !empOf(identity)) return false @@ -495,21 +494,13 @@ export function createSessionAccess({ const foreignOwner = !!(owner && String(owner) !== String(empNo)) - // IM/channel (and other host-internal) sessions: often unstamped + bot cwd. - // Let admin+ see those; never leak another user's stamped private session. - if (!foreignOwner && !owner && identity.permissions?.canViewSystemSessions - && isOutsideUserWorkspaceRoot(cwd, root)) { - return true - } - const registry = resolveRegistry() if (!registry || typeof registry.list !== 'function') return false let workspaces = [] try { workspaces = registry.list() || [] } catch { return false } for (const ws of workspaces) { if (!isVisibleWorkspace(identity, ws)) continue - // Foreign-owned sessions must not become visible via channel/system workspaces. - if (foreignOwner && isOutsideUserWorkspaceRoot(ws?.path, root)) continue + if (foreignOwner) continue if (workspaceContainsSession(ws, sessionId)) return true } return false @@ -963,10 +954,7 @@ ctx.inject(['workspaceController'], (wctx) => { && String(userWorkspaces.get(empNo).workspaceId) === String(wid))) { return true } - if (identity.permissions?.canViewSystemSessions - && isOutsideUserWorkspaceRoot(ws?.path, root)) { - return true - } + // View-all off: hide shared/channel workspaces from the sidebar partitions. return false } @@ -992,20 +980,6 @@ ctx.inject(['workspaceController'], (wctx) => { const allowed = new Set() const mapped = userWorkspaces.get(empNo)?.workspaceId if (mapped != null) allowed.add(String(mapped)) - // Keep channel/bot workspaces for admin+ (same rule as allowWorkspace). - if (identity?.permissions?.canViewSystemSessions) { - try { - const root = getWorkspaceRoot() - const registry = resolveRegistry() - const list = typeof registry?.list === 'function' ? (registry.list() || []) : [] - for (const ws of list) { - const id = ws?.id ?? ws?.workspaceId - if (id != null && isOutsideUserWorkspaceRoot(ws?.path, root)) { - allowed.add(String(id)) - } - } - } catch { /* ignore */ } - } return { ...frame, workspaceIds: (frame.workspaceIds || []).filter((id) => allowed.has(String(id))), diff --git a/uds-auth/lib/i18n.js b/uds-auth/lib/i18n.js index a52a2445..d695c250 100644 --- a/uds-auth/lib/i18n.js +++ b/uds-auth/lib/i18n.js @@ -62,7 +62,7 @@ export const MESSAGES = { // privacy / session visibility 'ui.viewAllSessionsTitle': '查看全部会话', - 'ui.viewAllSessionsIntro': '超级管理员默认可见全部会话(含渠道)。关闭后仅看自己的;侧栏与 @ 提及规则相同。', + 'ui.viewAllSessionsIntro': '默认可见全部会话(含渠道与共享工作区)。关闭后仅看自己名下的会话与个人工作区。', 'ui.viewAllSessionsToggle': '显示所有人的会话', 'ui.viewAllSessionsOn': '已开启:可见全部会话', 'ui.viewAllSessionsOff': '已关闭:仅可见自己的会话', @@ -214,7 +214,7 @@ export const MESSAGES = { 'ui.department': 'Department', 'ui.viewAllSessionsTitle': 'View all sessions', - 'ui.viewAllSessionsIntro': 'Super admins see all sessions by default (including channels). Turn off to only see your own; sidebar and @ mentions share the same rule.', + 'ui.viewAllSessionsIntro': 'All sessions are visible by default (including channels and shared workspaces). Turn off to only see sessions you own and your personal workspace.', 'ui.viewAllSessionsToggle': 'Show everyone’s sessions', 'ui.viewAllSessionsOn': 'On: all sessions visible', 'ui.viewAllSessionsOff': 'Off: only your own sessions', diff --git a/uds-auth/package.json b/uds-auth/package.json index 48476f25..32a422bb 100644 --- a/uds-auth/package.json +++ b/uds-auth/package.json @@ -1,6 +1,6 @@ { "name": "uds-auth", - "version": "0.2.12", + "version": "0.2.13", "description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation", "type": "module", "main": "lib/index.js", diff --git a/uds-auth/test/session-access.test.js b/uds-auth/test/session-access.test.js index ccb36fa0..2f457bdb 100644 --- a/uds-auth/test/session-access.test.js +++ b/uds-auth/test/session-access.test.js @@ -119,17 +119,57 @@ describe('createSessionAccess', () => { assert.equal(canAccessSession('legacy-other', user, { cwd: '/ws/u2' }), false) }) - it('admin can see unowned channel/system sessions outside user-workspaces', () => { + it('view-all off: admin only sees own sessions, not channel/shared workspaces', () => { const store = new RolesStore() store._roles.set('u1', ROLES.ADMIN) store.setViewAllSessions('u1', false) const accessOff = createSessionAccess({ sessionAcl: { getOwner: () => null }, userWorkspaces: { - get: () => null, - isUserPath: () => false, + get: (empNo) => (empNo === 'u1' ? { path: '/ws/u1', workspaceId: 'ws-u1' } : null), + isUserPath: (empNo, candidate) => { + if (empNo !== 'u1' || !candidate) return false + const path = String(candidate).replace(/\\/g, '/') + return path === '/ws/u1' || path.startsWith('/ws/u1/') + }, }, getWorkspaceRoot: () => '/ws', + getWorkspaceRegistry: () => ({ + list: () => ([ + { id: 'ws-u1', path: '/ws/u1', sessionIds: ['mine'] }, + { id: 'bot-ws', path: '/bots/whatsapp', sessionIds: ['ch-1'] }, + { id: 'shared', path: '/project/chatgpt', sessionIds: ['peer'] }, + ]), + }), + rolesStore: store, + }) + const admin = { + empNo: 'u1', + role: 'admin', + permissions: computePermissions('admin', { viewAllSessions: false }), + } + assert.equal(accessOff.canSeeAll(admin), false) + assert.equal(accessOff.canAccessSession('mine', admin, { cwd: '/ws/u1/a' }), true) + assert.equal(accessOff.canAccessSession('ch-1', admin, { cwd: '/bots/whatsapp' }), false) + assert.equal(accessOff.canAccessSession('peer', admin, { cwd: '/project/chatgpt' }), false) + assert.equal(accessOff.isVisibleWorkspace(admin, { id: 'ws-u1', path: '/ws/u1' }), true) + assert.equal(accessOff.isVisibleWorkspace(admin, { id: 'bot-ws', path: '/bots/whatsapp' }), false) + assert.equal(accessOff.isVisibleWorkspace(admin, { id: 'shared', path: '/project/chatgpt' }), false) + + store.setViewAllSessions('u1', true) + assert.equal(accessOff.canAccessSession('ch-1', admin, { cwd: '/bots/whatsapp' }), true) + assert.equal(accessOff.canAccessSession('peer', admin, { cwd: '/project/chatgpt' }), true) + }) + + it('view-all off: foreign-owned session stays hidden even in shared workspace', () => { + const store = new RolesStore() + store._roles.set('u1', ROLES.ADMIN) + store.setViewAllSessions('u1', false) + const ownersMap = new Map([['ch-owned', 'u2']]) + const accessOwned = createSessionAccess({ + sessionAcl: { getOwner: (id) => ownersMap.get(String(id)) || null }, + userWorkspaces: { get: () => null, isUserPath: () => false }, + getWorkspaceRoot: () => '/ws', getWorkspaceRegistry: () => ({ list: () => [] }), rolesStore: store, }) @@ -138,27 +178,7 @@ describe('createSessionAccess', () => { role: 'admin', permissions: computePermissions('admin', { viewAllSessions: false }), } - const user = { - empNo: 'u1', - role: 'user', - permissions: computePermissions('user'), - } - assert.equal(accessOff.canAccessSession('ch-1', admin, { cwd: '/bots/whatsapp' }), true) - assert.equal(accessOff.canAccessSession('ch-1', user, { cwd: '/bots/whatsapp' }), false) - assert.equal(accessOff.canAccessSession('s-peer', admin, { cwd: '/ws/u2/x' }), false) - - const ownersMap = new Map([['ch-owned', 'u2']]) - const accessOwned = createSessionAccess({ - sessionAcl: { getOwner: (id) => ownersMap.get(String(id)) || null }, - userWorkspaces: { get: () => null, isUserPath: () => false }, - getWorkspaceRoot: () => '/ws', - getWorkspaceRegistry: () => ({ list: () => [] }), - rolesStore: store, - }) assert.equal(accessOwned.canAccessSession('ch-owned', admin, { cwd: '/bots/wa' }), false) - - assert.equal(accessOff.isVisibleWorkspace(admin, { id: 'bot-ws', path: '/bots/whatsapp' }), true) - assert.equal(accessOff.isVisibleWorkspace(user, { id: 'bot-ws', path: '/bots/whatsapp' }), false) }) it('live rolesStore prefs override stale identity.permissions', () => {