Authenticate sockets by header or one-time ticket instead of ?token=
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
@@ -521,6 +521,7 @@ async def test_the_live_socket_ends_when_its_client_goes(
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.app = client.app
|
||||
self.headers: dict[str, str] = {}
|
||||
self.frames = ['{"type": "media", "names": []}']
|
||||
self.sent: list[str] = []
|
||||
|
||||
|
||||
@@ -892,6 +892,18 @@ def test_a_panels_socket_carries_only_what_it_draws(
|
||||
# A person's credential is not bounded at all.
|
||||
assert panel_scope(superuser_token_headers["Authorization"][7:], client.app) is None
|
||||
|
||||
# It opens the socket with a ticket like any browser, and the ticket
|
||||
# carries the panel's bound rather than widening it.
|
||||
ticket = client.post(
|
||||
f"{settings.API_V1_STR}/flows/ws-ticket",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert ticket.status_code == 200, ticket.text
|
||||
with client.websocket_connect(
|
||||
f"{settings.API_V1_STR}/flows/ws?ticket={ticket.json()['ticket']}"
|
||||
) as ws:
|
||||
assert ws.receive_json()["nodes"] == []
|
||||
|
||||
|
||||
def test_a_panel_fetches_only_the_media_its_tiles_are_showing(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""A socket's credential comes as a header or a one-time ticket, not in its URL."""
|
||||
|
||||
from time import monotonic
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.flow.remote import PROTOCOL
|
||||
|
||||
FLOWS = f"{settings.API_V1_STR}/flows"
|
||||
ATTACH = f"{settings.API_V1_STR}/workers/attach"
|
||||
|
||||
|
||||
def ticket(client: TestClient, headers: dict[str, str]) -> str:
|
||||
answer = client.post(f"{FLOWS}/ws-ticket", headers=headers)
|
||||
assert answer.status_code == 200, answer.text
|
||||
return str(answer.json()["ticket"])
|
||||
|
||||
|
||||
def opens(client: TestClient, url: str, headers: dict[str, str] | None = None) -> bool:
|
||||
try:
|
||||
with client.websocket_connect(url, headers=headers or {}) as ws:
|
||||
assert ws.receive_json()["type"] == "snapshot"
|
||||
except WebSocketDisconnect:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def test_the_live_socket_takes_a_header_and_still_a_token(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
assert opens(client, f"{FLOWS}/ws", superuser_token_headers)
|
||||
token = superuser_token_headers["Authorization"][7:]
|
||||
assert opens(client, f"{FLOWS}/ws?token={token}")
|
||||
assert not opens(client, f"{FLOWS}/ws")
|
||||
|
||||
|
||||
def test_a_ticket_opens_the_socket_once(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
once = ticket(client, superuser_token_headers)
|
||||
assert opens(client, f"{FLOWS}/ws?ticket={once}")
|
||||
assert not opens(client, f"{FLOWS}/ws?ticket={once}")
|
||||
|
||||
|
||||
def test_a_ticket_lapses_after_thirty_seconds(
|
||||
client: TestClient,
|
||||
superuser_token_headers: dict[str, str],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
stale = ticket(client, superuser_token_headers)
|
||||
later = monotonic() + 31
|
||||
monkeypatch.setattr("fluksio.api.socket_auth.monotonic", lambda: later)
|
||||
assert not opens(client, f"{FLOWS}/ws?ticket={stale}")
|
||||
|
||||
|
||||
def test_a_worker_attaches_with_a_header_and_still_a_token(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
token = client.post(
|
||||
f"{settings.API_V1_STR}/workers/tokens",
|
||||
headers=superuser_token_headers,
|
||||
json={"name": "header-box"},
|
||||
).json()["token"]
|
||||
hello = {"op": "hello", "protocol": PROTOCOL}
|
||||
bearer = {"Authorization": f"Bearer {token}"}
|
||||
for url, headers in ((ATTACH, bearer), (f"{ATTACH}?token={token}", {})):
|
||||
with client.websocket_connect(url, headers=headers) as ws:
|
||||
ws.send_json(hello)
|
||||
assert ws.receive_json()["op"] == "welcome"
|
||||
Reference in New Issue
Block a user