fix(webui): keep credentials out of service worker caches
This commit is contained in:
@@ -717,7 +717,7 @@ async def test_token_issue_route_requires_secret_when_static_token_configured(bu
|
||||
bus,
|
||||
port=port,
|
||||
token="static-token",
|
||||
tokenIssuePath="/auth/token",
|
||||
tokenIssuePath="/custom-token",
|
||||
websocketRequiresToken=True,
|
||||
)
|
||||
|
||||
@@ -725,15 +725,16 @@ async def test_token_issue_route_requires_secret_when_static_token_configured(bu
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
try:
|
||||
denied = await _http_get(f"http://127.0.0.1:{port}/auth/token")
|
||||
denied = await _http_get(f"http://127.0.0.1:{port}/custom-token")
|
||||
assert denied.status_code == 401
|
||||
|
||||
allowed = await _http_get(
|
||||
f"http://127.0.0.1:{port}/auth/token",
|
||||
f"http://127.0.0.1:{port}/custom-token",
|
||||
headers={"Authorization": "Bearer static-token"},
|
||||
)
|
||||
assert allowed.status_code == 200
|
||||
assert allowed.json()["token"].startswith("nbwt_")
|
||||
assert allowed.headers["Cache-Control"] == "no-store"
|
||||
finally:
|
||||
await channel.stop()
|
||||
await server_task
|
||||
@@ -3803,6 +3804,7 @@ async def test_token_issue_rejects_when_at_capacity(bus: MagicMock) -> None:
|
||||
headers={"Authorization": "Bearer s"},
|
||||
)
|
||||
assert resp.status_code == 429
|
||||
assert resp.headers["Cache-Control"] == "no-store"
|
||||
data = resp.json()
|
||||
assert "error" in data
|
||||
finally:
|
||||
|
||||
@@ -227,6 +227,7 @@ async def test_bootstrap_returns_token_for_localhost(
|
||||
try:
|
||||
resp = await _http_get("http://127.0.0.1:29901/webui/bootstrap")
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers["Cache-Control"] == "no-store"
|
||||
body = resp.json()
|
||||
assert body["token"].startswith("nbwt_")
|
||||
assert channel.gateway.tokens.issued_token_audiences[body["token"]] == "webui"
|
||||
|
||||
@@ -100,6 +100,7 @@ def http_json_response(
|
||||
*,
|
||||
status: int = 200,
|
||||
accept_encoding: str | None = None,
|
||||
extra_headers: list[tuple[str, str]] | None = None,
|
||||
) -> Response:
|
||||
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
headers = [
|
||||
@@ -112,6 +113,8 @@ def http_json_response(
|
||||
if len(body) >= _JSON_GZIP_MIN_BYTES and accepts_gzip(accept_encoding):
|
||||
body = gzip.compress(body, compresslevel=_JSON_GZIP_LEVEL, mtime=0)
|
||||
headers.append(("Content-Encoding", "gzip"))
|
||||
if extra_headers:
|
||||
headers.extend(extra_headers)
|
||||
headers.append(("Content-Length", str(len(body))))
|
||||
reason = http.HTTPStatus(status).phrase
|
||||
return Response(status, reason, Headers(headers), body)
|
||||
|
||||
@@ -121,6 +121,7 @@ from nanobot.webui.workspaces import WebUIWorkspaceController
|
||||
_SLOW_WEBUI_HTTP_LOG_MS = 1_000
|
||||
_WEBUI_MUTATION_PAYLOAD_ATTR = "_nanobot_webui_mutation_payload"
|
||||
_WEBUI_MUTATION_REQUEST_ATTR = "_nanobot_webui_mutation_request"
|
||||
_NO_STORE_HEADERS = [("Cache-Control", "no-store")]
|
||||
|
||||
_WEBUI_MUTATION_PATHS = {
|
||||
"automation.enable": "/api/webui/automations/enable",
|
||||
@@ -547,9 +548,16 @@ class GatewayHTTPHandler:
|
||||
"too many outstanding issued tokens ({}), rejecting issuance",
|
||||
len(self.tokens.issued_tokens),
|
||||
)
|
||||
return _http_json_response({"error": "too many outstanding tokens"}, status=429)
|
||||
return _http_json_response(
|
||||
{"error": "too many outstanding tokens"},
|
||||
status=429,
|
||||
extra_headers=_NO_STORE_HEADERS,
|
||||
)
|
||||
token_value = self.tokens.issue_token(self.config.token_ttl_s)
|
||||
return _http_json_response(token_response_payload(token_value, self.config.token_ttl_s))
|
||||
return _http_json_response(
|
||||
token_response_payload(token_value, self.config.token_ttl_s),
|
||||
extra_headers=_NO_STORE_HEADERS,
|
||||
)
|
||||
|
||||
# -- Bootstrap ----------------------------------------------------------
|
||||
|
||||
@@ -579,7 +587,7 @@ class GatewayHTTPHandler:
|
||||
"runtime_surface": self._runtime_surface,
|
||||
"runtime_capabilities": self._capabilities,
|
||||
}
|
||||
return _http_json_response(payload)
|
||||
return _http_json_response(payload, extra_headers=_NO_STORE_HEADERS)
|
||||
|
||||
api_token_allowed = bool(secret) or is_local_browser
|
||||
if not self.tokens.can_issue(include_api_token=api_token_allowed):
|
||||
@@ -587,6 +595,7 @@ class GatewayHTTPHandler:
|
||||
json.dumps({"error": "too many outstanding tokens"}).encode("utf-8"),
|
||||
status=429,
|
||||
content_type="application/json; charset=utf-8",
|
||||
extra_headers=_NO_STORE_HEADERS,
|
||||
)
|
||||
token = self.tokens.issue_token(self.config.token_ttl_s, audience="webui")
|
||||
api_token = (
|
||||
@@ -611,7 +620,7 @@ class GatewayHTTPHandler:
|
||||
}
|
||||
if api_token is not None:
|
||||
payload["api_token"] = api_token
|
||||
return _http_json_response(payload)
|
||||
return _http_json_response(payload, extra_headers=_NO_STORE_HEADERS)
|
||||
|
||||
def _bootstrap_ws_url(self, request: Any) -> str:
|
||||
headers = getattr(request, "headers", {}) or {}
|
||||
|
||||
Reference in New Issue
Block a user