5.0 KiB
Design Spec: Benutzerverwaltung Part 2 — LDAP-Integration
Datum: 2026-04-27 Status: Genehmigt
Ziel
LDAP-Authentifizierung gegen das Active Directory der TH Nürnberg als Fallback zur lokalen Auth. Bei erfolgreichem Login wird der lokale User-Eintrag angelegt/aktualisiert und ein Hintergrund-Sync aller AD-User angestoßen (login-getriggert, nicht periodisch).
AD-Verbindungsparameter (bereits in config.py)
| Setting | Wert |
|---|---|
LDAP_SERVER |
gso1.ads1.fh-nuernberg.de |
LDAP_DOMAIN |
ADS1 |
LDAP_SEARCH_BASE |
OU=users,OU=EFI,OU=Faculties,DC=ADS1,DC=fh-nuernberg,DC=de |
LDAP_ENABLED |
False (dev), True (prod) |
Section 1: LDAP-Authentifizierung
Neue Config-Felder (app/core/config.py)
LDAP_SYNC_MIN_INTERVAL_HOURS: int = 12 # Mindestabstand zwischen Syncs
LDAP_SYNC_LETTER_DELAY_SECONDS: float = 5.0 # Wartezeit zwischen Buchstaben-Batches
Neue Datei app/modules/auth/ldap.py
Reine Funktionen — kein FastAPI, kein DB-Zugriff, nur ldap3.
ldap_authenticate(username, password) -> dict | None
Öffnet NTLM-Verbindung gegen LDAP_SERVER, bindet, liest User-Attribute und schließt die Verbindung wieder. Gibt bei Erfolg ein Dict zurück, bei falschem Passwort oder Verbindungsfehler None (Verbindungsfehler wird geloggt). Eine einzige Verbindung für Bind + Attributabfrage.
Rückgabe-Dict:
{
"username": str, # sAMAccountName
"full_name": str, # f"{givenName} {sn}"
"department": str, # department (max 64 Zeichen, "N.N." wenn leer)
"role": str, # description (max 64 Zeichen, "N.N." wenn leer)
"account_expires": datetime | None, # accountExpires, None = kein Ablauf
}
sync_all_users(username, password, db, settings) -> None
Synchronisiert alle AD-User alphabetisch (a–z + _). Läuft in einem BackgroundTask-Thread:
has_to_update()prüfen — wenn letzter Sync <LDAP_SYNC_MIN_INTERVAL_HOURS, abbrechen- Neue NTLM-Verbindung mit den übergebenen Credentials öffnen
- Für jeden Buchstaben:
- AD-Einträge mit
CN=<buchstabe>*laden - Jeden Entry upserten (full_name, department, role, account_expires)
time.sleep(LDAP_SYNC_LETTER_DELAY_SECONDS)
- AD-Einträge mit
- User in lokaler DB die nicht in AD gefunden wurden UND
pw_hash is None→is_active = False - Verbindung schließen
has_to_update() liest den Zeitstempel des zuletzt geänderten Users aus der DB (entspricht Vorversion).
Geänderte Datei app/modules/auth/service.py
Geänderter Auth-Flow in authenticate_user:
1. user = get_user(db, username)
2. user existiert UND is_active=False → return None (absolute Sperre)
3. user existiert UND pw_hash gesetzt:
→ verify_password(password, pw_hash)
→ OK: last_login setzen, return user
4. ldap_enabled:
→ attrs = ldap_authenticate(username, password)
→ attrs is None: return None
→ OK: upsert_ldap_user(db, attrs), last_login setzen, return user
5. return None
Neue Funktion upsert_ldap_user(db, username, attrs) -> User
Legt User an falls nicht vorhanden, schreibt AD-Felder (full_name, department, role, account_expires). Setzt pw_hash nicht (bleibt None für reine LDAP-User).
Geänderte Datei app/modules/auth/router.py
POST /auth/login erhält einen BackgroundTasks-Parameter. Nach erfolgreichem LDAP-Login wird sync_all_users als BackgroundTask gestartet:
@router.post("/login")
async def login(background_tasks: BackgroundTasks, ...):
user = authenticate_user(...)
if user and settings.LDAP_ENABLED:
background_tasks.add_task(sync_all_users, username, password, db, settings)
...
Section 2: AD-Sync-Logik
Sync-Verhalten
| Situation | Aktion |
|---|---|
| User in AD, in DB | full_name, department, role, account_expires aktualisieren |
| User in AD, nicht in DB | Neuen User anlegen (is_active=True, pw_hash=None) |
User nicht in AD, in DB, pw_hash is None |
is_active = False |
User nicht in AD, in DB, pw_hash gesetzt |
Unverändert (lokaler Account, z. B. Admin) |
Timing
- Sync läuft nur wenn letzter Sync >
LDAP_SYNC_MIN_INTERVAL_HOURSher - Zwischen jedem Buchstaben-Batch:
LDAP_SYNC_LETTER_DELAY_SECONDSSleep - Gesamtdauer ca. 27 × 5s ≈ 2–3 Minuten (im Hintergrund, blockiert nichts)
Dateistruktur
app/modules/auth/
├── ldap.py # neu: ldap_authenticate, ldap_get_user_attrs, sync_all_users
├── service.py # geändert: LDAP-Stub füllen, upsert_ldap_user
└── router.py # geändert: BackgroundTasks in POST /auth/login
app/core/
└── config.py # geändert: LDAP_SYNC_MIN_INTERVAL_HOURS, LDAP_SYNC_LETTER_DELAY_SECONDS
Out of Scope
- Serviceaccount / periodischer Celery-Sync (→ mögliche spätere Erweiterung)
account_expires-Prüfung beim Login (Blockierung läuft über AD-Bind-Fehler undis_active)- Admin-UI für User-Verwaltung (→ separater Spec)
- Gruppen aus AD (→ Part 3)