Erste Produktivversion

This commit is contained in:
2026-08-08 20:31:58 +02:00
parent d8ded2816d
commit 7b21e2d1b4
110 changed files with 18170 additions and 644 deletions

88
tools/check-routes.py Normal file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Listet alle Endpunkte samt ihrer Berechtigungsabhängigkeiten.
python3 tools/check-routes.py
Findet Endpunkte, die weder eine Abhängigkeit mit Berechtigungsprüfung
haben noch ausdrücklich als offen eingetragen sind. Damit fällt auf, wenn
beim Hinzufügen einer Route die Absicherung vergessen wurde - was beim
Schreiben leicht passiert, weil FastAPI das nicht anmahnt.
Rein statisch: Zwei Fälle kann das Skript nicht sehen und sie sind unten
als Ausnahmen vermerkt - Endpunkte, die ihre Prüfung im Rumpf machen
(DELETE auf Mitgliedschaften: Eigentümer oder man selbst) und solche,
die über einen Token im Pfad geschützt sind.
"""
import ast, pathlib, re
GUARDS = {
"CurrentUser": "angemeldet",
"VerifiedUser": "bestätigt",
"AdminUser": "ADMIN",
"ReadableList": "Listen-Leser",
"EditableList": "Listen-Bearbeiter",
"OwnedList": "Listen-EIGENTÜMER",
"SharableList": "Teilen-Berechtigt",
}
# Endpunkte, die bewusst ohne Anmeldung erreichbar sind
INTENDED_PUBLIC = {
"/api/auth/register", "/api/auth/login", "/api/auth/verify",
"/api/auth/password/reset-request", "/api/auth/password/reset",
# Willkommensstrecke: Wer den Link oeffnet, HAT noch kein Passwort
# und kann sich deshalb nicht anmelden. Der Token ist der Nachweis.
"/api/auth/welcome/{token}", "/api/auth/welcome/complete",
# Bestaetigung einer Adressaenderung - ebenfalls per Token, weil der
# Klick aus dem Postfach kommt.
"/api/auth/email-change/{token}",
"/api/config", "/manifest.webmanifest", "/api/push/config",
"/healthz", "/readyz",
}
# Öffentlich, aber durch einen Token im Pfad geschützt
TOKEN_GUARDED = ("/api/public/",)
rows = []
for path in sorted(pathlib.Path("backend/app/routers").glob("*.py")):
src = path.read_text()
prefix = ""
m = re.search(r'APIRouter\((?:[^)]*?)prefix="([^"]*)"', src, re.S)
if m:
prefix = m.group(1)
tree = ast.parse(src)
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
for dec in node.decorator_list:
if not (isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute)):
continue
if dec.func.attr.upper() not in {"GET","POST","PUT","PATCH","DELETE"}:
continue
method = dec.func.attr.upper()
route = prefix + (dec.args[0].value if dec.args else "?")
guards = []
for arg in node.args.args + node.args.kwonlyargs:
ann = ast.unparse(arg.annotation) if arg.annotation else ""
for name, label in GUARDS.items():
if ann == name:
guards.append(label)
rows.append((route, method, guards, path.name))
print(f"{'Pfad':52} {'Methode':7} Schutz")
print("-" * 100)
problems = 0
for route, method, guards, file in sorted(rows):
if guards:
mark = ", ".join(guards)
elif route in INTENDED_PUBLIC:
mark = "offen (gewollt)"
elif route.startswith(TOKEN_GUARDED):
mark = "Token im Pfad"
else:
mark = ">>> KEIN SCHUTZ <<<"
problems += 1
print(f"{route:52} {method:7} {mark}")
print("-" * 100)
print(f"{len(rows)} Endpunkte, {problems} ohne erkennbaren Schutz")