From 74bff2e754775267dd073c98825a1b26027eef34 Mon Sep 17 00:00:00 2001 From: Midas Wollinger Date: Thu, 20 Aug 2026 20:07:36 +0200 Subject: [PATCH] connect fix 1 --- README.md | 11 +++++++-- app/admin/log_ws.py | 50 ++++++++++++++++++++++++++++++++++++++ app/admin/routes.py | 35 +++++--------------------- app/main.py | 2 ++ app/security/log_stream.py | 2 +- static/js/admin.js | 2 +- 6 files changed, 69 insertions(+), 33 deletions(-) create mode 100644 app/admin/log_ws.py diff --git a/README.md b/README.md index 82bfafd..41174a2 100644 --- a/README.md +++ b/README.md @@ -208,14 +208,21 @@ bleiben weiterhin vollstaendig im Audit-Log-Tab sichtbar, dort war die Information ohnehin redundant vorhanden). **3) Verbindungslog (Live-Tail, inkl. Debug)**: neuer Tab "Verbindungslog" + -WebSocket `GET /admin/ws/logs` streamen die Anwendungslogs dieses +WebSocket `GET /ws/logs` streamen die Anwendungslogs dieses Server-Prozesses live in den Browser — inkl. DEBUG-Detail zum SSH/RDP- Verbindungsaufbau (`jumphost.*`-Logger werden beim Start auf DEBUG gesetzt, siehe `app/security/log_stream.py`). Bewusst ein reiner In-Memory-Ring-Buffer (letzte 1000 Zeilen) + Pub/Sub ohne DB-Persistenz — ein Live-Tail wie `journalctl -f`, kein durchsuchbares Archiv. Nur fuer Super-Admins sichtbar/ erreichbar, da die Logs mandantenuebergreifend technische Details preisgeben -koennen. +koennen. Der Endpunkt liegt bewusst als eigener Top-Level-Router unter +`/ws/logs` (siehe `app/admin/log_ws.py`) statt unter `/admin/...` — der +nginx-Reverse-Proxy setzt die fuer WebSockets noetigen Upgrade-Header nur +fuer die `location /ws/ { ... }` (siehe +`ansible/roles/nginx_proxy/templates/jumphost.conf.j2`); ein Pfad unter +`/admin/ws/...` wuerde auf die generische `location /` fallen und der +Handshake wuerde hinter dem Proxy fehlschlagen (Status bliebe dauerhaft +"getrennt", ohne dass ein Fehler in der Anwendung selbst sichtbar wird). **4) Sessionview-Dashboard (nur Super-Admin)**: neuer Tab "Sessions" + `GET /admin/sessions` (aktive + historische Sitzungen ueber alle Mandanten), diff --git a/app/admin/log_ws.py b/app/admin/log_ws.py new file mode 100644 index 0000000..8347830 --- /dev/null +++ b/app/admin/log_ws.py @@ -0,0 +1,50 @@ +""" +Live-'Verbindungslog' (Super-Admin) -- WS /ws/logs. + +Bewusst ein EIGENER Top-Level-Router (kein Teil von app/admin/routes.py, +dessen Router mit prefix="/admin" registriert ist): der nginx-Reverse-Proxy +(ansible/roles/nginx_proxy/templates/jumphost.conf.j2) setzt die fuer +WebSockets noetigen Upgrade-Header (`proxy_http_version 1.1`, +`Connection: upgrade` etc.) nur fuer die `location /ws/ { ... }` -- +ein Pfad unter /admin/ws/... wuerde stattdessen auf die generische +`location / { ... }` fallen, die diese Header nicht setzt, wodurch der +WebSocket-Handshake hinter dem Proxy fehlschlaegt (Client sieht sofort +'getrennt', ohne dass die Anwendung selbst je einen Request sieht). Analog +zu app/ssh_proxy/terminal_ws.py (/ws/ssh/{host_id}) und +app/rdp_proxy/ws_tunnel.py (/ws/rdp/{host_id}), die aus demselben Grund +ebenfalls Top-Level-Router unter /ws/ sind. + +Reiner Live-Tail der Anwendungslogs dieses Server-Prozesses (siehe +app/security/log_stream.py) -- kein durchsuchbares Archiv, keine +Persistenz. Nur Super-Admin (require_global_admin-Semantik), da die Logs +ueber alle Mandanten hinweg technische Details preisgeben koennen. +""" +from __future__ import annotations + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from app.auth.deps import get_current_user_ws +from app.security import log_stream + +router = APIRouter() + + +@router.websocket("/ws/logs") +async def stream_logs(websocket: WebSocket): + user = await get_current_user_ws(websocket) + if user is None or not user.is_admin: + await websocket.close(code=4403) + return + + await websocket.accept() + queue = log_stream.subscribe() + try: + for line in log_stream.recent_lines(): + await websocket.send_json({"type": "line", "line": line}) + while True: + line = await queue.get() + await websocket.send_json({"type": "line", "line": line}) + except WebSocketDisconnect: + pass + finally: + log_stream.unsubscribe(queue) diff --git a/app/admin/routes.py b/app/admin/routes.py index 54b4f8d..948c2b1 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -24,12 +24,11 @@ import json import logging from pathlib import Path -from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect, status +from fastapi import APIRouter, Depends, HTTPException, Request, status from app.auth.deps import ( CurrentUser, effective_tenant_ids, - get_current_user_ws, require_admin_or_scope, require_admin_scope_or_host_role, require_admin_session, @@ -58,7 +57,7 @@ from app.models.schemas import ( UserGroupUpdateRequest, UserUpdateRequest, ) -from app.security import active_sessions, log_stream +from app.security import active_sessions from app.security.api_tokens import ( VALID_SCOPES, generate_token, @@ -1639,29 +1638,7 @@ async def get_session_recording(session_id: int, admin: CurrentUser = Depends(re return {"session_id": session_id, "verified": verified, "entry_count": entry_count} -# --- Verbindungslog (nur Super-Admin, Live-Tail) ------------------------------ -# -# Reiner Live-Tail der Anwendungslogs dieses Server-Prozesses (siehe -# app/security/log_stream.py) -- kein durchsuchbares Archiv, keine -# Persistenz. Nur Super-Admin (require_global_admin-Semantik), da die Logs -# ueber alle Mandanten hinweg technische Details preisgeben koennen. - -@router.websocket("/ws/logs") -async def stream_logs(websocket: WebSocket): - user = await get_current_user_ws(websocket) - if user is None or not user.is_admin: - await websocket.close(code=4403) - return - - await websocket.accept() - queue = log_stream.subscribe() - try: - for line in log_stream.recent_lines(): - await websocket.send_json({"type": "line", "line": line}) - while True: - line = await queue.get() - await websocket.send_json({"type": "line", "line": line}) - except WebSocketDisconnect: - pass - finally: - log_stream.unsubscribe(queue) +# Hinweis: Das Live-'Verbindungslog' (WS /ws/logs) liegt bewusst NICHT unter +# diesem /admin-Router, sondern als eigener Top-Level-Router in +# app/admin/log_ws.py -- siehe dort fuer den Grund (nginx-Reverse-Proxy- +# Location-Matching auf /ws/). diff --git a/app/main.py b/app/main.py index 945bb07..caafe11 100644 --- a/app/main.py +++ b/app/main.py @@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from app.admin.log_ws import router as admin_log_ws_router from app.admin.routes import router as admin_router from app.auth.deps import CurrentUser, require_global_admin from app.auth.routes import router as auth_router @@ -41,6 +42,7 @@ app = FastAPI(title="Jumphost Gateway", lifespan=lifespan, docs_url=None, redoc_ app.include_router(auth_router) app.include_router(admin_router) +app.include_router(admin_log_ws_router) app.include_router(catalog_router) app.include_router(ssh_ws_router) app.include_router(sftp_router) diff --git a/app/security/log_stream.py b/app/security/log_stream.py index cfa2214..58c7c71 100644 --- a/app/security/log_stream.py +++ b/app/security/log_stream.py @@ -4,7 +4,7 @@ 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). +WebSocket-Abonnenten (siehe app/admin/log_ws.py, WS /ws/logs). Bewusst KEINE Persistenz in der DB -- das ist ein reiner Live-Tail (wie 'journalctl -f'), kein durchsuchbares Archiv. Rein prozesslokal: bei mehreren diff --git a/static/js/admin.js b/static/js/admin.js index fa234a0..c9303af 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1217,7 +1217,7 @@ return; } const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; - logSocket = new WebSocket(`${proto}//${window.location.host}/admin/ws/logs`); + logSocket = new WebSocket(`${proto}//${window.location.host}/ws/logs`); setConnLogStatus("verbinde..."); logSocket.addEventListener("open", () => setConnLogStatus("verbunden")); logSocket.addEventListener("close", () => setConnLogStatus("getrennt"));