Fix auth throttle DoS and serialize admin bootstrap.

Evict/cap limiter keys, replace hard username lockouts with IP+user progressive delays cleared on success, and create bootstrap admins under the same advisory/mutex lock as role changes.
This commit is contained in:
2026-08-22 11:54:10 -07:00
parent 59513ab75e
commit 5bdaa8977f
7 changed files with 413 additions and 39 deletions
+47 -2
View File
@@ -9,14 +9,20 @@ import (
func TestMemoryConcurrentLastAdminDemotion(t *testing.T) {
m := NewMemory()
ctx := context.Background()
a := &User{Username: "admin_a", PasswordHash: "x", Role: RoleAdmin}
b := &User{Username: "admin_b", PasswordHash: "x", Role: RoleAdmin}
a := &User{Username: "admin_a", PasswordHash: "x", Role: RoleUser}
b := &User{Username: "admin_b", PasswordHash: "x", Role: RoleUser}
if err := m.CreateUser(ctx, a); err != nil {
t.Fatal(err)
}
if err := m.CreateUser(ctx, b); err != nil {
t.Fatal(err)
}
if err := m.SetUserRole(ctx, a.ID, RoleAdmin); err != nil {
t.Fatal(err)
}
if err := m.SetUserRole(ctx, b.ID, RoleAdmin); err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
errs := make(chan error, 2)
@@ -54,3 +60,42 @@ func TestMemoryConcurrentLastAdminDemotion(t *testing.T) {
t.Fatalf("admins remaining = %d, want 1", n)
}
}
func TestMemoryConcurrentBootstrapAdmin(t *testing.T) {
m := NewMemory()
ctx := context.Background()
a := &User{Username: "boot_a", PasswordHash: "x", Role: RoleAdmin}
b := &User{Username: "boot_b", PasswordHash: "x", Role: RoleAdmin}
var wg sync.WaitGroup
errs := make(chan error, 2)
wg.Add(2)
go func() {
defer wg.Done()
errs <- m.CreateUser(ctx, a)
}()
go func() {
defer wg.Done()
errs <- m.CreateUser(ctx, b)
}()
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
n, err := m.CountAdmins(ctx)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("bootstrap race left %d admins, want 1", n)
}
if a.Role == RoleAdmin && b.Role == RoleAdmin {
t.Fatal("both users kept RoleAdmin")
}
if a.Role != RoleAdmin && b.Role != RoleAdmin {
t.Fatal("neither user is admin")
}
}