89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
#!/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")
|