docs: add auth part 2 LDAP design spec
This commit is contained in:
parent
3832b81174
commit
929ce0c8d5
139
docs/superpowers/specs/2026-04-27-auth-part2-ldap-design.md
Normal file
139
docs/superpowers/specs/2026-04-27-auth-part2-ldap-design.md
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# 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`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"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:
|
||||||
|
|
||||||
|
1. `has_to_update()` prüfen — wenn letzter Sync < `LDAP_SYNC_MIN_INTERVAL_HOURS`, abbrechen
|
||||||
|
2. Neue NTLM-Verbindung mit den übergebenen Credentials öffnen
|
||||||
|
3. 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)`
|
||||||
|
4. User in lokaler DB die **nicht** in AD gefunden wurden UND `pw_hash is None` → `is_active = False`
|
||||||
|
5. 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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@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_HOURS` her
|
||||||
|
- Zwischen jedem Buchstaben-Batch: `LDAP_SYNC_LETTER_DELAY_SECONDS` Sleep
|
||||||
|
- 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 und `is_active`)
|
||||||
|
- Admin-UI für User-Verwaltung (→ separater Spec)
|
||||||
|
- Gruppen aus AD (→ Part 3)
|
||||||
Loading…
x
Reference in New Issue
Block a user