fix(auth): close admin bootstrap race and cover store SQL paths - #1227
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR adds transaction-scoped advisory locking to ChangesAdmin Bootstrap Invariant with Advisory Lock and Test Coverage
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
de69aec to
dab8f9c
Compare
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Adversarial review (PR #1227)Pushed one in-scope fix (commit Fixed in this PR (commit a5f6e69)
Verified clean
Pre-existing on main (not introduced here)Verified against the latest
Findings filed as follow-up issues (out-of-scope)Two adjacent hardening opportunities that surfaced while reviewing the bootstrap path; both fully triaged and filed against
VerdictIn-scope lint failure is fixed and pushed. The race fix itself is sound: lock key matches the migration, predicate matches @coderabbitai full review |
|
(•ᴗ•)⟃🐇 ✏️ Learnings added
✅ Action performedFull review finished. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Adding the DB-backed coverage requested by review finding TEST-01 (issue #1153) exposed a real bug: the CreateAdminIfNone conditional INSERT ... WHERE NOT EXISTS is not race-free on its own. Under READ COMMITTED each statement snapshots at statement start, so two concurrent bootstrap calls can both see "no admin" and both insert, producing two admins (reproduced by the new concurrent integration test, which failed with 2 winners against the old code). Fix: run the guarded insert inside a transaction that first takes the transaction-scoped advisory lock already used by the min-one-admin trigger (migration 000065). The second caller blocks until the first commits and its INSERT then sees the committed admin, so the NOT EXISTS guard suppresses the duplicate. DBConnection gains Begin(); database.Connection and pgxmock already provide it. Test coverage added per the report's recommendation: - pgxmock tests pinning the actual SQL for CreateAdminIfNone (19 column INSERT, NOT EXISTS predicate on $20, Administrators group forcing, ErrEmailInUse mapping, begin/lock/commit error paths), AdminExists predicate agreement, CountGroupMembers, ListUsers, ListAPIKeysByUser, UpdateAPIKeyLastUsed and Ping, taking these paths from 0% to 85-100% coverage. - testcontainers integration tests (integration build tag) proving bootstrap-insert-once semantics under two concurrent calls across 25 barrier-synced iterations, plus sequential no-op, inactive-admin and email-collision semantics against the real migrated schema. Closes #1153
The bootstrap-race fix introduced two new golangci-lint failures on the new transaction scaffolding in CreateAdminIfNone (Lint Code is failing on this PR for these reasons): - `_ = tx.Rollback(ctx)` does not satisfy errcheck under this project's `check-blank: true` setting. Replace with the project-standard `defer tx.Rollback(ctx) //nolint:errcheck` pattern used in internal/config/store_postgres.go. - `if _, err := tx.Exec(...)` on the advisory-lock acquire shadowed the outer `err` (govet shadow). Drop the short-declaration so the lock-exec assigns to the outer variable. No behavior change on the success or known-error paths. go test ./internal/auth/ -race: 577 passed. golangci-lint on store_postgres.go: no new findings on the touched lines.
a5f6e69 to
feaa190
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Merged to main after pre-merge adversarial review (verdict MERGE): the admin bootstrap race fix is real synchronization (pg_advisory_xact_lock with the key matching migrations 000065/000074, serializing with the min-one-admin trigger), verified by reverting the fix and reproducing the race (2 winners at iteration 1) then confirming the fix closes it (25 barrier-synced iterations, exactly 1 winner, clean under -race). Union with current main builds and tests green (585 auth tests + integration testcontainers run). Red CI jobs on the head are the repo-wide Lint/Security debt being fixed in #1364/#1363, not from this PR. |
Problem
Closes #1153 (review finding TEST-01, P2).
The auth PostgresStore's security-critical SQL paths had zero test coverage: CreateAdminIfNone (the admin bootstrap guard), CountGroupMembers (backs the last-admin protection), ListUsers, ListAPIKeysByUser, UpdateAPIKeyLastUsed and Ping were only ever exercised through interface mocks, so the 19-column bootstrap INSERT, the NOT EXISTS predicate and the Administrators-group forcing logic never actually ran in any test.
Writing the recommended DB-backed concurrency test then exposed a real bug: the comment on CreateAdminIfNone claimed the single INSERT ... WHERE NOT EXISTS statement is atomic, but under READ COMMITTED each statement snapshots at statement start, so two concurrent bootstrap calls can both observe "no admin" and both insert. The new integration test reproduced this against the unmodified code (2 winners, 2 admin rows).
Fix
pg_advisory_xact_lock(8059058058580001), exposed as the documented constantadminInvariantAdvisoryLockKey). The second concurrent caller blocks until the first commits; its INSERT statement then takes a fresh snapshot, sees the committed admin, and the NOT EXISTS guard suppresses the duplicate. Commit failures are surfaced as errors instead of reporting an unconfirmed insert as created.DBConnectiongainsBegin(ctx) (pgx.Tx, error);database.Connection(production) and pgxmock already provide it, and the hand-rolledMockDBConnectiongot the method.Tests
internal/auth/store_postgres_pgxmock_test.go(new, runs in the normal unit suite): pins the actual SQL text and argument order for CreateAdminIfNone (19-column INSERT fed by SELECT $1..$19, NOT EXISTS predicate bound to $20, Administrators-group forcing and dedup, ErrEmailInUse mapping, begin/lock/commit error paths), AdminExists predicate agreement, CountGroupMembers, ListUsers (incl. NULL handling and row errors), ListAPIKeysByUser, UpdateAPIKeyLastUsed and Ping.internal/auth/store_postgres_db_test.go(new,integrationbuild tag, testcontainers): proves bootstrap-insert-once semantics under two concurrent CreateAdminIfNone calls across 25 barrier-synced iterations, plus sequential no-op, inactive-admin-counts-as-no-admin (AdminExists agreement) and email-collision semantics against the real migrated schema.Evidence:
go test ./internal/auth/(and with-race): 577 passed.go test -tags integration ./internal/auth/ -run TestIntegration_CreateAdminIfNone: 6 passed post-fix; the concurrent test failed withexpected: 1, actual: 2winners against the pre-fix code, confirming it catches the real race.go tool cover -func).go build ./...,go vet ./...: clean.Summary by CodeRabbit
Release Notes
Bug Fixes & Improvements
Tests