Skip to content

fix(auth): reject RotateAPIKey when SoftDelete affects 0 rows#819

Open
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/rotate-api-key-soft-delete-race
Open

fix(auth): reject RotateAPIKey when SoftDelete affects 0 rows#819
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/rotate-api-key-soft-delete-race

Conversation

@rohith500

@rohith500 rohith500 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #817: RotateAPIKey no longer mints a successor when its own SoftDelete matches 0 rows (lost race against another Rotate or a Delete).

  • SoftDeleteModelRouterAPIKey: :exec:execrows
  • APIKeyRepository.SoftDelete returns (int64, error)
  • RotateAPIKey: n == 0ErrAPIKeyNotFound (no IssueAPIKey)
  • DeleteAPIKey: still discards the count (idempotent success unchanged)

Coordination with #726 / #565

Not yet on main (IssueAPIKeyWithCap absent). This PR uses today's IssueAPIKey call. When #726/#565 lands, expect a small rebase: the rows-affected check stays immediately before whichever Issue* call is current (IssueAPIKeyWithCap with target.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 SoftDelete succeeds (n==1) but the subsequent IssueAPIKey call 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

scripts/rotate_key_race_check (N=5, List-held-open)

concurrent_calls=5
rotate_successes=1
active_keys=1
RotateAPIKey error: api key not found  (×4)
race not observed this run (only one successor / one success)

Before (#817): 5 successes, 5 active keys.

scripts/rotate_key_race_ext_check

1. Concurrency sweep (List-held-open)

N Successes Active keys Errors
2 1 1 api key not found ×1
3 1 1 ×2
10 1 1 ×9
25 1 1 ×24

Before: N successes → N active keys (linear).

2. Rotate vs Delete

trials=10 zombie_replacements=0 rotate_errors=10 delete_errors=0
NO: did not observe a zombie replacement this run

Before: zombie_replacements=10 (Delete wins SoftDelete, Rotate still Issues).

3. HTTP (httptest + WithAdminOnlyPOST /admin/v1/keys/:id/rotate)

http_status_counts=map[201:1 404:4]
rotate_http_201=1 active_keys_with_race_name=1

(ErrAPIKeyNotFound → 404 via existing RotateAPIKeyHandler; 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)

upsert_successes=1 upsert_errors=1
psql active=1 soft_deleted=1
RESULT: unique constraint stops zombie keys; loser error propagates
pg_code=23505

Unaffected — still DB-protected by (installation_id, provider) unique index.

Made with Cursor

rohith500 and others added 2 commits July 22, 2026 21:06
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>
@rohith500
rohith500 marked this pull request as ready for review July 23, 2026 01:38
@rohith500
rohith500 force-pushed the fix/rotate-api-key-soft-delete-race branch from 5a20313 to e15246d Compare July 23, 2026 01:38
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Thank you so much for this fix, @rohith500 — the diagnosis of the #817 race is spot-on, and I really appreciate the thoroughness: the :exec:execrows change, the n == 0ErrAPIKeyNotFound guard, the in-memory fake-repo race tests, and the live-Postgres repro scripts under scripts/ are all exactly right.

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 CLAUDE.md, "Concise comments, sparingly"): comments should be minimal and explain why only — no multi-paragraph blocks, no narrating the previous/buggy behavior, and no referencing the current PR/caller. The expanded RotateAPIKey godoc (e.g. "...produced untracked zombie credentials when SoftDelete was :exec...") got condensed to a single-line invariant. Everything else — the SQL, sqlc regen, interface change, and tests — carried over unchanged.

Truly great catch on the zombie-credential race. Closing this in favor of #822; thanks again! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RotateAPIKey race: concurrent rotate/delete calls on the same key mint zombie successor keys

1 participant