A failing delete sets .Error on the returned session and leaves the transaction usable, so the code commits whatever did succeed. The operator is redirected to /sources as though the deletion worked, and h.dbMgr.DeleteDB then hard-deletes the per-webhook event database anyway — its own error is logged and swallowed too. The outcome is a webhook whose entrypoints or targets survive in the main database while its entire event history is permanently gone, reported as a success.
commitWebhook, twenty lines above in the same file, does check each statement and rolls back, so this reads as an oversight rather than a decision.
Definition of done: check each delete, roll back and return a 500 on any failure, and do not delete the event database unless the configuration delete committed.
Found while auditing every `Begin`/`Commit`/`Rollback` path for https://git.eeqj.de/sneak/webhooker/issues/256. Not fixed there: it is a different failure and out of that issue's scope.
`deleteWebhookResources` (`internal/handlers/source_management.go`) opens a transaction and issues three deletes without checking any of them:
```go
tx.Where("webhook_id = ?", webhook.ID).Delete(&database.Entrypoint{})
tx.Where("webhook_id = ?", webhook.ID).Delete(&database.Target{})
tx.Delete(&webhook)
err := tx.Commit().Error
```
A failing delete sets `.Error` on the returned session and leaves the transaction usable, so the code commits whatever did succeed. The operator is redirected to `/sources` as though the deletion worked, and `h.dbMgr.DeleteDB` then **hard-deletes the per-webhook event database anyway** — its own error is logged and swallowed too. The outcome is a webhook whose entrypoints or targets survive in the main database while its entire event history is permanently gone, reported as a success.
`commitWebhook`, twenty lines above in the same file, does check each statement and rolls back, so this reads as an oversight rather than a decision.
Definition of done: check each delete, roll back and return a 500 on any failure, and do not delete the event database unless the configuration delete committed.
Extract the deletion transaction into commitWebhookDeletion, matching commitWebhook's idiom in the same file: check every statement, tx.Rollback() and return the error on any failure.
deleteWebhookResources returns h.serverError on that error and does not touch the event database, so a failed configuration delete loses nothing.
Ordering choice for the window that has no transaction spanning SQLite and the filesystem: commit the configuration first, then DeleteDB. If DeleteDB then fails the webhook is gone and an unreferenced event database file is left on disk — reported to the operator with h.serverError rather than a success redirect, since a leftover file can be removed by hand while deleted history cannot be recovered.
Tests: a failing delete statement (injected on the targets table, which leaves the transaction usable exactly as a real database error does) must leave the webhook, entrypoint, target and event database file all intact and return 500; plus a positive control that a normal deletion still removes all four.
Audit: the repo has three Begin() sites. commitWebhook and createAndFanOut (internal/handlers/webhook.go) both check every statement and roll back; this was the only one that did not.
Plan:
- Extract the deletion transaction into `commitWebhookDeletion`, matching `commitWebhook`'s idiom in the same file: check every statement, `tx.Rollback()` and return the error on any failure.
- `deleteWebhookResources` returns `h.serverError` on that error and does not touch the event database, so a failed configuration delete loses nothing.
- Ordering choice for the window that has no transaction spanning SQLite and the filesystem: commit the configuration first, then `DeleteDB`. If `DeleteDB` then fails the webhook is gone and an unreferenced event database file is left on disk — reported to the operator with `h.serverError` rather than a success redirect, since a leftover file can be removed by hand while deleted history cannot be recovered.
- Tests: a failing delete statement (injected on the `targets` table, which leaves the transaction usable exactly as a real database error does) must leave the webhook, entrypoint, target and event database file all intact and return 500; plus a positive control that a normal deletion still removes all four.
- Audit: the repo has three `Begin()` sites. `commitWebhook` and `createAndFanOut` (`internal/handlers/webhook.go`) both check every statement and roll back; this was the only one that did not.
commitWebhookDeletion now checks each of the three deletes and rolls back on any failure; deleteWebhookResources reports it with h.serverError and does not touch the event database. The configuration commit runs before DeleteDB, and a DeleteDB failure is reported with h.serverError rather than the old success redirect, so the worst case is a leftover event database file the operator is told about instead of history destroyed behind a 303.
Verified: make check green with GOFLAGS=-count=1, lint 0 issues. A new test injects a failure into the targets delete (.Error set, transaction still usable — the exact reported condition) and asserts a 500 with the webhook, entrypoint, target and event database file all intact; with only the handler reverted it fails on every one of those assertions, including unable to find file .../events-<id>.db. A positive-control test covers normal deletion, and a running instance was driven through create-target-delete end to end: 303 to /sources, webhook delisted, detail 404, event database file gone.
Audit: the repo has three Begin() sites; commitWebhook and createAndFanOut both check every statement, so this was the only one.
Implemented in https://git.eeqj.de/sneak/webhooker/pulls/273.
`commitWebhookDeletion` now checks each of the three deletes and rolls back on any failure; `deleteWebhookResources` reports it with `h.serverError` and does not touch the event database. The configuration commit runs before `DeleteDB`, and a `DeleteDB` failure is reported with `h.serverError` rather than the old success redirect, so the worst case is a leftover event database file the operator is told about instead of history destroyed behind a 303.
Verified: `make check` green with `GOFLAGS=-count=1`, lint 0 issues. A new test injects a failure into the `targets` delete (`.Error` set, transaction still usable — the exact reported condition) and asserts a 500 with the webhook, entrypoint, target and event database file all intact; with only the handler reverted it fails on every one of those assertions, including `unable to find file .../events-<id>.db`. A positive-control test covers normal deletion, and a running instance was driven through create-target-delete end to end: 303 to `/sources`, webhook delisted, detail 404, event database file gone.
Audit: the repo has three `Begin()` sites; `commitWebhook` and `createAndFanOut` both check every statement, so this was the only one.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while auditing every
Begin/Commit/Rollbackpath for #256. Not fixed there: it is a different failure and out of that issue's scope.deleteWebhookResources(internal/handlers/source_management.go) opens a transaction and issues three deletes without checking any of them:A failing delete sets
.Erroron the returned session and leaves the transaction usable, so the code commits whatever did succeed. The operator is redirected to/sourcesas though the deletion worked, andh.dbMgr.DeleteDBthen hard-deletes the per-webhook event database anyway — its own error is logged and swallowed too. The outcome is a webhook whose entrypoints or targets survive in the main database while its entire event history is permanently gone, reported as a success.commitWebhook, twenty lines above in the same file, does check each statement and rolls back, so this reads as an oversight rather than a decision.Definition of done: check each delete, roll back and return a 500 on any failure, and do not delete the event database unless the configuration delete committed.
Plan:
commitWebhookDeletion, matchingcommitWebhook's idiom in the same file: check every statement,tx.Rollback()and return the error on any failure.deleteWebhookResourcesreturnsh.serverErroron that error and does not touch the event database, so a failed configuration delete loses nothing.DeleteDB. IfDeleteDBthen fails the webhook is gone and an unreferenced event database file is left on disk — reported to the operator withh.serverErrorrather than a success redirect, since a leftover file can be removed by hand while deleted history cannot be recovered.targetstable, which leaves the transaction usable exactly as a real database error does) must leave the webhook, entrypoint, target and event database file all intact and return 500; plus a positive control that a normal deletion still removes all four.Begin()sites.commitWebhookandcreateAndFanOut(internal/handlers/webhook.go) both check every statement and roll back; this was the only one that did not.Implemented in #273.
commitWebhookDeletionnow checks each of the three deletes and rolls back on any failure;deleteWebhookResourcesreports it withh.serverErrorand does not touch the event database. The configuration commit runs beforeDeleteDB, and aDeleteDBfailure is reported withh.serverErrorrather than the old success redirect, so the worst case is a leftover event database file the operator is told about instead of history destroyed behind a 303.Verified:
make checkgreen withGOFLAGS=-count=1, lint 0 issues. A new test injects a failure into thetargetsdelete (.Errorset, transaction still usable — the exact reported condition) and asserts a 500 with the webhook, entrypoint, target and event database file all intact; with only the handler reverted it fails on every one of those assertions, includingunable to find file .../events-<id>.db. A positive-control test covers normal deletion, and a running instance was driven through create-target-delete end to end: 303 to/sources, webhook delisted, detail 404, event database file gone.Audit: the repo has three
Begin()sites;commitWebhookandcreateAndFanOutboth check every statement, so this was the only one.