Validate an ``X-Session-API-Key`` and return the associated sandbox. Security: This function enforces that session API keys are only valid for RUNNING sandboxes. This is a critical security measure to prevent leaked keys from being used to access user secrets after a san
(session_api_key: str | None)
| 35 | |
| 36 | |
| 37 | async def validate_session_key(session_api_key: str | None) -> SandboxInfo: |
| 38 | """Validate an ``X-Session-API-Key`` and return the associated sandbox. |
| 39 | |
| 40 | Security: |
| 41 | This function enforces that session API keys are only valid for RUNNING |
| 42 | sandboxes. This is a critical security measure to prevent leaked keys |
| 43 | from being used to access user secrets after a sandbox has been paused, |
| 44 | stopped, or deleted. |
| 45 | |
| 46 | Raises: |
| 47 | HTTPException(401): if the key is missing or does not map to a sandbox. |
| 48 | HTTPException(401): if the sandbox is not in RUNNING state. |
| 49 | HTTPException(401): in SAAS mode if the sandbox has no owning user. |
| 50 | """ |
| 51 | if not session_api_key: |
| 52 | raise HTTPException( |
| 53 | status.HTTP_401_UNAUTHORIZED, |
| 54 | detail='X-Session-API-Key header is required', |
| 55 | ) |
| 56 | |
| 57 | # The sandbox service is scoped to users. To look up a sandbox by session |
| 58 | # key (which could belong to *any* user) we need an admin context. This |
| 59 | # is the same pattern used in webhook_router.valid_sandbox(). |
| 60 | state = InjectorState() |
| 61 | setattr(state, USER_CONTEXT_ATTR, ADMIN) |
| 62 | |
| 63 | async with get_sandbox_service(state) as sandbox_service: |
| 64 | sandbox_info = await sandbox_service.get_sandbox_by_session_api_key( |
| 65 | session_api_key |
| 66 | ) |
| 67 | |
| 68 | if sandbox_info is None: |
| 69 | raise HTTPException( |
| 70 | status.HTTP_401_UNAUTHORIZED, detail='Invalid session API key' |
| 71 | ) |
| 72 | |
| 73 | # Security: Reject session keys for non-running sandboxes. |
| 74 | # This prevents leaked keys from being used to access secrets after |
| 75 | # the sandbox has been paused, stopped, or deleted. |
| 76 | if sandbox_info.status != SandboxStatus.RUNNING: |
| 77 | _logger.warning( |
| 78 | 'Session key rejected for non-running sandbox', |
| 79 | extra={ |
| 80 | 'sandbox_id': sandbox_info.id, |
| 81 | 'status': sandbox_info.status.value, |
| 82 | }, |
| 83 | ) |
| 84 | raise HTTPException( |
| 85 | status.HTTP_401_UNAUTHORIZED, |
| 86 | detail='Sandbox is not running', |
| 87 | ) |
| 88 | |
| 89 | if not sandbox_info.created_by_user_id: |
| 90 | if get_global_config().app_mode == AppMode.SAAS: |
| 91 | _logger.error( |
| 92 | 'Sandbox had no user specified', |
| 93 | extra={'sandbox_id': sandbox_info.id}, |
| 94 | ) |