kundenplattform/scripts/reset_password.py
Thomas Stallinger cb820b9790 Security-Hardening vor öffentlicher Freigabe: Rate-Limiting, Passwort-Policy, Docs-Endpunkte, Security-Header
Sicherheits-Review 25.08.2026 vor geplanter öffentlicher Erreichbarkeit
über Pangolin (bisher nur Pangolin-interne Auth vorgeschaltet):

- auth.py: In-Memory-Rate-Limiting auf /login (je Email UND je Quell-IP,
  8 Fehlversuche / 10 Minuten), UserManager.validate_password() setzt
  jetzt eine Mindestlänge von 12 Zeichen durch (fastapi_users' Default
  war ein No-Op, ließ auch leere Passwörter zu).
- routers/admin_kunden.py, routers/organisation.py: fangen
  InvalidPasswordException/UserAlreadyExists jetzt sauber ab statt 500.
- app.py: /docs, /redoc, /openapi.json deaktiviert (legten zuvor die
  komplette Routenstruktur inkl. /admin/* offen), Security-Header-
  Middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
  CSP).
- scripts/reset_password.py: neues Skript, Gegenstück zu create_user.py,
  für die im selben Review gefundenen aktiven Testaccounts mit dem
  Passwort 'test123' (einer davon Superuser) gebraucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:14:34 +02:00

58 lines
1.8 KiB
Python

"""
Passwort eines bestehenden Kontos zuruecksetzen (Gegenstueck zu
create_user.py). Beispiel:
KUNDENPLATTFORM_DATABASE_URL=... .venv/bin/python scripts/reset_password.py \\
--email admin@schule-beispiel.de
"""
import argparse
import asyncio
import getpass
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi_users import exceptions # noqa: E402
from auth import UserDatabase, UserManager # noqa: E402
from db import get_async_database_connection # noqa: E402
async def passwort_zuruecksetzen(email: str, password: str):
async with await get_async_database_connection() as connection:
user_manager = UserManager(UserDatabase(connection))
user = await user_manager.get_by_email(email)
await user_manager.validate_password(password, user)
await user_manager.user_db.update(user, {"hashed_password": user_manager.password_helper.hash(password)})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--email", required=True)
args = parser.parse_args()
if not os.environ.get("KUNDENPLATTFORM_DATABASE_URL"):
sys.exit("KUNDENPLATTFORM_DATABASE_URL ist nicht gesetzt.")
password = getpass.getpass("Neues Passwort: ")
password_confirm = getpass.getpass("Neues Passwort (Wiederholung): ")
if password != password_confirm:
sys.exit("Passwörter stimmen nicht überein.")
try:
asyncio.run(passwort_zuruecksetzen(args.email, password))
except exceptions.UserNotExists:
sys.exit(f"Kein Konto mit der E-Mail {args.email} gefunden.")
except exceptions.InvalidPasswordException as fehler:
sys.exit(f"Passwort abgelehnt: {fehler.reason}")
print(f"Passwort für {args.email} zurückgesetzt.")
if __name__ == "__main__":
main()