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
+62 -1
View File
@@ -3,6 +3,12 @@ package store
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"plumber/internal/store/sqlc"
)
// Postgres implements Store against a sqlc-backed database.
@@ -16,8 +22,63 @@ func NewPostgres(db *sql.DB) *Postgres {
}
func (p *Postgres) CreateUser(ctx context.Context, u *User) error {
if u == nil {
return fmt.Errorf("user: nil")
}
if u.Role != RoleUser && u.Role != RoleAdmin {
return fmt.Errorf("invalid role")
}
u.Username = NormalizeUsername(u.Username)
if u.ID == "" {
u.ID = uuid.NewString()
}
if u.Name == "" {
u.Name = u.Username
}
if u.CreatedAt == "" {
u.CreatedAt = time.Now().UTC().Format(time.RFC3339)
}
if u.Role != RoleAdmin {
u.db = p.db
return u.Create(ctx)
}
// Bootstrap admin: serialize count+insert so two setup-secret registers
// cannot both observe zero admins.
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock($1)`, adminRoleLockKey); err != nil {
return err
}
q := sqlc.New(tx)
n, err := q.CountAdmins(ctx, string(RoleAdmin))
if err != nil {
return err
}
role := RoleAdmin
if n > 0 {
role = RoleUser
}
if err := q.CreateUser(ctx, sqlc.CreateUserParams{
ID: u.ID,
Username: u.Username,
Name: u.Name,
PasswordHash: u.PasswordHash,
Role: string(role),
CreatedAt: u.CreatedAt,
}); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
u.Role = role
u.db = p.db
return u.Create(ctx)
return nil
}
func (p *Postgres) UserByID(ctx context.Context, id string) (*User, error) {