mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
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 <cursoragent@cursor.com>
This commit is contained in:
parent
7c899942fb
commit
ef57e05942
5 changed files with 55 additions and 61 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue