Session-gate the /api/runs history list

This commit is contained in:
yoni
2026-07-21 02:24:06 +00:00
parent a0d0d248f3
commit 6ffe98312d
2 changed files with 40 additions and 3 deletions
+7 -3
View File
@@ -222,10 +222,14 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]:
self.end_headers()
def _handle_api(self, path: str, query: dict[str, list[str]]) -> None:
# The launched run is always viewable with no verification. Only the
# cross-run history list (/api/runs) is gated.
# The launched run is always viewable with no verification. The
# cross-run history list (/api/runs) unlocks its entries only for a
# caller that holds this process's session capability *and* is email
# verified, so merely reaching an exposed --host port never leaks the
# run list (the payload still advertises the count as a teaser).
if path == "/api/runs":
payload = build_runs_payload(state.base_dir, verified=auth.is_verified())
unlocked = self._has_session() and auth.is_verified()
payload = build_runs_payload(state.base_dir, verified=unlocked)
self._send_json(HTTPStatus.OK, payload)
return
if path == "/api/capabilities":
+33
View File
@@ -405,6 +405,39 @@ def test_historical_run_data_requires_verification(
httpd.server_close()
def test_runs_list_requires_session_and_verification(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
launched = _make_run(tmp_path, "launched", status="completed", end_time="2026-01-01T00:00:00Z")
_make_run(tmp_path, "other", status="completed", end_time="2026-01-01T00:00:00Z")
_bundle(tmp_path, monkeypatch)
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: True)
def _runs(cookie: str | None) -> dict[str, object]:
headers = {"Cookie": cookie} if cookie else {}
req = urllib.request.Request(f"{url}/api/runs", headers=headers) # noqa: S310
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server
return dict(json.loads(resp.read()))
httpd, url, token = serve(launched, open_browser=False)
try:
# A cookie-less caller (even with the machine verified) only sees the
# teaser count, never the run entries.
payload = _runs(None)
assert payload["locked"] is True
assert payload["count"] == 2
assert payload["runs"] == []
# With the session cookie and verification, the entries unlock.
payload = _runs(_session_cookie(url, token))
assert payload["locked"] is False
assert {r["name"] for r in payload["runs"]} == {"launched", "other"} # type: ignore[attr-defined]
finally:
httpd.shutdown()
httpd.server_close()
def test_server_rejects_path_traversal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
run_dir = _make_run(tmp_path, "guard", status="completed", end_time="2026-01-01T00:00:00Z")
secret = tmp_path / "secret.txt"