connect fix 1

This commit is contained in:
2026-08-20 20:07:36 +02:00
parent cd5957cbd2
commit 74bff2e754
6 changed files with 69 additions and 33 deletions

View File

@ -208,14 +208,21 @@ bleiben weiterhin vollstaendig im Audit-Log-Tab sichtbar, dort war die
Information ohnehin redundant vorhanden). Information ohnehin redundant vorhanden).
**3) Verbindungslog (Live-Tail, inkl. Debug)**: neuer Tab "Verbindungslog" + **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- Server-Prozesses live in den Browser — inkl. DEBUG-Detail zum SSH/RDP-
Verbindungsaufbau (`jumphost.*`-Logger werden beim Start auf DEBUG gesetzt, Verbindungsaufbau (`jumphost.*`-Logger werden beim Start auf DEBUG gesetzt,
siehe `app/security/log_stream.py`). Bewusst ein reiner In-Memory-Ring-Buffer 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 (letzte 1000 Zeilen) + Pub/Sub ohne DB-Persistenz — ein Live-Tail wie
`journalctl -f`, kein durchsuchbares Archiv. Nur fuer Super-Admins sichtbar/ `journalctl -f`, kein durchsuchbares Archiv. Nur fuer Super-Admins sichtbar/
erreichbar, da die Logs mandantenuebergreifend technische Details preisgeben 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" + **4) Sessionview-Dashboard (nur Super-Admin)**: neuer Tab "Sessions" +
`GET /admin/sessions` (aktive + historische Sitzungen ueber alle Mandanten), `GET /admin/sessions` (aktive + historische Sitzungen ueber alle Mandanten),

50
app/admin/log_ws.py Normal file
View File

@ -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)

View File

@ -24,12 +24,11 @@ import json
import logging import logging
from pathlib import Path 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 ( from app.auth.deps import (
CurrentUser, CurrentUser,
effective_tenant_ids, effective_tenant_ids,
get_current_user_ws,
require_admin_or_scope, require_admin_or_scope,
require_admin_scope_or_host_role, require_admin_scope_or_host_role,
require_admin_session, require_admin_session,
@ -58,7 +57,7 @@ from app.models.schemas import (
UserGroupUpdateRequest, UserGroupUpdateRequest,
UserUpdateRequest, UserUpdateRequest,
) )
from app.security import active_sessions, log_stream from app.security import active_sessions
from app.security.api_tokens import ( from app.security.api_tokens import (
VALID_SCOPES, VALID_SCOPES,
generate_token, 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} return {"session_id": session_id, "verified": verified, "entry_count": entry_count}
# --- Verbindungslog (nur Super-Admin, Live-Tail) ------------------------------ # Hinweis: Das Live-'Verbindungslog' (WS /ws/logs) liegt bewusst NICHT unter
# # diesem /admin-Router, sondern als eigener Top-Level-Router in
# Reiner Live-Tail der Anwendungslogs dieses Server-Prozesses (siehe # app/admin/log_ws.py -- siehe dort fuer den Grund (nginx-Reverse-Proxy-
# app/security/log_stream.py) -- kein durchsuchbares Archiv, keine # Location-Matching auf /ws/).
# 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)

View File

@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates 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.admin.routes import router as admin_router
from app.auth.deps import CurrentUser, require_global_admin from app.auth.deps import CurrentUser, require_global_admin
from app.auth.routes import router as auth_router 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(auth_router)
app.include_router(admin_router) app.include_router(admin_router)
app.include_router(admin_log_ws_router)
app.include_router(catalog_router) app.include_router(catalog_router)
app.include_router(ssh_ws_router) app.include_router(ssh_ws_router)
app.include_router(sftp_router) app.include_router(sftp_router)

View File

@ -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 Haengt sich als zusaetzlicher logging.Handler an den Root-Logger, haelt die
letzten LOG-Zeilen im Prozessspeicher (fuer den initialen Replay beim letzten LOG-Zeilen im Prozessspeicher (fuer den initialen Replay beim
Oeffnen des Tabs) und verteilt jede neue Zeile an alle aktuell verbundenen 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 Bewusst KEINE Persistenz in der DB -- das ist ein reiner Live-Tail (wie
'journalctl -f'), kein durchsuchbares Archiv. Rein prozesslokal: bei mehreren 'journalctl -f'), kein durchsuchbares Archiv. Rein prozesslokal: bei mehreren

View File

@ -1217,7 +1217,7 @@
return; return;
} }
const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; 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..."); setConnLogStatus("verbinde...");
logSocket.addEventListener("open", () => setConnLogStatus("verbunden")); logSocket.addEventListener("open", () => setConnLogStatus("verbunden"));
logSocket.addEventListener("close", () => setConnLogStatus("getrennt")); logSocket.addEventListener("close", () => setConnLogStatus("getrennt"));