internal/lockout: Store.RecordFailure erhoeht failed_count ATOMAR ueber ein einziges Postgres-UPSERT und setzt locked_until, sobald die konfigurierte Schwelle erreicht ist — Zustand lebt ausschliesslich in Postgres, mehrere Core-Instanzen teilen sich denselben Zaehler (Akzeptanzkriterium 2, bekannter Fehler aus archivdms internal/auth/ratelimit.go vermieden: kein In-Process-Zaehler). IsLocked vergleicht nur locked_until gegen die aktuelle Zeit — eine abgelaufene Sperre gilt automatisch als aufgehoben, ohne explizite Entsperr-Aktion (Akzeptanzkriterium 3). Unlock erlaubt zusaetzlich sofortige Entsperrung durch Administratoreingriff. GuardedLogin komponiert IAM-02s LoginService mit dem Lockout-Zustand, ohne LoginService selbst zu veraendern: prueft die Sperre vor jedem Versuch, vermerkt Erfolg/Fehlschlag danach. Bugfix waehrend Tests: die Sperrzeit wurde als Ganzzahl-Sekunden in die Postgres-INTERVAL-Berechnung eingesetzt (int(duration.Seconds())), wodurch Sperrzeiten unter 1 Sekunde (z.B. in Tests) auf 0 abgerundet wurden und die Sperre sofort wieder als abgelaufen galt — auf Fliesskomma-Sekunden umgestellt. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Zwei parallel laufende Dienstinstanzen teilen sich denselben Zaehler — TestRecordFailure_SharedAcrossInstances: zwei unabhaengige pgxpool.Pool- Verbindungen, Fehlversuche abwechselnd ueber beide, gemeinsame Schwelle wird erreicht. PASS. 2. Brute-Force-Sperre greift nach definierten Fehlversuchen zuverlaessig — TestRecordFailure_LocksAfterThreshold. PASS. 3. Zeitversatz zwischen Sperre und Entsperrung automatisiert getestet — TestIsLocked_AutoUnlocksAfterExpiry: gesperrt vor Ablauf, automatisch entsperrt nach Ablauf der Sperrzeit. PASS. Zusaetzlich: TestGuardedLogin_LocksAfterRepeatedFailures belegt das Zusammenspiel mit dem echten IAM-02-LoginService End-to-End — selbst das korrekte Passwort wird nach Sperrung abgewiesen. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
38 lines
1003 B
Go
38 lines
1003 B
Go
package lockout
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
|
|
)
|
|
|
|
var ErrAccountLocked = errors.New("lockout: konto ist vorübergehend gesperrt")
|
|
|
|
// GuardedLogin komponiert IAM-02s LoginService mit dem Lockout-Zustand,
|
|
// OHNE LoginService selbst zu veraendern (kein Umbau der Vorbedingung):
|
|
// vor jedem Login-Versuch wird die Sperre geprueft, nach jedem Versuch wird
|
|
// Erfolg/Fehlschlag im geteilten Postgres-Zustand vermerkt.
|
|
func GuardedLogin(ctx context.Context, store *Store, login *auth.LoginService, email, password string) (string, error) {
|
|
locked, _, err := store.IsLocked(ctx, email)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if locked {
|
|
return "", ErrAccountLocked
|
|
}
|
|
|
|
token, err := login.Login(ctx, email, password)
|
|
if err != nil {
|
|
if _, _, lockErr := store.RecordFailure(ctx, email); lockErr != nil {
|
|
return "", lockErr
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
if err := store.RecordSuccess(ctx, email); err != nil {
|
|
return "", err
|
|
}
|
|
return token, nil
|
|
}
|