#!/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")