feat(cloud): add scoped support admin sessions (#2369)

* feat(cloud): add scoped support admin sessions

* style(web): format support admin session changes

* fix(cloud): isolate support adapter sessions

* fix(cloud): authenticate plugin assets and report workspace resources

---------

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
Hyu
2026-07-31 17:41:55 +08:00
committed by GitHub
parent 9df021eb8f
commit 404e3466d9
37 changed files with 1703 additions and 102 deletions
@@ -418,6 +418,7 @@ class TestUserServiceGenerateJwtToken:
assert token is not None
class TestUserServiceVerifyJwtToken:
"""Tests for verify_jwt_token method."""
@@ -149,6 +149,36 @@ async def test_session_scope_matches_exact_tenant_placement_and_principal():
assert sessions == {}
async def test_support_admin_sessions_are_scoped_to_the_persisted_grant():
def support_context(grant_jti_hash: str) -> RequestContext:
return RequestContext(
instance_uuid='instance-test',
placement_generation=1,
request_id='request-test',
auth_type='support-admin',
principal=PrincipalContext(
principal_type=PrincipalType.SUPPORT_ADMIN,
actor_account_uuid='support-actor',
support_session_id=grant_jti_hash,
),
workspace=WorkspaceContext(
workspace_uuid='workspace-a',
membership_uuid=None,
role='owner',
permissions=frozenset({'resource.manage'}),
),
)
first_context = support_context('a' * 64)
second_context = support_context('b' * 64)
sessions: dict[str, dict] = {'session-test': {'status': 'waiting'}}
_bind_session_scope(sessions['session-test'], first_context)
assert _get_owned_session(sessions, 'session-test', second_context) is None
assert _pop_owned_session(sessions, 'session-test', second_context) is None
assert _get_owned_session(sessions, 'session-test', first_context) is sessions['session-test']
async def test_session_capacity_evicts_oldest_session_in_same_workspace():
owner_context = _request_context()
sessions: dict[str, dict] = {}
@@ -46,6 +46,20 @@ def plugin_router_cls():
yield PluginsRouterGroup
@pytest.mark.asyncio
async def test_authenticated_plugin_resource_fences_injected_workspace_context(plugin_router_cls):
connector = SimpleNamespace(
require_workspace_context=AsyncMock(return_value=CONTEXT),
)
router = object.__new__(plugin_router_cls)
router.ap = SimpleNamespace(plugin_connector=connector)
result = await router._require_authenticated_plugin_runtime_context(CONTEXT)
assert result == CONTEXT
connector.require_workspace_context.assert_awaited_once_with(CONTEXT)
@pytest.mark.asyncio
async def test_public_plugin_asset_route_is_disabled_for_multi_workspace_policy(plugin_router_cls):
connector = SimpleNamespace(
@@ -11,6 +11,7 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from langbot.pkg.cloud.launch import SpaceLaunchError, SpaceLaunchService
from langbot.pkg.cloud.support_admin import SupportAdminReplayError
pytestmark = pytest.mark.asyncio
@@ -57,9 +58,21 @@ def _service(private_key: Ed25519PrivateKey, *, now: int) -> SpaceLaunchService:
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
consumed: set[str] = set()
class DurableSupportAdminService:
async def consume_launch_grant(self, **kwargs):
grant_hash = kwargs['grant_jti_hash']
if grant_hash in consumed:
raise SupportAdminReplayError('already consumed')
consumed.add(grant_hash)
return SimpleNamespace(token='support-admin-token')
app = SimpleNamespace(
deployment=SimpleNamespace(multi_workspace_enabled=True, verification_key_id=KEY_ID),
workspace_service=SimpleNamespace(instance_uuid=INSTANCE_UUID),
logger=SimpleNamespace(info=lambda *args, **kwargs: None),
support_admin_session_service=DurableSupportAdminService(),
instance_config=SimpleNamespace(
data={
'space': {
@@ -86,6 +99,81 @@ async def test_consumes_valid_workspace_launch_assertion_once():
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
async def test_consumes_admin_owner_launch_once_and_validates_claims():
private_key = Ed25519PrivateKey.generate()
now = int(time.time())
service = _service(private_key, now=now)
claims = _claims(now=now)
claims['kind'] = 'workspace.support_admin_launch'
claims['payload'].update(
{
'launch_mode': 'support_admin',
'principal_type': 'support_admin',
'actor_account_uuid': '33333333-3333-4333-8333-333333333333',
'effective_role': 'owner',
}
)
claims['payload'].pop('account_uuid')
token = _sign(private_key, claims)
launch = await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
assert launch == {
'workspace_uuid': WORKSPACE_UUID,
'launch_mode': 'support_admin',
'actor_account_uuid': '33333333-3333-4333-8333-333333333333',
'effective_role': 'owner',
'grant_jti_hash': launch['grant_jti_hash'],
'support_admin_token': 'support-admin-token',
}
with pytest.raises(SpaceLaunchError, match='already been consumed'):
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
invalid = _claims(now=now)
invalid['kind'] = 'workspace.support_admin_launch'
invalid['payload'].update(
{
'launch_mode': 'support_admin',
'principal_type': 'support_admin',
'actor_account_uuid': '33333333-3333-4333-8333-333333333333',
'effective_role': 'member',
}
)
invalid['payload'].pop('account_uuid')
with pytest.raises(SpaceLaunchError, match='effective role'):
await service.consume_assertion(_sign(private_key, invalid), expected_workspace_uuid=WORKSPACE_UUID)
too_long = _claims(now=now)
too_long['kind'] = 'workspace.support_admin_launch'
too_long['exp'] = now + 91
too_long['payload'].update(
{
'launch_mode': 'support_admin',
'principal_type': 'support_admin',
'actor_account_uuid': '33333333-3333-4333-8333-333333333333',
'effective_role': 'owner',
}
)
too_long['payload'].pop('account_uuid')
with pytest.raises(SpaceLaunchError, match='lifetime exceeds 90 seconds'):
await service.consume_assertion(_sign(private_key, too_long), expected_workspace_uuid=WORKSPACE_UUID)
impersonating = _claims(now=now)
impersonating['kind'] = 'workspace.support_admin_launch'
impersonating['payload'].update(
{
'launch_mode': 'support_admin',
'principal_type': 'support_admin',
'actor_account_uuid': '33333333-3333-4333-8333-333333333333',
'effective_role': 'owner',
}
)
with pytest.raises(SpaceLaunchError, match='customer Account'):
await service.consume_assertion(_sign(private_key, impersonating), expected_workspace_uuid=WORKSPACE_UUID)
async def test_replay_cache_does_not_scan_all_live_assertions(monkeypatch):
private_key = Ed25519PrivateKey.generate()
now = int(time.time())
+30 -3
View File
@@ -106,15 +106,31 @@ class TestBuildHeartbeatPayload:
side_effect=AssertionError('Cloud heartbeat must not issue per-tenant COUNTs')
)
ap.pipeline_mgr = SimpleNamespace(
_pipelines_by_key={'pipeline-a': object(), 'pipeline-b': object()},
_pipelines_by_key={
('instance-a', 'workspace-a', 'pipeline-a'): object(),
('instance-a', 'workspace-a', 'pipeline-b'): object(),
},
)
ap.platform_mgr._bots_by_key = {
('instance-a', 'workspace-a', 'bot-a'): object(),
}
ap.tool_mgr = SimpleNamespace(
mcp_tool_loader=SimpleNamespace(
_sessions={'mcp-a': object(), 'mcp-b': object(), 'mcp-c': object()},
_sessions={
('instance-a', 'workspace-a', 1, 'mcp-a'): object(),
('instance-a', 'workspace-a', 1, 'mcp-b'): object(),
('instance-a', 'workspace-a', 1, 'mcp-c'): object(),
},
),
)
ap.rag_mgr = SimpleNamespace(
knowledge_bases={'kb-a': object()},
knowledge_bases={('workspace-a', 'kb-a'): object()},
)
ap.plugin_connector._workspace_installations = {
'workspace-a': {'plugin-a', 'plugin-b'},
}
ap.workspace_service.list_active_execution_bindings = AsyncMock(
return_value=[SimpleNamespace(workspace_uuid='workspace-a')],
)
payload = await heartbeat.build_heartbeat_payload(ap)
@@ -124,6 +140,17 @@ class TestBuildHeartbeatPayload:
assert features['mcp_server_count'] == 3
assert features['knowledge_base_count'] == 1
assert features['bot_count'] == 1
assert features['workspace_resources'] == [
{
'workspace_uuid': 'workspace-a',
'bot_count': 1,
'pipeline_count': 2,
'knowledge_base_count': 1,
'plugin_count': 2,
'mcp_server_count': 3,
'extension_count': 5,
}
]
ap.persistence_mgr.execute_async.assert_not_awaited()
@pytest.mark.asyncio