mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-12 22:00:57 +00:00
fix(cloud): retain launch replay records through clock skew
This commit is contained in:
@@ -7,6 +7,7 @@ import datetime
|
|||||||
import hashlib
|
import hashlib
|
||||||
import heapq
|
import heapq
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
@@ -124,7 +125,7 @@ class SpaceLaunchService:
|
|||||||
*,
|
*,
|
||||||
expected_workspace_uuid: str | None = None,
|
expected_workspace_uuid: str | None = None,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
claims = self._verify_assertion(assertion)
|
claims, clock_skew_seconds = self._verify_assertion(assertion)
|
||||||
payload = claims.get('payload')
|
payload = claims.get('payload')
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
raise SpaceLaunchError('Launch assertion payload must be a JSON object')
|
raise SpaceLaunchError('Launch assertion payload must be a JSON object')
|
||||||
@@ -139,14 +140,15 @@ class SpaceLaunchService:
|
|||||||
raise SpaceLaunchError('Launch assertion return path is invalid')
|
raise SpaceLaunchError('Launch assertion return path is invalid')
|
||||||
if expected_workspace_uuid is not None and workspace_uuid != expected_workspace_uuid:
|
if expected_workspace_uuid is not None and workspace_uuid != expected_workspace_uuid:
|
||||||
raise SpaceLaunchError('Launch assertion targets another Workspace')
|
raise SpaceLaunchError('Launch assertion targets another Workspace')
|
||||||
await self._consume_jti(_required_string(claims, 'jti'), _required_int(claims, 'exp', minimum=1))
|
replay_retention_expires_at = _required_int(claims, 'exp', minimum=1) + math.ceil(clock_skew_seconds)
|
||||||
|
await self._consume_jti(_required_string(claims, 'jti'), replay_retention_expires_at)
|
||||||
return {
|
return {
|
||||||
'account_uuid': account_uuid,
|
'account_uuid': account_uuid,
|
||||||
'workspace_uuid': workspace_uuid,
|
'workspace_uuid': workspace_uuid,
|
||||||
'return_path': return_path,
|
'return_path': return_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _verify_assertion(self, token: str) -> dict[str, typing.Any]:
|
def _verify_assertion(self, token: str) -> tuple[dict[str, typing.Any], float]:
|
||||||
if not getattr(getattr(self.ap, 'deployment', None), 'multi_workspace_enabled', False):
|
if not getattr(getattr(self.ap, 'deployment', None), 'multi_workspace_enabled', False):
|
||||||
raise SpaceLaunchError('Space direct launch requires verified Cloud mode')
|
raise SpaceLaunchError('Space direct launch requires verified Cloud mode')
|
||||||
public_key, key_id, clock_skew_seconds = self._trust_config()
|
public_key, key_id, clock_skew_seconds = self._trust_config()
|
||||||
@@ -197,7 +199,7 @@ class SpaceLaunchService:
|
|||||||
raise SpaceLaunchError('Launch assertion is expired')
|
raise SpaceLaunchError('Launch assertion is expired')
|
||||||
if expires_at <= max(issued_at, not_before):
|
if expires_at <= max(issued_at, not_before):
|
||||||
raise SpaceLaunchError('Launch assertion expiry must follow issue time')
|
raise SpaceLaunchError('Launch assertion expiry must follow issue time')
|
||||||
return claims
|
return claims, clock_skew_seconds
|
||||||
|
|
||||||
def _trust_config(self) -> tuple[Ed25519PublicKey, str, float]:
|
def _trust_config(self) -> tuple[Ed25519PublicKey, str, float]:
|
||||||
data = getattr(getattr(self.ap, 'instance_config', None), 'data', {}) or {}
|
data = getattr(getattr(self.ap, 'instance_config', None), 'data', {}) or {}
|
||||||
|
|||||||
@@ -91,6 +91,22 @@ async def test_consumes_valid_workspace_launch_assertion_once():
|
|||||||
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
|
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_consumed_assertion_remains_blocked_through_clock_skew_window():
|
||||||
|
private_key = Ed25519PrivateKey.generate()
|
||||||
|
now = int(time.time())
|
||||||
|
service = _service(private_key, now=now)
|
||||||
|
claims = _claims(now=now)
|
||||||
|
claims['iat'] = now - 10
|
||||||
|
claims['nbf'] = now - 10
|
||||||
|
claims['exp'] = now - 1
|
||||||
|
token = _sign(private_key, claims)
|
||||||
|
|
||||||
|
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
|
||||||
|
|
||||||
|
with pytest.raises(SpaceLaunchError, match='already been consumed'):
|
||||||
|
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
|
||||||
|
|
||||||
|
|
||||||
async def test_replay_cache_does_not_scan_all_live_assertions(monkeypatch):
|
async def test_replay_cache_does_not_scan_all_live_assertions(monkeypatch):
|
||||||
private_key = Ed25519PrivateKey.generate()
|
private_key = Ed25519PrivateKey.generate()
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
|
|||||||
Reference in New Issue
Block a user