umbau 1.0
This commit is contained in:
77
app/main.py
77
app/main.py
@ -4,10 +4,12 @@ Auslieferung zusammen (siehe Konzept 6.6 Web-Anwendungs-Hardening).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi import Depends, FastAPI, Query, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
@ -18,9 +20,11 @@ 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
|
||||
from app.catalog.routes import router as catalog_router
|
||||
from app.db import close_db, init_db
|
||||
from app.config import settings
|
||||
from app.db import close_db, get_db, init_db
|
||||
from app.rdp_proxy.ws_tunnel import router as rdp_ws_router
|
||||
from app.security import log_stream
|
||||
from app.security.session_reaper import reap_orphaned_sessions
|
||||
from app.ssh_proxy.proxy import bcrypt_kdf_available
|
||||
from app.ssh_proxy.sftp import router as sftp_router
|
||||
from app.ssh_proxy.terminal_ws import router as ssh_ws_router
|
||||
@ -56,6 +60,13 @@ async def lifespan(app: FastAPI):
|
||||
log_stream.install()
|
||||
_check_optional_dependencies()
|
||||
await init_db()
|
||||
# E7 (Umsetzungsauftrag_Sonnet5.md Teil E.1): Sitzungen, die beim letzten
|
||||
# Absturz/Neustart offen waren, haben ended_at IS NULL fuer immer -- es
|
||||
# gibt keinen Task mehr, der sie regulaer schliessen wuerde. Muss NACH
|
||||
# init_db() laufen (Migrationen/Tabellen muessen existieren) und VOR dem
|
||||
# ersten Request, damit die Sessionview und kuenftige
|
||||
# "wie viele Sitzungen offen"-Zaehlungen (Teil F) nie mit Geistern rechnen.
|
||||
await reap_orphaned_sessions(get_db())
|
||||
yield
|
||||
await close_db()
|
||||
|
||||
@ -88,7 +99,7 @@ async def security_headers_middleware(request: Request, call_next):
|
||||
"default-src 'self'; "
|
||||
"script-src 'self'; "
|
||||
"style-src 'self'; "
|
||||
"img-src 'self' data:; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"connect-src 'self' ws: wss:; "
|
||||
"frame-ancestors 'none'; "
|
||||
"base-uri 'self'; "
|
||||
@ -114,6 +125,15 @@ async def dashboard(request: Request):
|
||||
return templates.TemplateResponse(request, "dashboard.html", {})
|
||||
|
||||
|
||||
@app.get("/workspace", response_class=HTMLResponse)
|
||||
async def workspace_page(request: Request):
|
||||
# F3 (Umsetzungsauftrag_Sonnet5.md Teil F.3): dauerhafte Arbeitsflaeche
|
||||
# mit Seitenleiste -- wie /dashboard rein statisches Markup, Auth
|
||||
# clientseitig ueber GET /auth/me, Sitzungs-/Katalogdaten ueber die
|
||||
# bestehenden /catalog/*-Endpunkte (app/catalog/routes.py).
|
||||
return templates.TemplateResponse(request, "workspace.html", {})
|
||||
|
||||
|
||||
@app.get("/admin", response_class=HTMLResponse)
|
||||
async def admin_page(request: Request):
|
||||
# Wie /dashboard: Seite selbst ist statisches Markup ohne Secrets, die
|
||||
@ -134,8 +154,55 @@ async def rdp_page(request: Request, host_id: int):
|
||||
|
||||
|
||||
@app.get("/healthz")
|
||||
async def healthz():
|
||||
return {"status": "ok"}
|
||||
async def healthz(deep: bool = Query(default=False)):
|
||||
"""Flache Variante (Standard): Prozess laeuft, sonst nichts -- schnell
|
||||
genug fuer haeufige Load-Balancer-/Monitoring-Abfragen.
|
||||
|
||||
Tiefe Variante (`?deep=true`, Befund B.2 in Umsetzungsauftrag_Sonnet5.md
|
||||
Teil B): prueft zusaetzlich die DB-Verbindung (billiges `SELECT 1` auf
|
||||
der einen gemeinsamen Verbindung, siehe app/db.py) und guacd per
|
||||
TCP-Verbindungsversuch mit kurzem Timeout. Vorher lieferte /healthz
|
||||
IMMER "ok", auch wenn guacd tot war -- das fiel erst auf, wenn ein
|
||||
Benutzer eine RDP-Sitzung versuchte. Bewusst NICHT als Standardverhalten,
|
||||
damit ein ausgefallenes guacd nicht jede flache Liveness-Pruefung (die
|
||||
ueblicherweise sehr haeufig laeuft) mit in den Fehlerzustand reisst.
|
||||
"""
|
||||
if not deep:
|
||||
return {"status": "ok"}
|
||||
|
||||
checks: dict[str, str] = {}
|
||||
healthy = True
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
await asyncio.wait_for(conn.execute("SELECT 1"), timeout=2.0)
|
||||
checks["db"] = "ok"
|
||||
except Exception as exc:
|
||||
checks["db"] = f"fehler: {exc.__class__.__name__}"
|
||||
healthy = False
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(settings.guacd_host, settings.guacd_port), timeout=2.0
|
||||
)
|
||||
writer.close()
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
checks["guacd"] = "ok"
|
||||
except Exception as exc:
|
||||
checks["guacd"] = (
|
||||
f"nicht erreichbar unter {settings.guacd_host}:{settings.guacd_port} "
|
||||
f"({exc.__class__.__name__})"
|
||||
)
|
||||
healthy = False
|
||||
|
||||
status_code = 200 if healthy else 503
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={"status": "ok" if healthy else "fehler", "checks": checks},
|
||||
)
|
||||
|
||||
|
||||
# --- API-Dokumentation --------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user