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:
@@ -2,6 +2,7 @@ package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
@@ -270,6 +271,52 @@ func TestCreateUser(testingT *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUserRaceCondition(testingT *testing.T) {
|
||||
testingT.Parallel()
|
||||
|
||||
testingT.Run("concurrent setup requests create only one user", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc, cleanup := setupTestService(t)
|
||||
defer cleanup()
|
||||
|
||||
const goroutines = 10
|
||||
|
||||
results := make(chan error, goroutines)
|
||||
start := make(chan struct{})
|
||||
|
||||
for i := range goroutines {
|
||||
go func(idx int) {
|
||||
<-start // Wait for all goroutines to be ready
|
||||
|
||||
_, err := svc.CreateUser(
|
||||
context.Background(),
|
||||
fmt.Sprintf("admin%d", idx),
|
||||
"password123456",
|
||||
)
|
||||
results <- err
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Release all goroutines simultaneously
|
||||
close(start)
|
||||
|
||||
var successes, failures int
|
||||
for range goroutines {
|
||||
err := <-results
|
||||
if err == nil {
|
||||
successes++
|
||||
} else {
|
||||
assert.ErrorIs(t, err, auth.ErrUserExists)
|
||||
failures++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, successes, "exactly one goroutine should succeed")
|
||||
assert.Equal(t, goroutines-1, failures, "all other goroutines should fail with ErrUserExists")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthenticate(testingT *testing.T) {
|
||||
testingT.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user