fix(auth): reject RotateAPIKey when SoftDelete affects 0 rows#819
fix(auth): reject RotateAPIKey when SoftDelete affects 0 rows#819rohith500 wants to merge 2 commits into
Conversation
Deterministic fake-repo tests for concurrent RotateAPIKey and Rotate-vs-Delete SoftDelete races. Both fail on current :exec SoftDelete behavior (loser still mints a successor). Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Switch SoftDeleteModelRouterAPIKey to :execrows and have RotateAPIKey bail with ErrAPIKeyNotFound when SoftDelete matches no row, closing the concurrent-rotate and rotate-vs-delete zombie-key races from workweave#817. DeleteAPIKey keeps discarding the count for idempotent success. Also lands the DB-backed repro scripts referenced by workweave#817. Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
5a20313 to
e15246d
Compare
|
PR author is not in the allowed authors list. |
|
Thank you so much for this fix, @rohith500 — the diagnosis of the #817 race is spot-on, and I really appreciate the thoroughness: the I've opened #822 as a compliance rewrite so it can land cleanly. To be totally clear: this is not on you at all — the only change is trimming a few code comments. Our comment conventions are internal, still evolving, and genuinely not obvious from outside the repo. Your production logic and tests were kept verbatim, and you're credited as co-author. The one convention applied (root Truly great catch on the zombie-credential race. Closing this in favor of #822; thanks again! 🙏 |
Summary
Fixes #817:
RotateAPIKeyno longer mints a successor when its ownSoftDeletematches 0 rows (lost race against another Rotate or a Delete).SoftDeleteModelRouterAPIKey::exec→:execrowsAPIKeyRepository.SoftDeletereturns(int64, error)RotateAPIKey:n == 0→ErrAPIKeyNotFound(noIssueAPIKey)DeleteAPIKey: still discards the count (idempotent success unchanged)Coordination with #726 / #565
Not yet on main (
IssueAPIKeyWithCapabsent). This PR uses today'sIssueAPIKeycall. When #726/#565 lands, expect a small rebase: the rows-affected check stays immediately before whicheverIssue*call is current (IssueAPIKeyWithCapwithtarget.SpendCapUsdMicros). Complementary, not conflicting — but should not land independently without rebase awareness (see #817).Known pre-existing limitation (not introduced by this PR, deliberately unaddressed here)
If
SoftDeletesucceeds (n==1) but the subsequentIssueAPIKeycall fails for any reason, the caller gets an error and no token — but the old key is already revoked with no replacement, and the installation cache is not invalidated on that path. This is pre-existing behavior, confirmed unchanged by this PR (see adversarial recheck section 7). Not fixing here to keep this PR scoped to the #817 race specifically; worth its own issue if it's causing real support load.Test plan
make checkgreenscripts/rotate_key_race_check(N=5, List-held-open)Before (#817): 5 successes, 5 active keys.
scripts/rotate_key_race_ext_check1. Concurrency sweep (List-held-open)
api key not found×1Before: N successes → N active keys (linear).
2. Rotate vs Delete
Before:
zombie_replacements=10(Delete wins SoftDelete, Rotate still Issues).3. HTTP (
httptest+WithAdminOnly→POST /admin/v1/keys/:id/rotate)(
ErrAPIKeyNotFound→ 404 via existingRotateAPIKeyHandler; no new status code.)Before:
map[201:5], 5 active keys.4. Timing sensitivity: 0/20 hits at all staggers (race closed).
scripts/upsert_external_key_race_check(regression)Unaffected — still DB-protected by
(installation_id, provider)unique index.Made with Cursor