Allow admin (and Host plugins) to create workspaces.

super_admin/fallback_admin already could; admin now has canCreateWorkspace, and Host-side creates without browser ALS (IM) are no longer blocked as forbidden.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-16 21:52:14 +08:00
parent 050b99426f
commit 89700ef4f9
6 changed files with 26 additions and 15 deletions

View file

@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
"ui.networkError": "网络错误...", "ui.networkError": "网络错误...",
"ui.qrGenerateFailed": "生成二维码失败", "ui.qrGenerateFailed": "生成二维码失败",
"ui.cronLoginRequired": "登录后才能使用定时任务", "ui.cronLoginRequired": "登录后才能使用定时任务",
"ui.workspaceCreateForbidden": "只有超级管理员可以创建工作区", "ui.workspaceCreateForbidden": "只有管理员可以创建工作区",
"ui.workspaceLoginRequired": "登录后才能使用工作区", "ui.workspaceLoginRequired": "登录后才能使用工作区",
"err.not_logged_in": "未登录", "err.not_logged_in": "未登录",
"err.forbidden_settings": "当前账号无设置权限", "err.forbidden_settings": "当前账号无设置权限",
@ -138,7 +138,7 @@ window.__ModuleLoader__.load({
"err.session_forbidden": "无权访问该会话", "err.session_forbidden": "无权访问该会话",
"err.session_workspace_only": "只能在自己的工作区创建会话", "err.session_workspace_only": "只能在自己的工作区创建会话",
"err.workspace_path_only": "只能打开自己的工作区路径", "err.workspace_path_only": "只能打开自己的工作区路径",
"err.workspace_create_forbidden": "只有超级管理员可以创建工作区", "err.workspace_create_forbidden": "只有管理员可以创建工作区",
"err.no_skill_credentials": "请先完成 UAC 扫码登录", "err.no_skill_credentials": "请先完成 UAC 扫码登录",
"err.loopback_only_credentials": "agent-credentials 仅允许本机访问", "err.loopback_only_credentials": "agent-credentials 仅允许本机访问",
"err.loopback_only_outbound": "outbound 仅允许本机访问", "err.loopback_only_outbound": "outbound 仅允许本机访问",
@ -240,7 +240,7 @@ window.__ModuleLoader__.load({
"ui.networkError": "Network error...", "ui.networkError": "Network error...",
"ui.qrGenerateFailed": "Failed to generate QR", "ui.qrGenerateFailed": "Failed to generate QR",
"ui.cronLoginRequired": "Sign in to use scheduled tasks", "ui.cronLoginRequired": "Sign in to use scheduled tasks",
"ui.workspaceCreateForbidden": "Only super admins can create workspaces", "ui.workspaceCreateForbidden": "Only admins can create workspaces",
"ui.workspaceLoginRequired": "Sign in to use workspaces", "ui.workspaceLoginRequired": "Sign in to use workspaces",
"err.not_logged_in": "Not signed in", "err.not_logged_in": "Not signed in",
"err.forbidden_settings": "No settings permission", "err.forbidden_settings": "No settings permission",
@ -278,7 +278,7 @@ window.__ModuleLoader__.load({
"err.session_forbidden": "No access to this session", "err.session_forbidden": "No access to this session",
"err.session_workspace_only": "Sessions can only be created in your own workspace", "err.session_workspace_only": "Sessions can only be created in your own workspace",
"err.workspace_path_only": "You can only open your own workspace path", "err.workspace_path_only": "You can only open your own workspace path",
"err.workspace_create_forbidden": "Only super admins can create workspaces", "err.workspace_create_forbidden": "Only admins can create workspaces",
"err.no_skill_credentials": "Complete UAC QR sign-in first", "err.no_skill_credentials": "Complete UAC QR sign-in first",
"err.loopback_only_credentials": "agent-credentials is loopback-only", "err.loopback_only_credentials": "agent-credentials is loopback-only",
"err.loopback_only_outbound": "outbound is loopback-only", "err.loopback_only_outbound": "outbound is loopback-only",
@ -1852,7 +1852,7 @@ function reloadAfterLogin() {
ctx.effect(() => { ctx.effect(() => {
// Open/choose workspace is super_admin-only (canCreateWorkspace). // Open/choose workspace: admin / super_admin / fallback_admin (canCreateWorkspace).
// Everyone else uses the auto-provisioned per-user workspace and must not open the picker. // Everyone else uses the auto-provisioned per-user workspace and must not open the picker.
const CHOOSER = hostAriaSel('chooseWorkspace') const CHOOSER = hostAriaSel('chooseWorkspace')
// Inert composer: onClick lives on the card (cardWorkspaceTrigger), not the labeled node. // Inert composer: onClick lives on the card (cardWorkspaceTrigger), not the labeled node.

View file

@ -929,7 +929,10 @@ ctx.inject(['workspaceController'], (wctx) => {
wc.create = async (request) => { wc.create = async (request) => {
const identity = getUserContext() const identity = getUserContext()
if (identity?._internalProvision) return origCreate(request) if (identity?._internalProvision) return origCreate(request)
if (!identity?.permissions?.canCreateWorkspace) { // No ALS identity: Host-side plugins (IM / cron) create workspaces without a
// browser login. Authenticated browser calls always run under user context;
// there admin / super_admin / fallback_admin must have canCreateWorkspace.
if (identity && !identity.permissions?.canCreateWorkspace) {
throwForbidden('workspace_create_forbidden') throwForbidden('workspace_create_forbidden')
} }
return origCreate(request) return origCreate(request)
@ -1060,7 +1063,7 @@ ctx.inject(['workspaceController'], (wctx) => {
} }
}) })
// directory picker: only super_admin may pick/create dirs (others get auto workspaces) // directory picker: admin / super_admin / fallback_admin (canCreateWorkspace)
ctx.inject(['directoryPicker'], (dctx) => { ctx.inject(['directoryPicker'], (dctx) => {
const dp = dctx.directoryPicker const dp = dctx.directoryPicker
if (!dp || dp.__udsAcl) return if (!dp || dp.__udsAcl) return

View file

@ -101,7 +101,7 @@ export const MESSAGES = {
// client gates // client gates
'ui.cronLoginRequired': '登录后才能使用定时任务', 'ui.cronLoginRequired': '登录后才能使用定时任务',
'ui.workspaceCreateForbidden': '只有超级管理员可以创建工作区', 'ui.workspaceCreateForbidden': '只有管理员可以创建工作区',
'ui.workspaceLoginRequired': '登录后才能使用工作区', 'ui.workspaceLoginRequired': '登录后才能使用工作区',
// API / ACL errors (stable codes) // API / ACL errors (stable codes)
@ -137,7 +137,7 @@ export const MESSAGES = {
'err.session_forbidden': '无权访问该会话', 'err.session_forbidden': '无权访问该会话',
'err.session_workspace_only': '只能在自己的工作区创建会话', 'err.session_workspace_only': '只能在自己的工作区创建会话',
'err.workspace_path_only': '只能打开自己的工作区路径', 'err.workspace_path_only': '只能打开自己的工作区路径',
'err.workspace_create_forbidden': '只有超级管理员可以创建工作区', 'err.workspace_create_forbidden': '只有管理员可以创建工作区',
'err.no_skill_credentials': '请先完成 UAC 扫码登录', 'err.no_skill_credentials': '请先完成 UAC 扫码登录',
'err.loopback_only_credentials': 'agent-credentials 仅允许本机访问', 'err.loopback_only_credentials': 'agent-credentials 仅允许本机访问',
'err.loopback_only_outbound': 'outbound 仅允许本机访问', 'err.loopback_only_outbound': 'outbound 仅允许本机访问',
@ -250,7 +250,7 @@ export const MESSAGES = {
'ui.qrGenerateFailed': 'Failed to generate QR', 'ui.qrGenerateFailed': 'Failed to generate QR',
'ui.cronLoginRequired': 'Sign in to use scheduled tasks', 'ui.cronLoginRequired': 'Sign in to use scheduled tasks',
'ui.workspaceCreateForbidden': 'Only super admins can create workspaces', 'ui.workspaceCreateForbidden': 'Only admins can create workspaces',
'ui.workspaceLoginRequired': 'Sign in to use workspaces', 'ui.workspaceLoginRequired': 'Sign in to use workspaces',
'err.not_logged_in': 'Not signed in', 'err.not_logged_in': 'Not signed in',
@ -285,7 +285,7 @@ export const MESSAGES = {
'err.session_forbidden': 'No access to this session', 'err.session_forbidden': 'No access to this session',
'err.session_workspace_only': 'Sessions can only be created in your own workspace', 'err.session_workspace_only': 'Sessions can only be created in your own workspace',
'err.workspace_path_only': 'You can only open your own workspace path', 'err.workspace_path_only': 'You can only open your own workspace path',
'err.workspace_create_forbidden': 'Only super admins can create workspaces', 'err.workspace_create_forbidden': 'Only admins can create workspaces',
'err.no_skill_credentials': 'Complete UAC QR sign-in first', 'err.no_skill_credentials': 'Complete UAC QR sign-in first',
'err.loopback_only_credentials': 'agent-credentials is loopback-only', 'err.loopback_only_credentials': 'agent-credentials is loopback-only',
'err.loopback_only_outbound': 'outbound is loopback-only', 'err.loopback_only_outbound': 'outbound is loopback-only',

View file

@ -4,11 +4,12 @@
* 角色: * 角色:
* super_admin 所有权限 + 用户管理;默认可见全部会话(可在设置中关闭) * super_admin 所有权限 + 用户管理;默认可见全部会话(可在设置中关闭)
* fallback_admin 等同 super_admin(兜底 administrator) * fallback_admin 等同 super_admin(兜底 administrator)
* admin 无设置齿轮;仅看自己会话(含 @);可见渠道/系统会话 * admin 无设置齿轮;仅看自己会话(含 @);可见渠道/系统会话;可创建工作区
* user 仅看自己会话,无设置 * user 仅看自己会话,无设置
* *
* 超管/应急默认全览开启;prefs.viewAllSessions === false 时关闭。 * 超管/应急默认全览开启;prefs.viewAllSessions === false 时关闭。
* 设置齿轮仅超管/应急(canAccessSettings)。 * 设置齿轮仅超管/应急(canAccessSettings)。
* 创建工作区:super_admin / fallback_admin / admin(扫码不可用时现场通常只有 admin)。
* 持久化: roles.json (roles + prefs + fallbackPasswordHash) * 持久化: roles.json (roles + prefs + fallbackPasswordHash)
*/ */
import { createHash, randomBytes } from 'node:crypto' import { createHash, randomBytes } from 'node:crypto'
@ -61,12 +62,12 @@ export function computePermissions(role, opts = {}) {
case ROLES.ADMIN: case ROLES.ADMIN:
return { return {
canManageUsers: false, canManageUsers: false,
// 设置齿轮仅超管/应急;admin 仍可看渠道/系统会话 // 设置齿轮仅超管/应急;admin 仍可看渠道/系统会话,并可创建工作区
canAccessSettings: false, canAccessSettings: false,
canToggleViewAllSessions: false, canToggleViewAllSessions: false,
canViewAllSessions: false, canViewAllSessions: false,
canViewSystemSessions: true, canViewSystemSessions: true,
canCreateWorkspace: false, canCreateWorkspace: true,
} }
default: // user / undefined default: // user / undefined
return { return {

View file

@ -1,6 +1,6 @@
{ {
"name": "uds-auth", "name": "uds-auth",
"version": "0.2.3", "version": "0.2.4",
"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",

View file

@ -192,4 +192,11 @@ describe('RolesStore view-all prefs', () => {
assert.throws(() => store.setViewAllSessions('u1', true), (err) => err.code === 'forbidden_view_all_sessions') 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') 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)
assert.equal(computePermissions(ROLES.USER).canCreateWorkspace, false)
})
}) })