fix: prevent setup endpoint race condition (closes #26)
Add mutex and INSERT ON CONFLICT to CreateUser to prevent TOCTOU race where concurrent requests could create multiple admin users. Changes: - Add sync.Mutex to auth.Service to serialize CreateUser calls - Add models.CreateUserAtomic using INSERT ... ON CONFLICT(username) DO NOTHING - Check RowsAffected to detect conflicts at the DB level (defense-in-depth) - Add concurrent race condition test (10 goroutines, only 1 succeeds) The existing UNIQUE constraint on users.username was already in place. This fix adds the application-level protection (items 1 & 2 from #26).
This commit is contained in:
@@ -135,6 +135,46 @@ func FindUserByUsername(
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CreateUserAtomic inserts a user using INSERT ... ON CONFLICT(username) DO NOTHING.
|
||||
// It returns nil, nil if the insert was a no-op due to a conflict (user already exists).
|
||||
func CreateUserAtomic(
|
||||
ctx context.Context,
|
||||
db *database.Database,
|
||||
username, passwordHash string,
|
||||
) (*User, error) {
|
||||
query := "INSERT INTO users (username, password_hash) VALUES (?, ?) ON CONFLICT(username) DO NOTHING"
|
||||
|
||||
result, err := db.Exec(ctx, query, username, passwordHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inserting user: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking rows affected: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
// Conflict: user already exists
|
||||
return nil, nil //nolint:nilnil // nil,nil means conflict (no insert happened)
|
||||
}
|
||||
|
||||
insertID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting last insert id: %w", err)
|
||||
}
|
||||
|
||||
user := NewUser(db)
|
||||
user.ID = insertID
|
||||
|
||||
err = user.Reload(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reloading user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UserExists checks if any user exists in the database.
|
||||
func UserExists(ctx context.Context, db *database.Database) (bool, error) {
|
||||
var count int
|
||||
|
||||
Reference in New Issue
Block a user