From 7bcaa01222c05d2b8102e6f03b415e2ed964c7e5 Mon Sep 17 00:00:00 2001 From: Leo Le Bleis Date: Sat, 2 May 2026 23:44:33 +0100 Subject: [PATCH] =?UTF-8?q?fix(terminal):=20inline=20WS=20auth=20=E2=80=94?= =?UTF-8?q?=20Header/Query=20deps=20don't=20inject=20on=20WS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastAPI's WebSocket dependency resolver doesn't inject Header() or Query() parameters into shared deps the way HTTP does, so `require_api_key` raised 403 on every WS connection — the credential never reached the dep, even when set in either the X-API-Key header or ?api_key= query. Read directly from the WS scope instead. The HTTP routes still go through the shared dep unchanged. Also drops require_api_key from the terminal_router include — auth is now the WS handler's responsibility for this router. --- src/brew/main.py | 4 +++- src/brew/terminal/router.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/brew/main.py b/src/brew/main.py index aede756..6732dc3 100644 --- a/src/brew/main.py +++ b/src/brew/main.py @@ -223,7 +223,9 @@ def _require_terminal_enabled() -> None: app.include_router( terminal_router, prefix="/api", - dependencies=[Depends(require_api_key), Depends(_require_terminal_enabled)], + # require_api_key is handled inline in the WS handler — FastAPI's WS dep + # resolver doesn't inject Header/Query the same way HTTP does. + dependencies=[Depends(_require_terminal_enabled)], ) if _mcp_enabled: diff --git a/src/brew/terminal/router.py b/src/brew/terminal/router.py index 0e829c3..018d3ec 100644 --- a/src/brew/terminal/router.py +++ b/src/brew/terminal/router.py @@ -4,21 +4,38 @@ from typing import TYPE_CHECKING, Annotated -from fastapi import APIRouter, Depends, WebSocket +from fastapi import APIRouter, Depends, WebSocket, status +from brew.dependencies import get_settings from brew.terminal.dependencies import get_terminal_service if TYPE_CHECKING: + from brew.config import Settings from brew.terminal.service import TerminalService router = APIRouter() +def _extract_api_key(ws: WebSocket) -> str | None: + return ws.headers.get("x-api-key") or ws.query_params.get("api_key") + + @router.websocket("/terminal/ws") async def terminal_ws( ws: WebSocket, service: Annotated[TerminalService, Depends(get_terminal_service)], + settings: Annotated[Settings, Depends(get_settings)], ) -> None: + # Auth inline: include_router(dependencies=[...]) propagates deps to WS + # routes, but FastAPI's WS dependency resolver doesn't inject Header/Query + # parameters the same way HTTP does, so the shared `require_api_key` dep + # can't read the credential. Read directly from the WS scope instead. + if settings.api_key is not None: + provided = _extract_api_key(ws) + if provided is None or provided != settings.api_key.get_secret_value(): + await ws.close(code=status.WS_1008_POLICY_VIOLATION) + return + await ws.accept() async with service.attached() as session: await session.run(ws)