feat(tenancy): add Workspace multi-tenant foundation (#2353)

* Document multi-tenant workspace architecture

* Add OSS and commercial workspace boundaries

* docs: redesign multi-tenant workspace architecture

* feat(tenancy): implement workspace isolation

* docs(tenancy): record verification evidence

* docs(tenancy): revise single-instance SaaS topology

* docs(tenancy): refine architecture options

* docs: finalize cloud v2 multi-tenant decisions

* feat(tenancy): establish cloud isolation foundations

* feat(tenancy): harden shared cloud runtime boundaries

* docs(tenancy): record final isolation verification

* fix(tenancy): close isolation and permission gaps

* docs(tenancy): record final isolation verification

* feat(tenancy): connect cloud workspace control plane

* fix(build): install git for pinned SDK

* docs(cloud): update control plane verification

* chore: update multi-tenant SDK pin

* fix(cloud): skip legacy model sync during startup

* test(cloud): preserve minimal model manager fixtures

* fix(cloud): preserve authenticated account context

* fix(cloud): reuse authenticated account for user info

* feat(cloud): complete Workspace settings navigation

* test(web): cover Workspace dropdown menu

* feat(web): place workspace controls in sidebar

* refactor(web): streamline workspace controls

* style(web): format workspace layout test

* fix(cloud): surface runtime and workspace plan status

* fix(plugin): keep runtime identity stable across restarts

* fix(ui): widen and center workspace switcher

* fix(ui): hide roles from workspace switcher

* fix(ui): align workspace switcher with sidebar entries

* feat(workspace): add in-product collaboration and direct Cloud launch

* style: format collaboration changes

* fix(workspace): bind collaboration APIs to tenant UoW

* fix(cloud): preserve Core-owned collaboration state

* test(cloud): require Space identity for invite registration

* feat(cloud): complete secure invitation experience

* style(web): format invitation flows

* fix(cloud): recover box runtime without unscoped skill reload

* feat(oss): enforce invitation account and owner billing flows

* style: format OSS account service

* test(oss): cover invitation logout handoff

* fix(oss): resolve workspace owner in scoped session

* feat(cloud): harden multi-tenant runtime resources

* fix(cloud): bound runtime restart storms

* fix(cloud): eliminate periodic runtime CPU spikes

* fix(cloud): enforce instance capacity ceilings

* fix(cloud): scope public login capability discovery

* fix(cloud): bound tenant maintenance and monitoring work

* fix(runtime): bound tenant resource amplification

* fix(deps): pin green multi-tenant plugin SDK

* fix(cloud): handle unavailable skill capability

* fix(security): require authentication for image file endpoint (H-2)

- Changed /api/v1/files/image from AuthType.NONE to USER_TOKEN_OR_API_KEY
- Added Permission.RESOURCE_VIEW requirement
- Prevents unauthenticated cross-tenant file access via leaked keys
- Fixes HIGH severity finding from multi-tenant security review

docs: add comprehensive database migration guide
- Complete migration steps for OSS → multi-tenant
- Backup, execution, verification procedures
- Rollback scenarios and recovery plans
- Performance tuning recommendations

* test: add comprehensive cross-tenant isolation tests

Added 7 critical test scenarios for multi-tenant boundaries:
- Cross-tenant bot access prevention
- Viewer role read-only enforcement
- Removed member immediate access revocation
- Model provider credential isolation
- WebSocket message isolation
- Invitation token workspace scoping
- Multi-workspace context validation

These tests address P0-2 coverage gaps for:
- workspaces.py (membership & invitation flows)
- user.py (authentication & authorization)
- websocket_chat.py (real-time isolation)
- plugins.py (resource access control)

docs: finalize database migration guide

* fix(security): resolve M-1, M-2, M-3 security findings

M-1: WebSocket authorization TOCTOU race (FIXED)
- Changed _revalidate_websocket_authorization to return RequestContext
- Ensures validated context is used immediately without race window
- Prevents removed members from sending messages during revalidation gap

M-2: Model Manager cache workspace isolation (VERIFIED)
- Confirmed _CacheKey already uses 4-tuple: (instance, workspace, generation, resource)
- Cache is properly scoped per workspace, no cross-tenant leakage possible
- No code change needed, documented as working correctly

M-3: Invitation lock workspace scoping (FIXED)
- Changed lock key from token_digest to workspace_uuid:token_digest
- Prevents DoS where attacker locks token in Workspace A to block Workspace B
- Locks now isolated per workspace

All MEDIUM severity findings from security review now resolved.

* fix(cloud): unblock tenant CI and enforce knowledge quotas

* fix(tenancy): scope rerank model sync

---------

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
RockChinQ
2026-07-30 21:43:35 +08:00
committed by GitHub
parent 463b120923
commit e1ac5e0fc8
468 changed files with 78320 additions and 13137 deletions
+93 -2
View File
@@ -79,7 +79,7 @@ const zhHans = {
loading: '加载中...',
fieldRequired: '此字段为必填项',
or: '或',
loginWithSpace: '通过 Space 登录',
loginWithSpace: '使用 LangBot 账号登录',
spaceLoginRecommended: '推荐:使用官方提供的稳定模型 API 和云服务',
loginLocal: '使用本地账号登录',
loginWithPassword: '通过密码登录',
@@ -156,6 +156,7 @@ const zhHans = {
webhookHint: 'Webhook 允许 LangBot 将个人消息和群消息事件推送到外部系统',
actions: '操作',
apiKeyCreatedMessage: '请复制此 API 密钥,若按钮无效,请手动复制。',
apiKeyStoredSecurely: '密钥仅在创建时显示',
none: '无',
more: '更多 ({{count}})',
less: '收起',
@@ -264,8 +265,10 @@ const zhHans = {
searchProviders: '搜索供应商...',
langbotModelsDescription: 'LangBot Space 提供的云端模型',
credits: '积分',
loginWithSpace: '通过 Space 登录',
loginWithSpace: '使用 LangBot 账号登录',
loginToUseModels: '通过 Space 登录以使用云端模型',
ownerMustBindSpace: '工作区所有者需要绑定 Space 才能使用 LangBot 模型。',
usesOwnerSpaceBilling: '使用工作区所有者的 Space 计费与积分。',
noModels: '暂无模型',
langbotModels: 'LangBot 模型',
spaceTrialTooltip:
@@ -792,6 +795,8 @@ const zhHans = {
boxStdioRefusedSuggestion:
'请启用 Boxbox.enabled = true)并确认运行时连接正常,或将此服务器切换到 http/sse 模式。',
boxRequired: '需要 Box',
disabledByPolicy: '已被策略禁用',
stdioDisabledByPolicy: '此部署已禁用 Stdio MCP,请改用远程 MCP 服务器。',
stdioBlockedByBoxToast:
'Box 沙箱已禁用或不可用,无法保存 stdio 模式的 MCP。请启用 Box 或改为 http/sse 模式。',
toolsFound: '个工具',
@@ -1216,6 +1221,91 @@ const zhHans = {
bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起',
setPasswordHint: '设置密码后可使用邮箱密码登录',
spaceEmailMismatch: 'Space登录账号邮箱与本实例账号邮箱不匹配',
space_account_not_registeredTitle: '账户尚未注册',
space_account_not_registered:
'此 Space 邮箱尚无本地账户,请联系工作区所有者获取邀请。',
space_account_binding_requiredTitle: '需要绑定 Space',
space_account_binding_required:
'此本地账户必须先在账户设置中绑定 Space,才能使用 Space 登录。',
},
workspace: {
title: '工作区',
description: '管理成员、角色与邀请链接',
selectTitle: '选择工作区',
selectDescription: '选择你要进入的 LangBot 工作区。',
selectionLoadFailed: '无法加载你的工作区,请重试。',
switchWorkspace: '切换工作区',
settings: '工作区设置',
currentPlan: '当前计划',
planUnavailable: '暂不可用',
upgradePlan: '切换或升级计划',
ossSingletonDescription:
'当前自托管实例只有一个工作区,但可以包含多个用户。',
cloudManagedDescription:
'此工作区托管于 LangBot Cloud。成员在此管理,计费在 Cloud 中打开。',
loadFailed: '加载工作区信息失败',
members: '成员',
you: '你',
inviteMember: '邀请成员',
inviteDescription: '创建一次性链接,将其他用户加入当前工作区。',
emailPlaceholder: 'member@example.com',
createInvitation: '创建邀请',
invitationCreated: '邀请已创建',
delivery: {
sent: '邀请邮件已发送',
link_only: '邀请链接已创建',
failed: '邀请链接已创建,但邮件发送失败',
},
invitationCreateFailed: '创建邀请失败',
oneTimeLinkWarning: '请立即复制此链接。它只显示一次。',
copyInvitation: '复制邀请链接',
invitationCopied: '邀请链接已复制',
pendingInvitations: '待接受邀请',
expiresAt: '{{date}} 过期',
revokeInvitation: '撤销邀请',
invitationRevoked: '邀请已撤销',
invitationRevokeFailed: '撤销邀请失败',
acceptInvitation: '接受邀请',
invitedToWorkspace: '你已受邀加入 {{workspace}}',
checkingInvitation: '正在验证邀请…',
invitationMissing: '此邀请链接缺少必要信息。',
invitationExpired: '此邀请已过期。',
invitationAlreadyRevoked: '此邀请已被撤销。',
invitationAlreadyUsed: '此邀请已被使用。',
invitationInvalid: '此邀请无效或已不可用。',
invitationAccepted: '已接受邀请',
invitationAcceptFailed: '接受邀请失败',
invitationEmailMismatch: '此邀请属于另一个邮箱地址。',
existingAccountLoginRequired: '此邮箱已有账户,请登录后继续。',
acceptAsCurrentAccount: '使用当前账户接受',
authenticatedInvitationNotice:
'请先退出,再使用受邀账户登录。邀请令牌会被保留。',
logoutAndReturn: '退出并返回此邀请',
switchAccount: '切换账号',
registerAndAccept: '创建账户并接受',
alreadyHaveAccount: '我已有账户',
confirmPassword: '确认密码',
passwordMinimum: '密码至少需要 8 个字符。',
passwordMismatch: '两次输入的密码不一致。',
backToLogin: '返回登录',
memberUpdated: '成员角色已更新',
memberUpdateFailed: '更新成员角色失败',
removeMember: '移除成员',
removeMemberConfirm: '确定将此成员移出工作区吗?',
memberRemoved: '成员已移除',
memberRemoveFailed: '移除成员失败',
transferOwnership: '转让所有权',
types: {
personal: '个人',
team: '团队',
},
roles: {
owner: '所有者',
admin: '管理员',
developer: '开发者',
operator: '运维人员',
viewer: '查看者',
},
},
monitoring: {
title: '仪表盘',
@@ -1467,6 +1557,7 @@ const zhHans = {
settingsDialog: {
title: '设置',
nav: {
workspace: '工作区',
models: '模型',
api: 'API',
storage: '存储',