diff --git a/server/opensandbox_server/api/lifecycle.py b/server/opensandbox_server/api/lifecycle.py index 39bbc62b8..391ec1889 100644 --- a/server/opensandbox_server/api/lifecycle.py +++ b/server/opensandbox_server/api/lifecycle.py @@ -21,7 +21,7 @@ from typing import List, Optional -from fastapi import APIRouter, Body, Header, Query, Request, status +from fastapi import APIRouter, Body, Header, HTTPException, Query, Request, status from fastapi.responses import Response from opensandbox_server.extensions import validate_extensions @@ -45,6 +45,7 @@ Snapshot, SnapshotFilter, ) +from opensandbox_server.services.constants import SandboxErrorCodes from opensandbox_server.services.factory import create_sandbox_service from opensandbox_server.services.snapshot_service import create_snapshot_service @@ -504,6 +505,7 @@ def delete_snapshot( response_model_exclude_none=True, responses={ 200: {"description": "Endpoint retrieved successfully"}, + 400: {"model": ErrorResponse, "description": "The request was invalid or malformed"}, 401: {"model": ErrorResponse, "description": "Authentication credentials are missing or invalid"}, 403: {"model": ErrorResponse, "description": "The authenticated user lacks permission for this operation"}, 404: {"model": ErrorResponse, "description": "The requested resource does not exist"}, @@ -546,6 +548,18 @@ def get_sandbox_endpoint( HTTPException: If sandbox not found, endpoint not available, or signed routes are not supported by the runtime/configuration (400). """ + if use_server_proxy and expires is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "code": SandboxErrorCodes.INVALID_PARAMETER, + "message": ( + "use_server_proxy cannot be combined with expires; " + "signed endpoints require the gateway route." + ), + }, + ) + # Delegate to the service layer for endpoint resolution endpoint = sandbox_service.get_endpoint(sandbox_id, port, expires=expires) diff --git a/server/tests/test_routes_endpoint_behavior.py b/server/tests/test_routes_endpoint_behavior.py index 6d71c6853..40afedb6c 100644 --- a/server/tests/test_routes_endpoint_behavior.py +++ b/server/tests/test_routes_endpoint_behavior.py @@ -91,7 +91,34 @@ def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: ) assert response.status_code == 200 - assert response.json()["endpoint"] == "sandbox.example.com/opensandbox/sandboxes/sbx-001/proxy/44772" + assert ( + response.json()["endpoint"] + == "sandbox.example.com/opensandbox/sandboxes/sbx-001/proxy/44772" + ) + + +def test_get_endpoint_rejects_server_proxy_with_expires( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + class StubService: + @staticmethod + def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: + raise AssertionError("signed endpoint resolution should not run") + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.get( + "/v1/sandboxes/sbx-001/endpoints/44772", + params={"use_server_proxy": "true", "expires": "2000000000"}, + headers=auth_headers, + ) + + assert response.status_code == 400 + payload = response.json() + assert payload["code"] == "SANDBOX::INVALID_PARAMETER" + assert "use_server_proxy cannot be combined with expires" in payload["message"] def test_get_endpoint_rejects_non_numeric_port( diff --git a/specs/sandbox-lifecycle.yml b/specs/sandbox-lifecycle.yml index 9809d7d99..61eeda3fd 100644 --- a/specs/sandbox-lifecycle.yml +++ b/specs/sandbox-lifecycle.yml @@ -640,7 +640,7 @@ paths: maximum: 65535 - name: use_server_proxy in: query - description: Whether to return a server-proxied URL + description: Whether to return a server-proxied URL. Cannot be combined with `expires`. schema: type: boolean default: false @@ -652,7 +652,7 @@ paths: is **Linux / Unix epoch seconds** — a decimal `uint64` count of **whole seconds** since the Unix epoch (`1970-01-01 00:00:00` UTC, same as POSIX / `time(2)`), not milliseconds. Normalized to `expires_b36` for the four-segment route token. Omit to - get the unsigned/legacy response shape. + get the unsigned/legacy response shape. Cannot be combined with `use_server_proxy=true`. schema: type: string pattern: '^(0|[1-9][0-9]*)$'