mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
Unify admin-class workspace create and restore Add-workspace UI.
Give admin the same permissions as super/fallback, occupy directoryFlow at priority 1 so the button renders without colliding with the native picker, and inject uiWorkspace for pickDirectory. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
060500360d
commit
91d2515205
9 changed files with 211 additions and 146 deletions
6
uds-auth/.gitignore
vendored
6
uds-auth/.gitignore
vendored
|
|
@ -7,5 +7,11 @@ config.runtime.json
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
session-owners.json
|
||||
session-owners.json.bak*
|
||||
user-workspaces.json
|
||||
skill-credentials.json
|
||||
|
||||
# Local one-off probes / scratch (do not commit)
|
||||
scripts/find-dsh-process.ps1
|
||||
scripts/inspect-session-sample.cjs
|
||||
scripts/merge-sessions-into-workspace.ps1
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
|
|||
const { useCallback, useEffect, useLayoutEffect, useRef, useState } = React
|
||||
|
||||
const name = 'uds-auth'
|
||||
const inject = ['slots', 'locale']
|
||||
const inject = ['slots', 'locale', 'uiWorkspace']
|
||||
const PAGE_SIZE = 50
|
||||
const LOCALE_NS = 'uds-auth'
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ window.__ModuleLoader__.load({
|
|||
"ui.settingsTitle": "UAC 认证",
|
||||
"ui.settingsIntro": "工号+token 双校验;UAC 挂死时用应急账号 administrator 密码登录。",
|
||||
"ui.loginRequiredPage": "请先登录后查看此页",
|
||||
"ui.roleHint": "当前角色:{role}。首位扫码登录且 roles.json 为空时会自动成为超管;普通 admin 需超管在「用户管理」提权后再扫码登录。应急账号 administrator 需超管先设密码,再在登录面板用账密登录。",
|
||||
"ui.roleHint": "当前角色:{role}。超管只是身份标签,与管理员权限相同;首位扫码登录且 roles.json 为空时自动获得超管身份,也可在「用户管理」设为管理员。应急账号 administrator 需先设密码,再在登录面板用账密登录。",
|
||||
"ui.deployConfig": "部署配置",
|
||||
"ui.userSearchUrl": "用户搜索 URL(token 校验)",
|
||||
"ui.workspaceRoot": "工作区根目录(空=$DSH_HOME/user-workspaces)",
|
||||
|
|
@ -173,7 +173,7 @@ window.__ModuleLoader__.load({
|
|||
"ui.settingsTitle": "UAC Auth",
|
||||
"ui.settingsIntro": "EmpNo + token verification; when UAC is down, sign in with emergency account administrator.",
|
||||
"ui.loginRequiredPage": "Sign in to view this page",
|
||||
"ui.roleHint": "Current role: {role}. The first QR login with an empty roles.json becomes super admin; grant admin in User management then re-scan. Set the emergency password before using administrator on the login panel.",
|
||||
"ui.roleHint": "Current role: {role}. Super admin is only an identity label with the same permissions as admin; the first QR login with an empty roles.json gets that identity, or grant admin in User management. Set the emergency password before using administrator on the login panel.",
|
||||
"ui.deployConfig": "Deploy config",
|
||||
"ui.userSearchUrl": "User search URL (token verify)",
|
||||
"ui.workspaceRoot": "Workspace root (empty=$DSH_HOME/user-workspaces)",
|
||||
|
|
@ -1793,17 +1793,71 @@ function reloadAfterLogin() {
|
|||
|
||||
|
||||
|
||||
// Fallback directoryFlow at priority 1 (NOT 0).
|
||||
// Single-slot cells collide only on the exact priority; the shipped
|
||||
// native/browse picker owns 0 ("lowest renders"). We occupy priority 1 so
|
||||
// entries(hole).length > 0 → Add workspace button renders even when the
|
||||
// official picker failed to mount, without crashing Loader.
|
||||
ctx.effect(() => {
|
||||
let disposers = []
|
||||
const Gate = function UdsAuthDirectoryFlowGate(props) {
|
||||
React.useEffect(() => {
|
||||
if (props && props.open) {
|
||||
try { props.onCancel && props.onCancel() } catch { /* ignore */ }
|
||||
const Flow = function UdsAuthDirectoryFlow(props) {
|
||||
const open = !!(props && props.open)
|
||||
const armed = useRef(false)
|
||||
const outcome = useRef(props)
|
||||
outcome.current = props
|
||||
const alive = useRef(true)
|
||||
useEffect(() => {
|
||||
alive.current = true
|
||||
return () => { alive.current = false }
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
armed.current = false
|
||||
return
|
||||
}
|
||||
}, [props && props.open])
|
||||
if (armed.current) return
|
||||
armed.current = true
|
||||
const can = document.documentElement.getAttribute('data-uds-can-create-ws') === '1'
|
||||
if (!can) {
|
||||
try { outcome.current.onCancel && outcome.current.onCancel() } catch { /* ignore */ }
|
||||
return
|
||||
}
|
||||
let pickPromise
|
||||
try {
|
||||
const ui = ctx.uiWorkspace
|
||||
if (ui && typeof ui.pickDirectory === 'function') {
|
||||
pickPromise = ui.pickDirectory()
|
||||
} else {
|
||||
pickPromise = Promise.reject(new Error('directory picker unavailable'))
|
||||
}
|
||||
} catch (err) {
|
||||
pickPromise = Promise.reject(err)
|
||||
}
|
||||
pickPromise.then(
|
||||
(path) => {
|
||||
if (!alive.current) return
|
||||
if (path == null) {
|
||||
try { outcome.current.onCancel && outcome.current.onCancel() } catch { /* ignore */ }
|
||||
} else {
|
||||
try { outcome.current.onPicked && outcome.current.onPicked(path) } catch { /* ignore */ }
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!alive.current) return
|
||||
const msg = reason instanceof Error ? reason.message : String(reason)
|
||||
try { outcome.current.onError && outcome.current.onError(msg) } catch { /* ignore */ }
|
||||
},
|
||||
)
|
||||
}, [open])
|
||||
return null
|
||||
}
|
||||
const installGate = () => {
|
||||
|
||||
const disposers = []
|
||||
const clear = () => {
|
||||
for (const d of disposers.splice(0)) {
|
||||
try { d() } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
const install = () => {
|
||||
if (disposers.length) return
|
||||
for (const slotName of [
|
||||
'sidebar.workspaces.directoryFlow',
|
||||
|
|
@ -1812,61 +1866,37 @@ function reloadAfterLogin() {
|
|||
try {
|
||||
disposers.push(ctx.slots.register({
|
||||
name: slotName,
|
||||
id: 'uds-auth-dir-gate',
|
||||
order: 9999,
|
||||
}, Gate))
|
||||
} catch { /* slot may be undeclared briefly */ }
|
||||
id: 'uds-auth-dir-flow',
|
||||
// Must differ from shipped picker priority 0.
|
||||
priority: 1,
|
||||
}, Flow))
|
||||
} catch { /* slot undeclared, or priority already taken — ignore */ }
|
||||
}
|
||||
}
|
||||
const clearGate = () => {
|
||||
for (const d of disposers) {
|
||||
try { d() } catch { /* ignore */ }
|
||||
}
|
||||
disposers = []
|
||||
}
|
||||
// Only shadow directoryFlow while anonymous. Logged-in users (including
|
||||
// non-creators) keep the native/browse occupant so "Add workspace" still
|
||||
// renders; CSS + RPC + Host ACL deny create for users without permission.
|
||||
// Never location.reload() here — remount attr flips caused infinite refresh.
|
||||
const sync = () => {
|
||||
const loggedIn = document.documentElement.getAttribute('data-uds-logged-in') === '1'
|
||||
if (loggedIn) {
|
||||
clearGate()
|
||||
return
|
||||
}
|
||||
installGate()
|
||||
}
|
||||
|
||||
const injectOffs = [
|
||||
'sidebar.workspaces.directoryFlow',
|
||||
'conversation.hero.workspace.directoryFlow',
|
||||
].map((slotName) => {
|
||||
try {
|
||||
return ctx.slots.inject(slotName, () => { sync() })
|
||||
return ctx.slots.inject(slotName, () => { install() })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
sync()
|
||||
const onAuth = () => { sync() }
|
||||
window.addEventListener('uds-auth-changed', onAuth)
|
||||
const mo = new MutationObserver(sync)
|
||||
mo.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-uds-can-create-ws', 'data-uds-logged-in', 'data-uds-auth-ready'],
|
||||
})
|
||||
install()
|
||||
return () => {
|
||||
window.removeEventListener('uds-auth-changed', onAuth)
|
||||
mo.disconnect()
|
||||
clearGate()
|
||||
clear()
|
||||
for (const off of injectOffs) {
|
||||
try { if (typeof off === 'function') off() } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}, 'uds-auth: directory-flow-gate')
|
||||
}, 'uds-auth: directory-flow-fallback')
|
||||
|
||||
// Anonymous / non-creators: CSS (data-uds-can-create-ws) + click lock + Host ACL.
|
||||
|
||||
ctx.effect(() => {
|
||||
// Open/choose workspace: admin / super_admin / fallback_admin (canCreateWorkspace).
|
||||
// Open/choose workspace: admin-class (canCreateWorkspace).
|
||||
// Everyone else uses the auto-provisioned per-user workspace and must not open the picker.
|
||||
const CHOOSER = hostAriaSel('chooseWorkspace')
|
||||
// Inert composer: onClick lives on the card (cardWorkspaceTrigger), not the labeled node.
|
||||
|
|
|
|||
|
|
@ -411,9 +411,13 @@ export function createSessionAccess({
|
|||
return !!store.resolvePermissions(empNo).canViewAllSessions
|
||||
}
|
||||
if (identity.permissions?.canViewAllSessions) return true
|
||||
// No store (tests / misconfig): keep legacy super visibility.
|
||||
// No store (tests / misconfig): admin-class sees all by default.
|
||||
// If permissions were supplied and view-all is off, respect that.
|
||||
if (identity.permissions && Object.prototype.hasOwnProperty.call(identity.permissions, 'canViewAllSessions')) {
|
||||
return !!identity.permissions.canViewAllSessions
|
||||
}
|
||||
const role = identity.role || identity.userContext?.role
|
||||
return role === 'super_admin' || role === 'fallback_admin'
|
||||
return role === 'super_admin' || role === 'fallback_admin' || role === 'admin'
|
||||
|| String(empNo) === 'administrator'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const MESSAGES = {
|
|||
'ui.settingsTitle': 'UAC 认证',
|
||||
'ui.settingsIntro': '工号+token 双校验;UAC 挂死时用应急账号 administrator 密码登录。',
|
||||
'ui.loginRequiredPage': '请先登录后查看此页',
|
||||
'ui.roleHint': '当前角色:{role}。首位扫码登录且 roles.json 为空时会自动成为超管;普通 admin 需超管在「用户管理」提权后再扫码登录。应急账号 administrator 需超管先设密码,再在登录面板用账密登录。',
|
||||
'ui.roleHint': '当前角色:{role}。超管只是身份标签,与管理员权限相同;首位扫码登录且 roles.json 为空时自动获得超管身份,也可在「用户管理」设为管理员。应急账号 administrator 需先设密码,再在登录面板用账密登录。',
|
||||
'ui.deployConfig': '部署配置',
|
||||
'ui.userSearchUrl': '用户搜索 URL(token 校验)',
|
||||
'ui.workspaceRoot': '工作区根目录(空=$DSH_HOME/user-workspaces)',
|
||||
|
|
@ -179,7 +179,7 @@ export const MESSAGES = {
|
|||
'ui.settingsTitle': 'UAC Auth',
|
||||
'ui.settingsIntro': 'EmpNo + token verification; when UAC is down, sign in with emergency account administrator.',
|
||||
'ui.loginRequiredPage': 'Sign in to view this page',
|
||||
'ui.roleHint': 'Current role: {role}. The first QR login with an empty roles.json becomes super admin; grant admin in User management then re-scan. Set the emergency password before using administrator on the login panel.',
|
||||
'ui.roleHint': 'Current role: {role}. Super admin is only an identity label with the same permissions as admin; the first QR login with an empty roles.json gets that identity, or grant admin in User management. Set the emergency password before using administrator on the login panel.',
|
||||
'ui.deployConfig': 'Deploy config',
|
||||
'ui.userSearchUrl': 'User search URL (token verify)',
|
||||
'ui.workspaceRoot': 'Workspace root (empty=$DSH_HOME/user-workspaces)',
|
||||
|
|
|
|||
|
|
@ -219,8 +219,11 @@ export function createAuthMiddleware(config, sessionStore, rolesStore, hooks = {
|
|||
|
||||
export function requirePermission(ctx, permission) {
|
||||
if (!ctx?.permissions) return false
|
||||
// Legacy alias: "super_admin" means admin-class identity (超管 / 应急 / 管理员).
|
||||
if (permission === 'super_admin') {
|
||||
return ctx.role === ROLES.SUPER_ADMIN || ctx.role === ROLES.FALLBACK_ADMIN
|
||||
return ctx.role === ROLES.SUPER_ADMIN
|
||||
|| ctx.role === ROLES.FALLBACK_ADMIN
|
||||
|| ctx.role === ROLES.ADMIN
|
||||
}
|
||||
return !!ctx.permissions[permission]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
/**
|
||||
* uds-auth 角色存储 + 权限管理
|
||||
*
|
||||
* 角色:
|
||||
* super_admin 所有权限 + 用户管理;默认可见全部会话(可在设置中关闭)
|
||||
* fallback_admin 等同 super_admin(兜底 administrator)
|
||||
* admin 无设置齿轮;仅看自己会话(含 @);可见渠道/系统会话;可创建工作区
|
||||
* user 仅看自己会话,无设置
|
||||
* 角色(身份标签;admin 级权限相同):
|
||||
* super_admin 身份:首位扫码 bootstrap / 显式提权;权限 = admin 级
|
||||
* fallback_admin 身份:应急账号 administrator;权限 = admin 级
|
||||
* admin 身份:用户管理中提权;权限 = admin 级
|
||||
* user 仅看自己会话,无设置 / 不可建工作区
|
||||
*
|
||||
* 超管/应急默认全览开启;prefs.viewAllSessions === false 时关闭。
|
||||
* 设置齿轮仅超管/应急(canAccessSettings)。
|
||||
* 创建工作区:super_admin / fallback_admin / admin(扫码不可用时现场通常只有 admin)。
|
||||
* admin 级(isAdminClass)权限相同:用户管理、设置、建工作区、默认可看全部会话。
|
||||
* 超管只是身份;默认可持有该身份的包括 admin,以及首位扫码加入者(bootstrap)。
|
||||
* prefs.viewAllSessions === false 时关闭全览。
|
||||
* 持久化: roles.json (roles + prefs + fallbackPasswordHash)
|
||||
*/
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
|
|
@ -33,58 +33,44 @@ export const ROLES = {
|
|||
/** zh labels for list/search; UI should translate via i18n role.* keys. */
|
||||
export const ROLE_LABELS = ROLE_LABELS_ZH
|
||||
|
||||
/** admin 级身份:超管 / 应急 / 管理员(权限相同,仅身份标签不同) */
|
||||
export function isAdminClass(role) {
|
||||
return role === ROLES.SUPER_ADMIN
|
||||
|| role === ROLES.FALLBACK_ADMIN
|
||||
|| role === ROLES.ADMIN
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算角色权限 (纯函数)
|
||||
* @param {string} role
|
||||
* @param {{ viewAllSessions?: boolean }} [opts] 个人偏好;超管默认可见全部
|
||||
* @param {{ viewAllSessions?: boolean }} [opts] 个人偏好;admin 级默认可见全部
|
||||
*/
|
||||
export function computePermissions(role, opts = {}) {
|
||||
const viewAll = !!opts.viewAllSessions
|
||||
switch (role) {
|
||||
case ROLES.SUPER_ADMIN:
|
||||
return {
|
||||
canManageUsers: true,
|
||||
canAccessSettings: true,
|
||||
canToggleViewAllSessions: true,
|
||||
canViewAllSessions: viewAll,
|
||||
canViewSystemSessions: true,
|
||||
canCreateWorkspace: true,
|
||||
}
|
||||
case ROLES.FALLBACK_ADMIN:
|
||||
return {
|
||||
canManageUsers: true,
|
||||
canAccessSettings: true,
|
||||
canToggleViewAllSessions: true,
|
||||
canViewAllSessions: viewAll,
|
||||
canViewSystemSessions: true,
|
||||
canCreateWorkspace: true,
|
||||
}
|
||||
case ROLES.ADMIN:
|
||||
return {
|
||||
canManageUsers: false,
|
||||
// 设置齿轮仅超管/应急;admin 仍可看渠道/系统会话,并可创建工作区
|
||||
canAccessSettings: false,
|
||||
canToggleViewAllSessions: false,
|
||||
canViewAllSessions: false,
|
||||
canViewSystemSessions: true,
|
||||
canCreateWorkspace: true,
|
||||
}
|
||||
default: // user / undefined
|
||||
return {
|
||||
canManageUsers: false,
|
||||
canAccessSettings: false,
|
||||
canToggleViewAllSessions: false,
|
||||
canViewAllSessions: false,
|
||||
canViewSystemSessions: false,
|
||||
canCreateWorkspace: false,
|
||||
}
|
||||
if (isAdminClass(role)) {
|
||||
return {
|
||||
canManageUsers: true,
|
||||
canAccessSettings: true,
|
||||
canToggleViewAllSessions: true,
|
||||
canViewAllSessions: viewAll,
|
||||
canViewSystemSessions: true,
|
||||
canCreateWorkspace: true,
|
||||
}
|
||||
}
|
||||
// user / undefined
|
||||
return {
|
||||
canManageUsers: false,
|
||||
canAccessSettings: false,
|
||||
canToggleViewAllSessions: false,
|
||||
canViewAllSessions: false,
|
||||
canViewSystemSessions: false,
|
||||
canCreateWorkspace: false,
|
||||
}
|
||||
}
|
||||
|
||||
/** 角色是否允许开启「查看全部会话」(仅超管 / 应急) */
|
||||
/** 角色是否允许开启「查看全部会话」(admin 级) */
|
||||
export function canToggleViewAllSessions(role) {
|
||||
return role === ROLES.SUPER_ADMIN
|
||||
|| role === ROLES.FALLBACK_ADMIN
|
||||
return isAdminClass(role)
|
||||
}
|
||||
|
||||
function hashPassword(password) {
|
||||
|
|
@ -226,8 +212,8 @@ export class RolesStore {
|
|||
}
|
||||
|
||||
/**
|
||||
* 个人偏好:超管/应急默认开启查看全部;显式 false 才关闭。
|
||||
* admin/user 不会走到这里(resolvePermissions 里 allowToggle=false)。
|
||||
* 个人偏好:admin 级默认开启查看全部;显式 false 才关闭。
|
||||
* user 不会走到这里(resolvePermissions 里 allowToggle=false)。
|
||||
*/
|
||||
isViewAllSessionsEnabled(empNo) {
|
||||
if (!empNo) return false
|
||||
|
|
@ -318,18 +304,18 @@ export class RolesStore {
|
|||
return n
|
||||
}
|
||||
|
||||
// === 角色管理 (super_admin only) ===
|
||||
// === 角色管理 (admin 级) ===
|
||||
|
||||
/**
|
||||
* 设置用户角色
|
||||
* 保护性 invariant: 至少保留 1 个 super_admin
|
||||
* 保护性 invariant: 至少保留 1 个 super_admin 身份(若表中曾有)
|
||||
*/
|
||||
async setRole(empNo, newRole, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
if (!isAdminClass(currentAdminRole)) {
|
||||
throw codedError('forbidden_set_role')
|
||||
}
|
||||
|
||||
// invariant: 不能让系统变成 0 个 super_admin
|
||||
// invariant: 不能让系统变成 0 个 super_admin(身份仍保留)
|
||||
const currentRole = this._roles.get(empNo)
|
||||
if (currentRole === ROLES.SUPER_ADMIN && newRole !== ROLES.SUPER_ADMIN) {
|
||||
const superAdmins = await this.countByRole(ROLES.SUPER_ADMIN)
|
||||
|
|
@ -345,7 +331,7 @@ export class RolesStore {
|
|||
|
||||
/** 删除用户 */
|
||||
async removeUser(empNo, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
if (!isAdminClass(currentAdminRole)) {
|
||||
throw codedError('forbidden_remove_user')
|
||||
}
|
||||
const currentRole = this._roles.get(empNo)
|
||||
|
|
@ -363,7 +349,7 @@ export class RolesStore {
|
|||
|
||||
/** 确保用户存在 (如果不存在设为 user) */
|
||||
ensureUser(empNo, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
if (!isAdminClass(currentAdminRole)) {
|
||||
throw codedError('forbidden_add_user')
|
||||
}
|
||||
if (!this._roles.has(empNo)) {
|
||||
|
|
@ -376,7 +362,7 @@ export class RolesStore {
|
|||
// === Fallback Administrator ===
|
||||
|
||||
setFallbackPassword(password, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN && currentAdminRole !== ROLES.FALLBACK_ADMIN) {
|
||||
if (!isAdminClass(currentAdminRole)) {
|
||||
throw codedError('forbidden_set_fallback')
|
||||
}
|
||||
if (!password || password.length < 6) {
|
||||
|
|
@ -388,7 +374,7 @@ export class RolesStore {
|
|||
}
|
||||
|
||||
clearFallbackPassword(currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN && currentAdminRole !== ROLES.FALLBACK_ADMIN) {
|
||||
if (!isAdminClass(currentAdminRole)) {
|
||||
throw codedError('forbidden_clear_fallback')
|
||||
}
|
||||
this._fallbackPasswordHash = null
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ export function identityFromAls(rolesStore) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @deprecated prefer isAdminClass — 超管只是身份,与 admin / 应急同权 */
|
||||
export function isSuperLike(role) {
|
||||
return role === ROLES.SUPER_ADMIN || role === ROLES.FALLBACK_ADMIN
|
||||
return role === ROLES.SUPER_ADMIN
|
||||
|| role === ROLES.FALLBACK_ADMIN
|
||||
|| role === ROLES.ADMIN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "uds-auth",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.11",
|
||||
"description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation",
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
|
|
@ -65,7 +65,8 @@
|
|||
"immediately": true,
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-locale"
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-workspace"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ describe('createSessionAccess', () => {
|
|||
assert.equal(canAccessSession('s-other', superAdmin), false)
|
||||
})
|
||||
|
||||
it('admin and user only see owned or own-workspace sessions by default', () => {
|
||||
it('admin-class sees all by default; user only owned or own-workspace sessions', () => {
|
||||
const { canAccessSession, canSeeAll } = makeAccess({
|
||||
's-owned': 'u1',
|
||||
's-peer': 'u2',
|
||||
|
|
@ -68,39 +68,48 @@ describe('createSessionAccess', () => {
|
|||
const admin = {
|
||||
empNo: 'u1',
|
||||
role: 'admin',
|
||||
permissions: computePermissions('admin'),
|
||||
permissions: computePermissions('admin', { viewAllSessions: true }),
|
||||
}
|
||||
const user = {
|
||||
empNo: 'u1',
|
||||
role: 'user',
|
||||
permissions: computePermissions('user'),
|
||||
}
|
||||
assert.equal(canSeeAll(admin), false)
|
||||
assert.equal(canSeeAll(admin), true)
|
||||
assert.equal(canSeeAll(user), false)
|
||||
assert.equal(admin.permissions.canToggleViewAllSessions, false)
|
||||
assert.equal(admin.permissions.canToggleViewAllSessions, true)
|
||||
assert.equal(user.permissions.canToggleViewAllSessions, false)
|
||||
|
||||
assert.equal(canAccessSession('s-owned', admin), true)
|
||||
assert.equal(canAccessSession('s-peer', 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('admin cannot enable view-all even if preference flag is passed', () => {
|
||||
const { canAccessSession, canSeeAll } = makeAccess({ 's-peer': 'u2' })
|
||||
it('admin can turn off view-all via preference (same as super_admin)', () => {
|
||||
const store = new RolesStore()
|
||||
store._roles.set('op', ROLES.ADMIN)
|
||||
store.setViewAllSessions('op', false)
|
||||
const ownersMap = new Map([['s-peer', 'u2']])
|
||||
const access = createSessionAccess({
|
||||
sessionAcl: { getOwner: (id) => ownersMap.get(String(id)) || null },
|
||||
userWorkspaces: { get: () => null, isUserPath: () => false },
|
||||
getWorkspaceRoot: () => '/ws',
|
||||
getWorkspaceRegistry: () => ({ list: () => [] }),
|
||||
rolesStore: store,
|
||||
})
|
||||
const admin = {
|
||||
empNo: 'u1',
|
||||
empNo: 'op',
|
||||
role: 'admin',
|
||||
permissions: computePermissions('admin', { viewAllSessions: true }),
|
||||
}
|
||||
assert.equal(admin.permissions.canViewAllSessions, false)
|
||||
assert.equal(admin.permissions.canToggleViewAllSessions, false)
|
||||
assert.equal(canSeeAll(admin), false)
|
||||
assert.equal(canAccessSession('s-peer', admin), false)
|
||||
assert.equal(access.canSeeAll(admin), false)
|
||||
assert.equal(access.canAccessSession('s-peer', admin), false)
|
||||
assert.equal(store.resolvePermissions('op').canToggleViewAllSessions, true)
|
||||
})
|
||||
|
||||
it('missing owner does not deny when cwd is under user path', () => {
|
||||
|
|
@ -111,26 +120,45 @@ describe('createSessionAccess', () => {
|
|||
})
|
||||
|
||||
it('admin can see unowned channel/system sessions outside user-workspaces', () => {
|
||||
const { canAccessSession, isVisibleWorkspace } = makeAccess({})
|
||||
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,
|
||||
},
|
||||
getWorkspaceRoot: () => '/ws',
|
||||
getWorkspaceRegistry: () => ({ list: () => [] }),
|
||||
rolesStore: store,
|
||||
})
|
||||
const admin = {
|
||||
empNo: 'u1',
|
||||
role: 'admin',
|
||||
permissions: computePermissions('admin'),
|
||||
permissions: computePermissions('admin', { viewAllSessions: false }),
|
||||
}
|
||||
const user = {
|
||||
empNo: 'u1',
|
||||
role: 'user',
|
||||
permissions: computePermissions('user'),
|
||||
}
|
||||
assert.equal(canAccessSession('ch-1', admin, { cwd: '/bots/whatsapp' }), true)
|
||||
assert.equal(canAccessSession('ch-1', user, { cwd: '/bots/whatsapp' }), false)
|
||||
assert.equal(canAccessSession('s-peer', admin, { cwd: '/ws/u2/x' }), false)
|
||||
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 access = makeAccess({ 'ch-owned': 'u2' })
|
||||
assert.equal(access.canAccessSession('ch-owned', admin, { cwd: '/bots/wa' }), 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(isVisibleWorkspace(admin, { id: 'bot-ws', path: '/bots/whatsapp' }), true)
|
||||
assert.equal(isVisibleWorkspace(user, { id: 'bot-ws', path: '/bots/whatsapp' }), 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', () => {
|
||||
|
|
@ -163,7 +191,7 @@ describe('createSessionAccess', () => {
|
|||
})
|
||||
|
||||
describe('RolesStore view-all prefs', () => {
|
||||
it('defaults on for super_admin and can be turned off', () => {
|
||||
it('defaults on for admin-class and can be turned off', () => {
|
||||
const store = new RolesStore()
|
||||
store._roles.set('boss', ROLES.SUPER_ADMIN)
|
||||
store._roles.set('op', ROLES.ADMIN)
|
||||
|
|
@ -179,24 +207,28 @@ describe('RolesStore view-all prefs', () => {
|
|||
store.setViewAllSessions('boss', true)
|
||||
assert.equal(store.resolvePermissions('boss').canViewAllSessions, true)
|
||||
|
||||
assert.throws(() => store.setViewAllSessions('op', true), (err) => err.code === 'forbidden_view_all_sessions')
|
||||
store._prefs.set('op', { viewAllSessions: true })
|
||||
assert.equal(store.resolvePermissions('op').canToggleViewAllSessions, true)
|
||||
assert.equal(store.resolvePermissions('op').canViewAllSessions, true)
|
||||
store.setViewAllSessions('op', false)
|
||||
assert.equal(store.resolvePermissions('op').canViewAllSessions, false)
|
||||
assert.equal(store.resolvePermissions('op').canToggleViewAllSessions, false)
|
||||
})
|
||||
|
||||
it('rejects view-all toggle for ordinary users and admins', () => {
|
||||
it('rejects view-all toggle for ordinary users', () => {
|
||||
const store = new RolesStore()
|
||||
store._roles.set('u1', ROLES.USER)
|
||||
store._roles.set('a1', ROLES.ADMIN)
|
||||
assert.throws(() => store.setViewAllSessions('u1', true), (err) => err.code === 'forbidden_view_all_sessions')
|
||||
assert.throws(() => store.setViewAllSessions('a1', true), (err) => err.code === 'forbidden_view_all_sessions')
|
||||
})
|
||||
|
||||
it('lets admin, super_admin, and fallback_admin create workspaces', () => {
|
||||
assert.equal(computePermissions(ROLES.SUPER_ADMIN).canCreateWorkspace, true)
|
||||
assert.equal(computePermissions(ROLES.FALLBACK_ADMIN).canCreateWorkspace, true)
|
||||
assert.equal(computePermissions(ROLES.ADMIN).canCreateWorkspace, true)
|
||||
it('admin-class roles share create-workspace and manage-users permissions', () => {
|
||||
for (const role of [ROLES.SUPER_ADMIN, ROLES.FALLBACK_ADMIN, ROLES.ADMIN]) {
|
||||
const p = computePermissions(role, { viewAllSessions: true })
|
||||
assert.equal(p.canCreateWorkspace, true)
|
||||
assert.equal(p.canManageUsers, true)
|
||||
assert.equal(p.canAccessSettings, true)
|
||||
assert.equal(p.canToggleViewAllSessions, true)
|
||||
assert.equal(p.canViewAllSessions, true)
|
||||
}
|
||||
assert.equal(computePermissions(ROLES.USER).canCreateWorkspace, false)
|
||||
assert.equal(computePermissions(ROLES.USER).canManageUsers, false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue