mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-05 02:56:06 +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>
214 lines
7.9 KiB
Python
214 lines
7.9 KiB
Python
"""
|
|
sys.modules isolation utilities for breaking circular import chains.
|
|
|
|
Provides safe, reversible sys.modules manipulation for tests that need to
|
|
import modules with heavy import-time side effects (auto-registration,
|
|
circular dependencies, etc.).
|
|
|
|
Usage pattern:
|
|
1. Create mock objects for modules that cause circular imports
|
|
2. Use isolated_sys_modules to temporarily patch sys.modules
|
|
3. Import target module after patching
|
|
4. Test the real production code
|
|
5. Context manager automatically restores original sys.modules state
|
|
|
|
Key principle: mock only what breaks the import chain, not what the code needs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import enum
|
|
from contextlib import contextmanager
|
|
from typing import Generator
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
_MISSING = object()
|
|
|
|
|
|
class MockLifecycleControlScope(enum.Enum):
|
|
"""Mock enum for breaking circular import in core.entities."""
|
|
|
|
APPLICATION = 'application'
|
|
PLATFORM = 'platform'
|
|
PLUGIN = 'plugin'
|
|
PROVIDER = 'provider'
|
|
|
|
|
|
@contextmanager
|
|
def isolated_sys_modules(
|
|
mocks: dict[str, object],
|
|
clear: list[str] | None = None,
|
|
) -> Generator[None, None, None]:
|
|
"""
|
|
Context manager for isolated sys.modules manipulation.
|
|
|
|
Safely patches sys.modules with mocks and clears specified modules,
|
|
then restores original state on exit. This prevents test pollution
|
|
where mocks leak into subsequent tests.
|
|
|
|
Args:
|
|
mocks: Dict mapping module names to mock objects.
|
|
These will be set in sys.modules during the context.
|
|
clear: List of module names to remove from sys.modules before
|
|
entering the context. Useful for forcing re-import of
|
|
modules that depend on mocked modules.
|
|
|
|
Example:
|
|
>>> with isolated_sys_modules(
|
|
... mocks={'my_pkg.heavy_module': MagicMock()},
|
|
... clear=['my_pkg.target_module'],
|
|
... ):
|
|
... from my_pkg.target_module import MyClass # Safe import
|
|
|
|
Note:
|
|
- Modules in both mocks and clear will be mocked (not cleared)
|
|
- Original state is restored even if exception occurs
|
|
- Modules not in sys.modules before context are removed after
|
|
- Package attributes (e.g., my_pkg.submodule) are also saved/restored
|
|
"""
|
|
clear = clear or []
|
|
touched = set(mocks.keys()) | set(clear)
|
|
|
|
# Save original state for modules we'll touch
|
|
saved: dict[str, object] = {}
|
|
for name in touched:
|
|
if name in sys.modules:
|
|
saved[name] = sys.modules[name]
|
|
|
|
# Importing a submodule also mutates its parent package attribute. Preserve
|
|
# that state for every mocked or cleared module, otherwise restoring only
|
|
# sys.modules leaves stale class/enum identities attached to the package.
|
|
saved_attrs: dict[str, tuple[str, str, object]] = {}
|
|
for name in touched:
|
|
pkg_name, separator, attr_name = name.rpartition('.')
|
|
if separator and pkg_name in sys.modules:
|
|
saved_attrs[name] = (
|
|
pkg_name,
|
|
attr_name,
|
|
getattr(sys.modules[pkg_name], attr_name, _MISSING),
|
|
)
|
|
|
|
try:
|
|
# Clear modules first (force re-import)
|
|
for name in clear:
|
|
if name not in mocks: # Don't clear if we're mocking it
|
|
sys.modules.pop(name, None)
|
|
saved_attr = saved_attrs.get(name)
|
|
if saved_attr is not None:
|
|
pkg_name, attr_name, _ = saved_attr
|
|
if pkg_name in sys.modules and hasattr(sys.modules[pkg_name], attr_name):
|
|
delattr(sys.modules[pkg_name], attr_name)
|
|
|
|
# Apply mocks
|
|
for name, module in mocks.items():
|
|
sys.modules[name] = module
|
|
|
|
# Update package attributes to point to mocks
|
|
# This is critical because `from package import submodule` gets the attribute,
|
|
# not sys.modules directly
|
|
for mock_name, (pkg_name, attr_name) in _PACKAGE_ATTRIBUTE_UPDATES.items():
|
|
if mock_name in mocks and pkg_name in sys.modules:
|
|
setattr(sys.modules[pkg_name], attr_name, mocks[mock_name])
|
|
|
|
yield
|
|
|
|
finally:
|
|
# Restore original state - critical for test isolation
|
|
for name in touched:
|
|
if name in saved:
|
|
sys.modules[name] = saved[name]
|
|
else:
|
|
# Wasn't in sys.modules originally, remove it
|
|
sys.modules.pop(name, None)
|
|
|
|
# Restore package attributes (or remove ones that did not previously
|
|
# exist), keeping package lookup consistent with restored sys.modules.
|
|
for pkg_name, attr_name, original_value in saved_attrs.values():
|
|
if pkg_name not in sys.modules:
|
|
continue
|
|
if original_value is _MISSING:
|
|
if hasattr(sys.modules[pkg_name], attr_name):
|
|
delattr(sys.modules[pkg_name], attr_name)
|
|
else:
|
|
setattr(sys.modules[pkg_name], attr_name, original_value)
|
|
|
|
|
|
def make_pipeline_handler_import_mocks() -> dict[str, MagicMock]:
|
|
"""
|
|
Create mock objects needed to break circular import chain in handlers.
|
|
|
|
The import chain:
|
|
handler → core.app → pipeline.controller → http_controller
|
|
→ groups/plugins → taskmgr (partial init)
|
|
|
|
This function creates minimal mocks that break this chain without
|
|
affecting the handler's ability to use real pipeline.entities
|
|
(needed for ResultType enum comparisons).
|
|
|
|
Returns:
|
|
Dict mapping module names to MagicMock objects.
|
|
|
|
Note:
|
|
These mocks are intentionally minimal - they only provide what's
|
|
needed to prevent circular imports. The actual handler code uses
|
|
real imports from langbot_plugin.api and langbot.pkg.pipeline.entities.
|
|
"""
|
|
# Mock core.entities with proper Enum class
|
|
mock_entities = MagicMock()
|
|
mock_entities.LifecycleControlScope = MockLifecycleControlScope
|
|
|
|
# Mock core.app - Application class is referenced but not instantiated
|
|
mock_app = MagicMock()
|
|
|
|
# Mock provider.runner - has preregistered_runners attribute
|
|
mock_runner = MagicMock()
|
|
mock_runner.preregistered_runners = [] # Empty by default, tests override
|
|
|
|
# Mock utils.importutil - prevents auto-import of runners
|
|
mock_importutil = MagicMock()
|
|
mock_importutil.import_modules_in_pkg = lambda pkg: None
|
|
mock_importutil.import_modules_in_pkgs = lambda pkgs: None
|
|
|
|
return {
|
|
'langbot.pkg.core.entities': mock_entities,
|
|
'langbot.pkg.core.app': mock_app,
|
|
'langbot.pkg.pipeline.controller': MagicMock(),
|
|
'langbot.pkg.pipeline.pipelinemgr': MagicMock(),
|
|
'langbot.pkg.pipeline.process.process': MagicMock(),
|
|
'langbot.pkg.provider.runner': mock_runner,
|
|
'langbot.pkg.utils.importutil': mock_importutil,
|
|
}
|
|
|
|
|
|
# Package attributes that need to be updated alongside sys.modules mocking.
|
|
# When Python imports a submodule (e.g., langbot.pkg.provider.runner), it
|
|
# automatically sets an attribute on the parent package. The import statement
|
|
# `from ....provider import runner` gets this attribute, not sys.modules directly.
|
|
# This dict maps mock module names to the parent packages that need attribute updates.
|
|
_PACKAGE_ATTRIBUTE_UPDATES: dict[str, tuple[str, str]] = {
|
|
'langbot.pkg.provider.runner': ('langbot.pkg.provider', 'runner'),
|
|
}
|
|
|
|
|
|
def get_handler_modules_to_clear(handler_name: str) -> list[str]:
|
|
"""
|
|
Get list of handler-related modules to clear before import.
|
|
|
|
These modules need to be cleared so they're re-imported after
|
|
the circular import chain is mocked. Without clearing, they'd
|
|
already be in sys.modules (possibly partially initialized).
|
|
|
|
Args:
|
|
handler_name: The handler file name (e.g., 'chat', 'command')
|
|
|
|
Returns:
|
|
List of module names to clear.
|
|
"""
|
|
return [
|
|
'langbot.pkg.pipeline.process.handler',
|
|
'langbot.pkg.pipeline.process.handlers',
|
|
f'langbot.pkg.pipeline.process.handlers.{handler_name}',
|
|
]
|