more admin stuff 2
This commit is contained in:
46
app/security/active_sessions.py
Normal file
46
app/security/active_sessions.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""
|
||||
In-Memory-Registry der laufenden SSH/RDP-Sitzungen dieses Server-Prozesses.
|
||||
|
||||
Wird von app/ssh_proxy/terminal_ws.py und app/rdp_proxy/ws_tunnel.py beim
|
||||
Sitzungsstart befuellt (Referenz auf den eigenen asyncio.Task) und beim
|
||||
Sitzungsende wieder entfernt. Basis fuer die Superadmin-'Sessionview'
|
||||
(app/admin/routes.py: GET /admin/sessions, POST /admin/sessions/{id}/terminate)
|
||||
-- 'Beenden' funktioniert per asyncio.Task.cancel(), was den regulaeren
|
||||
finally-Cleanup-Pfad der jeweiligen WS-Route ausloest (DB-Update, Audit-
|
||||
Log-Eintrag, WebSocket schliessen), genau wie bei einem normalen Logout.
|
||||
|
||||
Bewusst rein prozesslokal (kein Redis/DB-Backing): bei einem einzelnen
|
||||
uvicorn-Worker (Standard-Deployment dieses Projekts, siehe ansible/) sieht
|
||||
und beendet jeder Superadmin-Request alle laufenden Sitzungen. Bei mehreren
|
||||
Worker-Prozessen sind nur die Sitzungen DIESES Workers 'killable' -- die
|
||||
Sessionview zeigt das ueber das 'killable'-Feld pro Sitzung an, statt einen
|
||||
Beenden-Versuch fehlschlagen zu lassen."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActiveSession:
|
||||
session_id: int
|
||||
task: asyncio.Task
|
||||
|
||||
|
||||
_active: dict[int, ActiveSession] = {}
|
||||
|
||||
|
||||
def register(session_id: int, task: asyncio.Task) -> None:
|
||||
_active[session_id] = ActiveSession(session_id=session_id, task=task)
|
||||
|
||||
|
||||
def unregister(session_id: int) -> None:
|
||||
_active.pop(session_id, None)
|
||||
|
||||
|
||||
def get(session_id: int) -> ActiveSession | None:
|
||||
return _active.get(session_id)
|
||||
|
||||
|
||||
def all_ids() -> set[int]:
|
||||
return set(_active.keys())
|
||||
71
app/security/log_stream.py
Normal file
71
app/security/log_stream.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""
|
||||
Ring-Buffer + Pub/Sub fuer das Live-'Verbindungslog' im Admin-Bereich.
|
||||
|
||||
Haengt sich als zusaetzlicher logging.Handler an den Root-Logger, haelt die
|
||||
letzten LOG-Zeilen im Prozessspeicher (fuer den initialen Replay beim
|
||||
Oeffnen des Tabs) und verteilt jede neue Zeile an alle aktuell verbundenen
|
||||
WebSocket-Abonnenten (siehe app/admin/routes.py, WS /admin/ws/logs).
|
||||
|
||||
Bewusst KEINE Persistenz in der DB -- das ist ein reiner Live-Tail (wie
|
||||
'journalctl -f'), kein durchsuchbares Archiv. Rein prozesslokal: bei mehreren
|
||||
Worker-Prozessen sieht ein Abonnent nur die Logs des Worker-Prozesses, der
|
||||
seine WebSocket-Verbindung bedient.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
MAX_BUFFER_LINES = 1000
|
||||
_FORMATTER = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
|
||||
_buffer: deque[str] = deque(maxlen=MAX_BUFFER_LINES)
|
||||
_subscribers: set[asyncio.Queue[str]] = set()
|
||||
_installed = False
|
||||
|
||||
|
||||
class _BroadcastHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
line = _FORMATTER.format(record)
|
||||
except Exception:
|
||||
return
|
||||
_buffer.append(line)
|
||||
for queue in list(_subscribers):
|
||||
try:
|
||||
queue.put_nowait(line)
|
||||
except asyncio.QueueFull:
|
||||
# Langsamer/verschwundener Abonnent -- lieber eine Zeile
|
||||
# verlieren als das Event-Loop-Logging blockieren.
|
||||
pass
|
||||
|
||||
|
||||
def install() -> None:
|
||||
"""Idempotent -- an app.main::lifespan gebunden. Haengt den Handler an
|
||||
den Root-Logger (sieht alles, was propagiert) und hebt den Level des
|
||||
eigenen App-Namespace 'jumphost' auf DEBUG an, damit die Verbindungs-
|
||||
Debug-Meldungen (SSH/RDP-Handshake etc.) ueberhaupt erzeugt werden, ohne
|
||||
Drittanbieter-Logger (uvicorn, asyncssh) unnoetig auf DEBUG zu stellen."""
|
||||
global _installed
|
||||
if _installed:
|
||||
return
|
||||
handler = _BroadcastHandler()
|
||||
handler.setLevel(logging.DEBUG)
|
||||
logging.getLogger().addHandler(handler)
|
||||
logging.getLogger("jumphost").setLevel(logging.DEBUG)
|
||||
_installed = True
|
||||
|
||||
|
||||
def subscribe() -> asyncio.Queue[str]:
|
||||
queue: asyncio.Queue[str] = asyncio.Queue(maxsize=500)
|
||||
_subscribers.add(queue)
|
||||
return queue
|
||||
|
||||
|
||||
def unsubscribe(queue: asyncio.Queue[str]) -> None:
|
||||
_subscribers.discard(queue)
|
||||
|
||||
|
||||
def recent_lines() -> list[str]:
|
||||
return list(_buffer)
|
||||
Reference in New Issue
Block a user