From 6187a98dc7cbd95df443031d03d8eb43e5d59c9f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 18 Jun 2026 22:01:48 -0400 Subject: [PATCH] Enable CORS on ToolShed read-only API ToolShed FastAPI app emitted no CORS headers, blocking browser clients (e.g. Galaxy Workflows VS Code extension on vscode.dev) from reading its public read-only JSON API. Opt the public read-only metadata endpoints into the existing per-route allow_cors mechanism (reflected origin, no credentials, auto OPTIONS preflight) rather than blanketing the whole app: /api/tools, /api/tools/{tool_id}/versions/{tool_version}, /api/repositories, /api/repositories/get_ordered_installable_revisions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/functional/test_shed_cors.py | 37 +++++++++++++++++++ lib/tool_shed/webapp/api2/repositories.py | 2 + lib/tool_shed/webapp/api2/tools.py | 2 + 3 files changed, 41 insertions(+) create mode 100644 lib/tool_shed/test/functional/test_shed_cors.py diff --git a/lib/tool_shed/test/functional/test_shed_cors.py b/lib/tool_shed/test/functional/test_shed_cors.py new file mode 100644 index 00000000000..2c96d5000d8 --- /dev/null +++ b/lib/tool_shed/test/functional/test_shed_cors.py @@ -0,0 +1,37 @@ +import requests + +from ..base.api import ShedApiTestCase + +ORIGIN = "https://vscode.dev" + + +class TestShedCorsApi(ShedApiTestCase): + def test_preflight_allows_cross_origin_get(self): + response = requests.options( + f"{self.url}/api/repositories", + headers={ + "Origin": ORIGIN, + "Access-Control-Request-Method": "GET", + }, + ) + assert response.status_code == 200 + assert response.headers.get("access-control-allow-origin") == ORIGIN + + def test_get_response_reflects_origin(self): + response = requests.get(f"{self.url}/api/repositories", headers={"Origin": ORIGIN}) + response.raise_for_status() + assert response.headers.get("access-control-allow-origin") == ORIGIN + + def test_credentials_not_exposed_cross_origin(self): + # No Access-Control-Allow-Credentials keeps the cookie-authenticated surface + # unreadable cross-origin: a browser will not expose a credentialed response + # without that header. + response = requests.get(f"{self.url}/api/repositories", headers={"Origin": ORIGIN}) + response.raise_for_status() + assert "access-control-allow-credentials" not in response.headers + + def test_uncovered_endpoint_has_no_cors(self): + # CORS is opt-in per route; an endpoint not marked allow_cors emits no header. + response = requests.get(f"{self.url}/api/ga4gh/trs/v2/service-info", headers={"Origin": ORIGIN}) + response.raise_for_status() + assert "access-control-allow-origin" not in response.headers diff --git a/lib/tool_shed/webapp/api2/repositories.py b/lib/tool_shed/webapp/api2/repositories.py index 6c00fdea005..7586177e8a2 100644 --- a/lib/tool_shed/webapp/api2/repositories.py +++ b/lib/tool_shed/webapp/api2/repositories.py @@ -131,6 +131,7 @@ class FastAPIRepositories: "/api/repositories", description="Get a list of repositories or perform a search.", operation_id="repositories__index", + allow_cors=True, ) def index( self, @@ -271,6 +272,7 @@ class FastAPIRepositories: "/api/repositories/get_ordered_installable_revisions", description="Get an ordered list of the repository changeset revisions that are installable", operation_id="repositories__get_ordered_installable_revisions", + allow_cors=True, ) def get_ordered_installable_revisions( self, diff --git a/lib/tool_shed/webapp/api2/tools.py b/lib/tool_shed/webapp/api2/tools.py index 6c099b1253e..bbc9ace773c 100644 --- a/lib/tool_shed/webapp/api2/tools.py +++ b/lib/tool_shed/webapp/api2/tools.py @@ -68,6 +68,7 @@ class FastAPITools: @router.get( "/api/tools", operation_id="tools__index", + allow_cors=True, ) def index( self, @@ -146,6 +147,7 @@ class FastAPITools: "/api/tools/{tool_id}/versions/{tool_version}", operation_id="tools__parameter_model", summary="Return Galaxy's meta model description of the tool's inputs", + allow_cors=True, ) def show_tool( self,