feat(api/attackers): per-attacker SSE events stream
GET /api/v1/attackers/{uuid}/events streams behavioural events for
one attacker. Mirrors decnet/web/router/topology/api_events.py
end-to-end: ?token= auth, require_stream_viewer gate,
sse_connection_slot per-user cap, snapshot-on-connect, three bus
subscriptions (attacker.observation.>, attacker.fingerprint_rotated,
attacker.scored) merged through asyncio.Queue, 15s keepalive,
request.is_disconnected() exit, finally task cancellation.
Per-attacker filter keys on payload['attacker_uuid'] which the
profiler worker stamps onto every published payload (Phase 5 P5.0
amendment) — O(1) drop without a repo round-trip per event.
_sse_name_for derives SSE event names:
attacker.observation.<primitive> → observation.<primitive>
attacker.fingerprint_rotated → fingerprint.rotated
attacker.scored → attacker.scored
10 tests cover snapshot, live forward, per-attacker filter (drops
other attackers' events), fingerprint.rotated forward, 404, 401, and
the sse-name derivation across all four cases. Topology events
regression green.
This commit is contained in:
27
decnet/web/router/attackers/_guards.py
Normal file
27
decnet/web/router/attackers/_guards.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Shared helpers for the per-attacker routes.
|
||||
|
||||
Currently houses the 404 guard used by the SSE events stream
|
||||
(:mod:`api_events`). Mirrors the topology router's
|
||||
``_guards.get_topology_or_404`` shape so a future grep for "guard"
|
||||
finds both.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from decnet.web.dependencies import repo
|
||||
|
||||
|
||||
async def get_attacker_or_404(attacker_uuid: str) -> dict[str, Any]:
|
||||
"""Fetch an Attacker row by UUID or raise 404.
|
||||
|
||||
The 404 fires *after* auth (the route's role gate runs first), so
|
||||
an existence probe can't leak a UUID's presence to an
|
||||
unauthenticated caller.
|
||||
"""
|
||||
attacker = await repo.get_attacker_by_uuid(attacker_uuid)
|
||||
if not attacker:
|
||||
raise HTTPException(status_code=404, detail="Attacker not found")
|
||||
return attacker
|
||||
Reference in New Issue
Block a user