fix(resource): idempotent DELETE — no double-deprovision (bug-bash #4/#12)#229
Merged
Merged
Conversation
62bd730 to
b968b1e
Compare
…12) A repeated DELETE on a resource (retry, double-click, or concurrent request) re-ran SoftDeleteResource and re-issued the backend deprovision a second time. The second teardown races the first and can error against an already-gone backend, surfacing a spurious 5xx for what is logically a success. Add an idempotent early-return after the team-ownership check: when the resource is already status='deleted', report success ({"ok":true,"already_deleted":true,"id":...}) and do nothing. The first DELETE already tore the backend down. Test: TestResourceDelete_Idempotent_DoubleDelete — first DELETE → 200, second DELETE → 200 with already_deleted=true. Verified against real Postgres+Redis locally; runs in CI build-and-test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b968b1e to
b0061db
Compare
mastermanas805
added a commit
that referenced
this pull request
Jun 3, 2026
…e-deprovision TOCTOU (#236) Design-review follow-up to #229. The idempotent-DELETE fix used a `resource.Status == "deleted"` PRE-READ before SoftDeleteResource + deprovision — a check-then-act TOCTOU: two concurrent DELETEs both read status='active', both pass the pre-read, and BOTH fire the destructive provisioner Deprovision / storage Deprovision RPC against the same backend. That is the truehomie-db DROP incident class (an active customer DB dropped via a path that re-ran teardown). Fix: make the soft-delete the authoritative atomic guard. New models.SoftDeleteResourceIfActive does `UPDATE ... SET status='deleted' WHERE id=$1 AND status != 'deleted'` and returns RowsAffected>0. The handler deprovisions ONLY when this call won the transition (deleted==true); any racer or sequential retry gets deleted==false → idempotent {"ok":true,"already_deleted":true} with NO second deprovision. Removes the redundant pre-read (the rows-affected check subsumes it and is race-safe). SoftDeleteResource is kept unchanged for the ~18 provision-rollback callers that always operate on a just-created row. Tests: models.TestSoftDeleteResourceIfActive_Branches (sqlmock: won / no-op / error — 100% of the new fn). The existing TestResourceDelete_Idempotent_ DoubleDelete now exercises the rows-affected !deleted branch via a sequential retry (200 already_deleted, no re-deprovision). All changed handler lines covered. Verified vs real Postgres+Redis. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A repeated
DELETE /resources/:token(retry, double-click, concurrent request) re-ranSoftDeleteResourceand re-issued the backend deprovision a second time. The second teardown races the first and can error against an already-gone backend — surfacing a spurious 5xx for what is logically a success.Fix
Idempotent early-return after the team-ownership check: when the resource is already
status='deleted', report success and do nothing.The first DELETE already tore the backend down; the ownership/404 posture is preserved (a resource you don't own still 404s before this check).
Coverage
First DELETE → 200; second DELETE → 200 with
already_deleted=true.🤖 Generated with Claude Code