mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
Restrict @ mention and session query ACL so admin/user only see owned or workspace sessions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e48b8bd082
commit
e2c2e949ed
5 changed files with 394 additions and 110 deletions
|
|
@ -41,7 +41,7 @@ originSystemCode: ''
|
|||
- `POST /uds-auth/outbound` — **loopback**:白名单出站并注入鉴权头
|
||||
- 用户管理 / 兜底管理员:见 `/uds-auth/api/users*`、`/uds-auth/api/fallback/*`
|
||||
- **默认兜底账号**(扫码不可用时):用户名 `administrator`,密码 `Admin@123`(首次启动自动启用;可在设置中改密或关闭)
|
||||
- **ACL**:租户边界以工作区为准(可见工作区下的会话可访问);`session-owners.json` 仅记录工号供导出/分析,不是主鉴权键
|
||||
- **ACL**:`super_admin` / 兜底 `administrator` 可见全部会话(含 `@` 提及);`admin` / `user` 仅可见 **自己拥有的** 或 **自己工作区路径下的** 会话。侧栏、`session/search` 与 `@` 候选共用同一规则
|
||||
|
||||
## Skill 认证(给他人改造 skill 时)
|
||||
|
||||
|
|
|
|||
|
|
@ -370,6 +370,114 @@ function throwForbidden(code) {
|
|||
throw err
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared session visibility helpers (sidebar + @ mention + query reads).
|
||||
* Visibility: super_admin / fallback_admin see all; others see owner OR own workspace.
|
||||
*/
|
||||
export function createSessionAccess({
|
||||
sessionAcl,
|
||||
userWorkspaces,
|
||||
getWorkspaceRoot,
|
||||
getWorkspaceRegistry,
|
||||
resolveLiveCwd,
|
||||
}) {
|
||||
const canSeeAll = (identity) => {
|
||||
if (!identity) return false
|
||||
if (identity.permissions?.canViewAllSessions) return true
|
||||
const role = identity.role || identity.userContext?.role
|
||||
if (role === 'fallback_admin' || role === 'super_admin') return true
|
||||
if (String(identity.empNo || identity.userContext?.empNo || '') === 'administrator') return true
|
||||
return false
|
||||
}
|
||||
|
||||
const empOf = (identity) => identity?.empNo || identity?.userContext?.empNo || null
|
||||
|
||||
const resolveRegistry = () => {
|
||||
try {
|
||||
if (typeof getWorkspaceRegistry === 'function') {
|
||||
const r = getWorkspaceRegistry()
|
||||
if (r) return r
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return null
|
||||
}
|
||||
|
||||
const resolveSessionCwd = (sessionId, rowHint) => {
|
||||
if (rowHint?.cwd) return String(rowHint.cwd)
|
||||
if (rowHint?.header?.cwd) return String(rowHint.header.cwd)
|
||||
if (typeof resolveLiveCwd === 'function') {
|
||||
try {
|
||||
const cwd = resolveLiveCwd(sessionId)
|
||||
if (cwd) return String(cwd)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const workspaceContainsSession = (ws, sessionId) => {
|
||||
const sid = String(sessionId)
|
||||
try {
|
||||
const ids = ws?.sessionIds
|
||||
if (ids && typeof ids[Symbol.iterator] === 'function') {
|
||||
for (const id of ids) {
|
||||
if (String(id) === sid) return true
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
const raw = ws?.record?.sessionIds
|
||||
if (Array.isArray(raw) && raw.some((id) => String(id) === sid)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const isVisibleWorkspace = (identity, ws) => {
|
||||
if (canSeeAll(identity)) return true
|
||||
const empNo = empOf(identity)
|
||||
if (!empNo || !ws) return false
|
||||
const root = getWorkspaceRoot()
|
||||
const wid = ws.id ?? ws.workspaceId
|
||||
const path = ws.path
|
||||
return userWorkspaces.isUserPath(empNo, path, root)
|
||||
|| (userWorkspaces.get(empNo)?.workspaceId
|
||||
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner stamp OR cwd/workspace under the caller's provisioned path.
|
||||
* Missing owner alone does not deny (legacy sessions rely on workspace/cwd).
|
||||
*/
|
||||
const canAccessSession = (sessionId, identity, rowHint) => {
|
||||
if (!identity || !empOf(identity)) return false
|
||||
if (canSeeAll(identity)) return true
|
||||
if (sessionId == null) return false
|
||||
const empNo = empOf(identity)
|
||||
const owner = sessionAcl?.getOwner?.(sessionId) || null
|
||||
if (owner && String(owner) === String(empNo)) return true
|
||||
|
||||
const root = getWorkspaceRoot()
|
||||
const cwd = resolveSessionCwd(sessionId, rowHint)
|
||||
if (cwd && userWorkspaces.isUserPath(empNo, 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
|
||||
if (workspaceContainsSession(ws, sessionId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
canSeeAll,
|
||||
empOf,
|
||||
resolveRegistry,
|
||||
resolveSessionCwd,
|
||||
isVisibleWorkspace,
|
||||
canAccessSession,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Host ACL wrappers.
|
||||
*/
|
||||
|
|
@ -383,6 +491,37 @@ export function installDshAcl(ctx, {
|
|||
}) {
|
||||
const disposers = []
|
||||
|
||||
const access = createSessionAccess({
|
||||
sessionAcl,
|
||||
userWorkspaces,
|
||||
getWorkspaceRoot,
|
||||
getWorkspaceRegistry: () => {
|
||||
try {
|
||||
if (typeof getWorkspaceRegistry === 'function') {
|
||||
const r = getWorkspaceRegistry()
|
||||
if (r) return r
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
try { return ctx.get('workspaceRegistry') } catch { return null }
|
||||
},
|
||||
resolveLiveCwd: (sessionId) => {
|
||||
try {
|
||||
const agents = ctx.get('agents')
|
||||
const agent = agents?.get?.(sessionId)
|
||||
return agent?.session?.header?.cwd || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
})
|
||||
const {
|
||||
canSeeAll,
|
||||
empOf,
|
||||
resolveRegistry,
|
||||
isVisibleWorkspace,
|
||||
canAccessSession,
|
||||
} = access
|
||||
|
||||
// Stamp owner on session create
|
||||
const offCreated = ctx.on('session/created', (session) => {
|
||||
try {
|
||||
|
|
@ -397,114 +536,161 @@ export function installDshAcl(ctx, {
|
|||
})
|
||||
disposers.push(() => offCreated?.())
|
||||
|
||||
const extractSessionId = (request) => {
|
||||
if (!request || typeof request !== 'object') return null
|
||||
return request.address?.sessionId
|
||||
?? request.sessionId
|
||||
?? request.id
|
||||
?? request.childSessionId
|
||||
?? null
|
||||
}
|
||||
|
||||
// Browser HTTP always enters ALS via withUserContext(null|identity).
|
||||
// In-process Host callers (WhatsApp/IM, cron fire) never enter ALS → undefined.
|
||||
// Treat undefined as host-internal and skip UDS ACL (pre-auth behavior).
|
||||
const assertCanAccess = (request, rowHint) => {
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return
|
||||
if (!empOf(identity)) throwForbidden('login_required_session')
|
||||
if (canSeeAll(identity)) return
|
||||
const sessionId = extractSessionId(request)
|
||||
if (!canAccessSession(sessionId, identity, rowHint)) {
|
||||
throwForbidden('session_forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
const assertSessionReadable = (sessionId, rowHint) => {
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return
|
||||
if (!empOf(identity)) throwForbidden('login_required_session')
|
||||
if (canSeeAll(identity)) return
|
||||
if (!canAccessSession(sessionId, identity, rowHint)) {
|
||||
throwForbidden('session_forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
const filterSessionRecords = (records, identity) => (records || []).filter((row) => {
|
||||
const id = row?.header?.id ?? row?.sessionId ?? row?.id
|
||||
return id != null && canAccessSession(id, identity, {
|
||||
cwd: row?.header?.cwd ?? row?.cwd,
|
||||
header: row?.header,
|
||||
})
|
||||
})
|
||||
|
||||
// @ mention discovery (SessionReferenceResolver) lists via sessionQuery.listSessions,
|
||||
// bypassing sessionController ACL — filter the corpus here.
|
||||
ctx.inject(['sessionQuery'], (qctx) => {
|
||||
const sq = qctx.sessionQuery
|
||||
if (!sq || sq.__udsAcl) return
|
||||
sq.__udsAcl = true
|
||||
|
||||
if (typeof sq.listSessions === 'function') {
|
||||
const origList = sq.listSessions.bind(sq)
|
||||
sq.listSessions = async (signal) => {
|
||||
const records = await origList(signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return records
|
||||
if (!empOf(identity)) return []
|
||||
if (canSeeAll(identity)) return records
|
||||
return filterSessionRecords(records, identity)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof sq.filterSessions === 'function') {
|
||||
const origFilter = sq.filterSessions.bind(sq)
|
||||
sq.filterSessions = async (filters, signal) => {
|
||||
const records = await origFilter(filters, signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return records
|
||||
if (!empOf(identity)) return []
|
||||
if (canSeeAll(identity)) return records
|
||||
return filterSessionRecords(records, identity)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof sq.searchSessions === 'function') {
|
||||
const origSearch = sq.searchSessions.bind(sq)
|
||||
sq.searchSessions = async (request, exec) => {
|
||||
const page = await origSearch(request, exec)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return page
|
||||
if (!empOf(identity)) {
|
||||
return page && typeof page === 'object'
|
||||
? { ...page, hits: [], items: [] }
|
||||
: page
|
||||
}
|
||||
if (canSeeAll(identity)) return page
|
||||
if (!page || typeof page !== 'object') return page
|
||||
const filterHits = (hits) => (hits || []).filter((hit) => {
|
||||
const id = hit?.sessionId ?? hit?.header?.id ?? hit?.id
|
||||
return id != null && canAccessSession(id, identity, {
|
||||
cwd: hit?.cwd ?? hit?.header?.cwd,
|
||||
header: hit?.header,
|
||||
})
|
||||
})
|
||||
return {
|
||||
...page,
|
||||
...(Array.isArray(page.hits) ? { hits: filterHits(page.hits) } : {}),
|
||||
...(Array.isArray(page.items) ? { items: filterHits(page.items) } : {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const method of ['readSession', 'readSurface', 'readTitle', 'readTitleSnapshot', 'listEvents', 'observeSession']) {
|
||||
if (typeof sq[method] !== 'function') continue
|
||||
const orig = sq[method].bind(sq)
|
||||
sq[method] = async (sessionId, ...rest) => {
|
||||
assertSessionReadable(sessionId)
|
||||
return orig(sessionId, ...rest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Belt-and-suspenders: filter @ candidates even if listSessions wrap order changes.
|
||||
ctx.inject(['sessionReferenceResolver'], (rctx) => {
|
||||
const resolver = rctx.sessionReferenceResolver
|
||||
if (!resolver || resolver.__udsAcl) return
|
||||
resolver.__udsAcl = true
|
||||
|
||||
if (typeof resolver.listCandidates === 'function') {
|
||||
const origList = resolver.listCandidates.bind(resolver)
|
||||
resolver.listCandidates = async (agent, query, limit, signal) => {
|
||||
const candidates = await origList(agent, query, limit, signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return candidates
|
||||
if (!empOf(identity)) return []
|
||||
if (canSeeAll(identity)) return candidates
|
||||
return (candidates || []).filter((c) => canAccessSession(c?.sessionId, identity, {
|
||||
cwd: c?.cwd,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof resolver.prepare === 'function') {
|
||||
const origPrepare = resolver.prepare.bind(resolver)
|
||||
resolver.prepare = async (agent, content, references, signal) => {
|
||||
const identity = getUserContext()
|
||||
if (identity !== undefined) {
|
||||
if (!empOf(identity)) throwForbidden('login_required_session')
|
||||
if (!canSeeAll(identity)) {
|
||||
for (const ref of references || []) {
|
||||
const sid = ref?.sessionId
|
||||
if (sid != null && !canAccessSession(sid, identity)) {
|
||||
throwForbidden('session_forbidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return origPrepare(agent, content, references, signal)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ctx.inject(['sessionController'], (sctx) => {
|
||||
const sc = sctx.sessionController
|
||||
if (!sc || sc.__udsAcl) return
|
||||
sc.__udsAcl = true
|
||||
|
||||
const canSeeAll = (identity) => {
|
||||
if (!identity) return false
|
||||
if (identity.permissions?.canViewAllSessions) return true
|
||||
const role = identity.role || identity.userContext?.role
|
||||
if (role === 'fallback_admin' || role === 'super_admin') return true
|
||||
if (String(identity.empNo || identity.userContext?.empNo || '') === 'administrator') return true
|
||||
return false
|
||||
}
|
||||
|
||||
const empOf = (identity) => identity?.empNo || identity?.userContext?.empNo || null
|
||||
|
||||
const extractSessionId = (request) => {
|
||||
if (!request || typeof request !== 'object') return null
|
||||
return request.address?.sessionId
|
||||
?? request.sessionId
|
||||
?? request.id
|
||||
?? request.childSessionId
|
||||
?? null
|
||||
}
|
||||
|
||||
const resolveRegistry = () => {
|
||||
try {
|
||||
if (typeof getWorkspaceRegistry === 'function') {
|
||||
const r = getWorkspaceRegistry()
|
||||
if (r) return r
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
try { return ctx.get('workspaceRegistry') } catch { return null }
|
||||
}
|
||||
|
||||
const resolveSessionCwd = (sessionId, rowHint) => {
|
||||
if (rowHint?.cwd) return String(rowHint.cwd)
|
||||
try {
|
||||
const agents = ctx.get('agents')
|
||||
const agent = agents?.get?.(sessionId)
|
||||
const cwd = agent?.session?.header?.cwd
|
||||
if (cwd) return String(cwd)
|
||||
} catch { /* ignore */ }
|
||||
return null
|
||||
}
|
||||
|
||||
const workspaceContainsSession = (ws, sessionId) => {
|
||||
const sid = String(sessionId)
|
||||
try {
|
||||
const ids = ws?.sessionIds
|
||||
if (ids && typeof ids[Symbol.iterator] === 'function') {
|
||||
for (const id of ids) {
|
||||
if (String(id) === sid) return true
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
const raw = ws?.record?.sessionIds
|
||||
if (Array.isArray(raw) && raw.some((id) => String(id) === sid)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const isVisibleWorkspace = (identity, ws) => {
|
||||
if (canSeeAll(identity)) return true
|
||||
const empNo = empOf(identity)
|
||||
if (!empNo || !ws) return false
|
||||
const root = getWorkspaceRoot()
|
||||
const wid = ws.id ?? ws.workspaceId
|
||||
const path = ws.path
|
||||
return userWorkspaces.isUserPath(empNo, path, root)
|
||||
|| (userWorkspaces.get(empNo)?.workspaceId
|
||||
&& String(userWorkspaces.get(empNo).workspaceId) === String(wid))
|
||||
}
|
||||
|
||||
/** Workspace-first access: visible workspace membership or cwd under user path. */
|
||||
const canAccessSession = (sessionId, identity, rowHint) => {
|
||||
if (!identity || !empOf(identity)) return false
|
||||
if (canSeeAll(identity)) return true
|
||||
if (sessionId == null) return false
|
||||
const empNo = empOf(identity)
|
||||
const root = getWorkspaceRoot()
|
||||
const cwd = resolveSessionCwd(sessionId, rowHint)
|
||||
if (cwd && userWorkspaces.isUserPath(empNo, 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
|
||||
if (workspaceContainsSession(ws, sessionId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Browser HTTP always enters ALS via withUserContext(null|identity).
|
||||
// In-process Host callers (WhatsApp/IM, cron fire) never enter ALS → undefined.
|
||||
// Treat undefined as host-internal and skip UDS ACL (pre-auth behavior).
|
||||
const assertCanAccess = (request, rowHint) => {
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return
|
||||
if (!empOf(identity)) throwForbidden('login_required_session')
|
||||
if (canSeeAll(identity)) return
|
||||
const sessionId = extractSessionId(request)
|
||||
if (!canAccessSession(sessionId, identity, rowHint)) {
|
||||
throwForbidden('session_forbidden')
|
||||
}
|
||||
}
|
||||
|
||||
const assertCreateTargetAllowed = async (req, identity) => {
|
||||
if (canSeeAll(identity) || identity.permissions?.canCreateWorkspace) return
|
||||
const empNo = empOf(identity)
|
||||
|
|
@ -536,6 +722,7 @@ export function installDshAcl(ctx, {
|
|||
sc.listState.list = async (signal) => {
|
||||
const items = await origStateList(signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return items
|
||||
if (!empOf(identity)) return []
|
||||
if (canSeeAll(identity)) return items
|
||||
return filterItems(items, identity)
|
||||
|
|
@ -545,6 +732,7 @@ export function installDshAcl(ctx, {
|
|||
sc.listState.search = async (query, signal) => {
|
||||
const value = await origStateSearch(query, signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return value
|
||||
if (!empOf(identity)) return { items: [], hasMore: false }
|
||||
if (canSeeAll(identity)) return value
|
||||
return sessionAcl.filterListValue(value, (id, row) => canAccessSession(id, identity, row))
|
||||
|
|
@ -556,6 +744,7 @@ export function installDshAcl(ctx, {
|
|||
sc.list = async (request, signal) => {
|
||||
const value = await origList(request, signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return value
|
||||
if (!empOf(identity)) return { items: [] }
|
||||
if (canSeeAll(identity)) return value
|
||||
return sessionAcl.filterListValue(value, (id, row) => canAccessSession(id, identity, row))
|
||||
|
|
@ -565,6 +754,7 @@ export function installDshAcl(ctx, {
|
|||
sc.search = async (request, signal) => {
|
||||
const value = await origSearch(request, signal)
|
||||
const identity = getUserContext()
|
||||
if (identity === undefined) return value
|
||||
if (!empOf(identity)) return { items: [], hasMore: false }
|
||||
if (canSeeAll(identity)) return value
|
||||
return sessionAcl.filterListValue(value, (id, row) => canAccessSession(id, identity, row))
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
* uds-auth 角色存储 + 权限管理
|
||||
*
|
||||
* 角色:
|
||||
* super_admin 所有权限 + 用户管理
|
||||
* admin 设置权限 + 仅看自己会话
|
||||
* user 仅看自己会话,无设置
|
||||
* super_admin 所有权限 + 用户管理 + 可见全部会话(含 @)
|
||||
* fallback_admin 等同 super_admin(兜底 administrator)
|
||||
* admin 设置权限 + 仅看自己会话(含 @)
|
||||
* user 仅看自己会话,无设置
|
||||
*
|
||||
* 持久化: roles.json (单实例文件) + MemoryStore 同步
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Session ownership audit store — sessionId → empNo.
|
||||
* Session ownership store — sessionId → empNo.
|
||||
*
|
||||
* Used for export / analytics. Tenant isolation is workspace-based
|
||||
* (see dsh-acl.js); missing owner must NOT deny access by itself.
|
||||
* Used with workspace/cwd checks in dsh-acl.js (owner OR own workspace).
|
||||
* Missing owner must NOT deny access by itself (legacy sessions).
|
||||
*/
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
|
|
|||
93
uds-auth/test/session-access.test.js
Normal file
93
uds-auth/test/session-access.test.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createSessionAccess } from '../lib/dsh-acl.js'
|
||||
|
||||
function makeAccess(owners = {}) {
|
||||
const ownersMap = new Map(Object.entries(owners))
|
||||
const userPaths = new Map([
|
||||
['u1', { path: '/ws/u1', workspaceId: 'ws-u1' }],
|
||||
['u2', { path: '/ws/u2', workspaceId: 'ws-u2' }],
|
||||
])
|
||||
return createSessionAccess({
|
||||
sessionAcl: {
|
||||
getOwner(id) {
|
||||
return ownersMap.get(String(id)) || null
|
||||
},
|
||||
},
|
||||
userWorkspaces: {
|
||||
get(empNo) {
|
||||
return userPaths.get(String(empNo)) || null
|
||||
},
|
||||
isUserPath(empNo, candidate, root) {
|
||||
const row = userPaths.get(String(empNo))
|
||||
if (!row?.path || !candidate) return false
|
||||
const base = String(row.path).replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
const path = String(candidate).replace(/\\/g, '/')
|
||||
return path === base || path.startsWith(base + '/')
|
||||
},
|
||||
},
|
||||
getWorkspaceRoot: () => '/ws',
|
||||
getWorkspaceRegistry: () => ({
|
||||
list: () => ([
|
||||
{ id: 'ws-u1', path: '/ws/u1', sessionIds: ['s-in-u1'] },
|
||||
{ id: 'ws-u2', path: '/ws/u2', sessionIds: ['s-in-u2'] },
|
||||
{ id: 'ws-shared', path: '/ws/shared', sessionIds: ['s-shared'] },
|
||||
]),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
describe('createSessionAccess', () => {
|
||||
it('super_admin and administrator see all sessions', () => {
|
||||
const { canAccessSession, canSeeAll } = makeAccess({ 's-other': 'u2' })
|
||||
const superAdmin = {
|
||||
empNo: 'boss',
|
||||
role: 'super_admin',
|
||||
permissions: { canViewAllSessions: true },
|
||||
}
|
||||
const fallback = {
|
||||
empNo: 'administrator',
|
||||
role: 'fallback_admin',
|
||||
permissions: { canViewAllSessions: true },
|
||||
}
|
||||
assert.equal(canSeeAll(superAdmin), true)
|
||||
assert.equal(canSeeAll(fallback), true)
|
||||
assert.equal(canAccessSession('s-other', superAdmin), true)
|
||||
assert.equal(canAccessSession('s-other', fallback), true)
|
||||
})
|
||||
|
||||
it('admin and user only see owned or own-workspace sessions', () => {
|
||||
const { canAccessSession, canSeeAll } = makeAccess({
|
||||
's-owned': 'u1',
|
||||
's-peer': 'u2',
|
||||
})
|
||||
const admin = {
|
||||
empNo: 'u1',
|
||||
role: 'admin',
|
||||
permissions: { canViewAllSessions: false, canAccessSettings: true },
|
||||
}
|
||||
const user = {
|
||||
empNo: 'u1',
|
||||
role: 'user',
|
||||
permissions: { canViewAllSessions: false },
|
||||
}
|
||||
assert.equal(canSeeAll(admin), false)
|
||||
assert.equal(canSeeAll(user), false)
|
||||
|
||||
assert.equal(canAccessSession('s-owned', admin), true)
|
||||
assert.equal(canAccessSession('s-in-u1', user), true)
|
||||
assert.equal(canAccessSession('s-cwd', user, { cwd: '/ws/u1/project' }), true)
|
||||
|
||||
assert.equal(canAccessSession('s-peer', admin), false)
|
||||
assert.equal(canAccessSession('s-in-u2', user), false)
|
||||
assert.equal(canAccessSession('s-shared', user), false)
|
||||
assert.equal(canAccessSession('s-cwd-peer', user, { cwd: '/ws/u2/x' }), false)
|
||||
})
|
||||
|
||||
it('missing owner does not deny when cwd is under user path', () => {
|
||||
const { canAccessSession } = makeAccess({})
|
||||
const user = { empNo: 'u1', role: 'user', permissions: { canViewAllSessions: false } }
|
||||
assert.equal(canAccessSession('legacy', user, { cwd: '/ws/u1' }), true)
|
||||
assert.equal(canAccessSession('legacy-other', user, { cwd: '/ws/u2' }), false)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue