diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index c4647787209..e02b6b0c901 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -225,7 +225,7 @@ async def host_header_middleware(request: Request, call_next): async def auth_middleware(request: Request, call_next): """Require the session token on all /api/ routes except the public list.""" path = request.url.path - if path.startswith("/api/") and path not in _PUBLIC_API_PATHS and not path.startswith("/api/plugins/"): + if path.startswith("/api/") and path not in _PUBLIC_API_PATHS: if not _has_valid_session_token(request): return JSONResponse( status_code=401, diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index f2aed86d426..bf5551f9e0b 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1826,6 +1826,49 @@ class TestNormaliseThemeExtensions: assert r["componentStyles"]["card"] == {"opacity": "0.8", "zIndex": "5"} + + + +class TestPluginAPIAuth: + """Tests that plugin API routes require the session token (issue #19533).""" + + @pytest.fixture(autouse=True) + def _setup_test_client(self, monkeypatch, _isolate_hermes_home): + """Create a TestClient without the session token header.""" + try: + from starlette.testclient import TestClient + except ImportError: + pytest.skip("fastapi/starlette not installed") + + import hermes_state + from hermes_constants import get_hermes_home + from hermes_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN + + monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", get_hermes_home() / "state.db") + + self.client = TestClient(app) + self.auth_client = TestClient(app) + self.auth_client.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN + + def test_plugin_route_requires_auth(self): + """Plugin API routes should return 401 without a valid session token.""" + # Use a known plugin route (kanban board) + resp = self.client.get("/api/plugins/kanban/board") + assert resp.status_code == 401 + + def test_plugin_route_allows_auth(self): + """Plugin API routes should work with a valid session token.""" + # This test verifies the fix doesn't break authenticated access. + # The kanban plugin may not be loaded in the test environment, + # so we accept 200 (plugin loaded) or 404 (plugin not mounted). + resp = self.auth_client.get("/api/plugins/kanban/board") + assert resp.status_code in (200, 404) + + def test_plugin_post_requires_auth(self): + """Plugin POST routes should return 401 without a valid session token.""" + resp = self.client.post("/api/plugins/kanban/tasks", json={"title": "test"}) + assert resp.status_code == 401 + class TestDashboardPluginManifestExtensions: """Tests for the extended plugin manifest fields (tab.override, tab.hidden, slots) read by _discover_dashboard_plugins()."""