mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 21:40:49 +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.add": "添加",
|
||||||
"ui.department": "部门",
|
"ui.department": "部门",
|
||||||
"ui.viewAllSessionsTitle": "查看全部会话",
|
"ui.viewAllSessionsTitle": "查看全部会话",
|
||||||
"ui.viewAllSessionsIntro": "超级管理员默认可见全部会话(含渠道)。关闭后仅看自己的;侧栏与 @ 提及规则相同。",
|
"ui.viewAllSessionsIntro": "默认可见全部会话(含渠道与共享工作区)。关闭后仅看自己名下的会话与个人工作区。",
|
||||||
"ui.viewAllSessionsToggle": "显示所有人的会话",
|
"ui.viewAllSessionsToggle": "显示所有人的会话",
|
||||||
"ui.viewAllSessionsOn": "已开启:可见全部会话",
|
"ui.viewAllSessionsOn": "已开启:可见全部会话",
|
||||||
"ui.viewAllSessionsOff": "已关闭:仅可见自己的会话",
|
"ui.viewAllSessionsOff": "已关闭:仅可见自己的会话",
|
||||||
|
|
@ -207,7 +207,7 @@ window.__ModuleLoader__.load({
|
||||||
"ui.add": "Add",
|
"ui.add": "Add",
|
||||||
"ui.department": "Department",
|
"ui.department": "Department",
|
||||||
"ui.viewAllSessionsTitle": "View all sessions",
|
"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.viewAllSessionsToggle": "Show everyone’s sessions",
|
||||||
"ui.viewAllSessionsOn": "On: all sessions visible",
|
"ui.viewAllSessionsOn": "On: all sessions visible",
|
||||||
"ui.viewAllSessionsOff": "Off: only your own sessions",
|
"ui.viewAllSessionsOff": "Off: only your own sessions",
|
||||||
|
|
|
||||||
|
|
@ -465,21 +465,20 @@ export function createSessionAccess({
|
||||||
const root = getWorkspaceRoot()
|
const root = getWorkspaceRoot()
|
||||||
const wid = ws.id ?? ws.workspaceId
|
const wid = ws.id ?? ws.workspaceId
|
||||||
const path = ws.path
|
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)
|
if (userWorkspaces.isUserPath(empNo, path, root)
|
||||||
|| (userWorkspaces.get(empNo)?.workspaceId
|
|| (userWorkspaces.get(empNo)?.workspaceId
|
||||||
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))) {
|
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Channel / bot / shared harness workspaces live outside user-workspaces.
|
|
||||||
if (identity.permissions?.canViewSystemSessions && isOutsideUserWorkspaceRoot(path, root)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owner stamp OR cwd/workspace under the caller's provisioned path.
|
* Owner stamp OR cwd under the caller's provisioned path.
|
||||||
* Settings roles also see unowned system/channel sessions (cwd outside user-workspaces).
|
* 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) => {
|
const canAccessSession = (sessionId, identity, rowHint) => {
|
||||||
if (!identity || !empOf(identity)) return false
|
if (!identity || !empOf(identity)) return false
|
||||||
|
|
@ -495,21 +494,13 @@ export function createSessionAccess({
|
||||||
|
|
||||||
const foreignOwner = !!(owner && String(owner) !== String(empNo))
|
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()
|
const registry = resolveRegistry()
|
||||||
if (!registry || typeof registry.list !== 'function') return false
|
if (!registry || typeof registry.list !== 'function') return false
|
||||||
let workspaces = []
|
let workspaces = []
|
||||||
try { workspaces = registry.list() || [] } catch { return false }
|
try { workspaces = registry.list() || [] } catch { return false }
|
||||||
for (const ws of workspaces) {
|
for (const ws of workspaces) {
|
||||||
if (!isVisibleWorkspace(identity, ws)) continue
|
if (!isVisibleWorkspace(identity, ws)) continue
|
||||||
// Foreign-owned sessions must not become visible via channel/system workspaces.
|
if (foreignOwner) continue
|
||||||
if (foreignOwner && isOutsideUserWorkspaceRoot(ws?.path, root)) continue
|
|
||||||
if (workspaceContainsSession(ws, sessionId)) return true
|
if (workspaceContainsSession(ws, sessionId)) return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -963,10 +954,7 @@ ctx.inject(['workspaceController'], (wctx) => {
|
||||||
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))) {
|
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (identity.permissions?.canViewSystemSessions
|
// View-all off: hide shared/channel workspaces from the sidebar partitions.
|
||||||
&& isOutsideUserWorkspaceRoot(ws?.path, root)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -992,20 +980,6 @@ ctx.inject(['workspaceController'], (wctx) => {
|
||||||
const allowed = new Set()
|
const allowed = new Set()
|
||||||
const mapped = userWorkspaces.get(empNo)?.workspaceId
|
const mapped = userWorkspaces.get(empNo)?.workspaceId
|
||||||
if (mapped != null) allowed.add(String(mapped))
|
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 {
|
return {
|
||||||
...frame,
|
...frame,
|
||||||
workspaceIds: (frame.workspaceIds || []).filter((id) => allowed.has(String(id))),
|
workspaceIds: (frame.workspaceIds || []).filter((id) => allowed.has(String(id))),
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const MESSAGES = {
|
||||||
|
|
||||||
// privacy / session visibility
|
// privacy / session visibility
|
||||||
'ui.viewAllSessionsTitle': '查看全部会话',
|
'ui.viewAllSessionsTitle': '查看全部会话',
|
||||||
'ui.viewAllSessionsIntro': '超级管理员默认可见全部会话(含渠道)。关闭后仅看自己的;侧栏与 @ 提及规则相同。',
|
'ui.viewAllSessionsIntro': '默认可见全部会话(含渠道与共享工作区)。关闭后仅看自己名下的会话与个人工作区。',
|
||||||
'ui.viewAllSessionsToggle': '显示所有人的会话',
|
'ui.viewAllSessionsToggle': '显示所有人的会话',
|
||||||
'ui.viewAllSessionsOn': '已开启:可见全部会话',
|
'ui.viewAllSessionsOn': '已开启:可见全部会话',
|
||||||
'ui.viewAllSessionsOff': '已关闭:仅可见自己的会话',
|
'ui.viewAllSessionsOff': '已关闭:仅可见自己的会话',
|
||||||
|
|
@ -214,7 +214,7 @@ export const MESSAGES = {
|
||||||
'ui.department': 'Department',
|
'ui.department': 'Department',
|
||||||
|
|
||||||
'ui.viewAllSessionsTitle': 'View all sessions',
|
'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.viewAllSessionsToggle': 'Show everyone’s sessions',
|
||||||
'ui.viewAllSessionsOn': 'On: all sessions visible',
|
'ui.viewAllSessionsOn': 'On: all sessions visible',
|
||||||
'ui.viewAllSessionsOff': 'Off: only your own sessions',
|
'ui.viewAllSessionsOff': 'Off: only your own sessions',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uds-auth",
|
"name": "uds-auth",
|
||||||
"version": "0.2.12",
|
"version": "0.2.13",
|
||||||
"description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation",
|
"description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
|
|
||||||
|
|
@ -119,17 +119,57 @@ describe('createSessionAccess', () => {
|
||||||
assert.equal(canAccessSession('legacy-other', user, { cwd: '/ws/u2' }), false)
|
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()
|
const store = new RolesStore()
|
||||||
store._roles.set('u1', ROLES.ADMIN)
|
store._roles.set('u1', ROLES.ADMIN)
|
||||||
store.setViewAllSessions('u1', false)
|
store.setViewAllSessions('u1', false)
|
||||||
const accessOff = createSessionAccess({
|
const accessOff = createSessionAccess({
|
||||||
sessionAcl: { getOwner: () => null },
|
sessionAcl: { getOwner: () => null },
|
||||||
userWorkspaces: {
|
userWorkspaces: {
|
||||||
get: () => null,
|
get: (empNo) => (empNo === 'u1' ? { path: '/ws/u1', workspaceId: 'ws-u1' } : null),
|
||||||
isUserPath: () => false,
|
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',
|
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: () => [] }),
|
getWorkspaceRegistry: () => ({ list: () => [] }),
|
||||||
rolesStore: store,
|
rolesStore: store,
|
||||||
})
|
})
|
||||||
|
|
@ -138,27 +178,7 @@ describe('createSessionAccess', () => {
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
permissions: computePermissions('admin', { viewAllSessions: false }),
|
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(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', () => {
|
it('live rolesStore prefs override stale identity.permissions', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue