mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
e1ac5e0fc8
* 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>
134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
import asyncio
|
||
import json
|
||
import traceback
|
||
from quart import Quart, jsonify, request
|
||
from slack_sdk.web.async_client import AsyncWebClient
|
||
from .slackevent import SlackEvent
|
||
from typing import Callable
|
||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||
|
||
_MAX_CALLBACK_BODY_BYTES = 1024 * 1024
|
||
|
||
|
||
class SlackClient:
|
||
def __init__(self, bot_token: str, signing_secret: str, logger: None, unified_mode: bool = False):
|
||
self.bot_token = bot_token
|
||
self.signing_secret = signing_secret
|
||
self.unified_mode = unified_mode
|
||
self.app = Quart(__name__)
|
||
self.app.config['MAX_CONTENT_LENGTH'] = _MAX_CALLBACK_BODY_BYTES
|
||
self.client = AsyncWebClient(self.bot_token)
|
||
|
||
# 只有在非统一模式下才注册独立路由
|
||
if not self.unified_mode:
|
||
self.app.add_url_rule(
|
||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||
)
|
||
|
||
self._message_handlers = {
|
||
'example': [],
|
||
}
|
||
self.bot_user_id = None # 避免机器人回复自己的消息
|
||
self.logger = logger
|
||
|
||
async def handle_callback_request(self):
|
||
"""处理回调请求(独立端口模式,使用全局 request)"""
|
||
return await self._handle_callback_internal(request)
|
||
|
||
async def handle_unified_webhook(self, req):
|
||
"""处理回调请求(统一 webhook 模式,显式传递 request)。
|
||
|
||
Args:
|
||
req: Quart Request 对象
|
||
|
||
Returns:
|
||
响应数据
|
||
"""
|
||
return await self._handle_callback_internal(req)
|
||
|
||
async def _handle_callback_internal(self, req):
|
||
"""处理回调请求的内部实现。
|
||
|
||
Args:
|
||
req: Quart Request 对象
|
||
"""
|
||
try:
|
||
body = await req.get_data()
|
||
if len(body) > _MAX_CALLBACK_BODY_BYTES:
|
||
raise ValueError('Slack callback body exceeds the size limit')
|
||
data = await asyncio.to_thread(json.loads, body)
|
||
if 'type' in data:
|
||
if data['type'] == 'url_verification':
|
||
return data['challenge']
|
||
|
||
bot_user_id = data.get('event', {}).get('bot_id', '')
|
||
|
||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||
return jsonify({'status': 'ok'})
|
||
|
||
# 处理私信
|
||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||
event = SlackEvent.from_payload(data)
|
||
await self._handle_message(event)
|
||
return jsonify({'status': 'ok'})
|
||
|
||
# 处理群聊
|
||
if data.get('event', {}).get('type') == 'app_mention':
|
||
data.setdefault('event', {})['channel_type'] = 'channel'
|
||
event = SlackEvent.from_payload(data)
|
||
await self._handle_message(event)
|
||
return jsonify({'status': 'ok'})
|
||
|
||
return jsonify({'status': 'ok'})
|
||
|
||
except Exception as e:
|
||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||
raise (e)
|
||
|
||
async def _handle_message(self, event: SlackEvent):
|
||
"""
|
||
处理消息事件。
|
||
"""
|
||
msg_type = event.type
|
||
if msg_type in self._message_handlers:
|
||
for handler in self._message_handlers[msg_type]:
|
||
await handler(event)
|
||
|
||
def on_message(self, msg_type: str):
|
||
"""注册消息类型处理器"""
|
||
|
||
def decorator(func: Callable[[platform_events.Event], None]):
|
||
if msg_type not in self._message_handlers:
|
||
self._message_handlers[msg_type] = []
|
||
self._message_handlers[msg_type].append(func)
|
||
return func
|
||
|
||
return decorator
|
||
|
||
async def send_message_to_channel(self, text: str, channel_id: str):
|
||
try:
|
||
response = await self.client.chat_postMessage(channel=channel_id, text=text)
|
||
if self.bot_user_id is None and response.get('ok'):
|
||
self.bot_user_id = response['message']['bot_id']
|
||
return
|
||
except Exception as e:
|
||
await self.logger.error(f'Error in send_message: {e}')
|
||
raise e
|
||
|
||
async def send_message_to_one(self, text: str, user_id: str):
|
||
try:
|
||
response = await self.client.chat_postMessage(channel='@' + user_id, text=text)
|
||
if self.bot_user_id is None and response.get('ok'):
|
||
self.bot_user_id = response['message']['bot_id']
|
||
|
||
return
|
||
except Exception as e:
|
||
await self.logger.error(f'Error in send_message: {traceback.format_exc()}')
|
||
raise e
|
||
|
||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||
"""
|
||
启动 Quart 应用。
|
||
"""
|
||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|