[Backend] Preserve colliding content instead of deleting it during account-deletion anonymization #412

Closed
opened 2026-08-25 20:44:52 +00:00 by claude-bot · 0 comments
Contributor

Severity: LOW

Found in the August 2026 session lifecycle review (#319).

When a user deletes their account, rows that would collide with an already-anonymised "deleted user" tombstone on a uniqueness constraint are deleted outright rather than anonymised — so if a second GM with a public note on the same session later deletes their own account, that second GM's note content is destroyed (not tombstoned like everyone else's), because it collides with the first deleted GM's now-anonymised note on the same (session_id, visibility) uniqueness pair. The same collision-driven deletion applies to votes and attendance rows, silently shrinking historical tallies.

Evidence

  • webapp/backend/app/services/account_deletion_service.py:119-158 (_repoint_anonymize) — builds a collision subquery checking whether re-pointing a row's user_id to the tombstone user would collide with a row the tombstone already owns on the given conflict_cols, and for any row that collides, deletes it (collide = delete(model).where(...), executed before the anonymising update) rather than merging or preserving its content.
  • Applied to SessionNote (:244, conflict on (session_id, visibility)), Vote (:233), and SessionAttendance (:236) — any of these can collide once more than one account that used the deletion flow touches the same row-uniqueness key.

Failure scenario
Two different GMs who have both left the group each request account deletion at different times. The first GM's public note for a shared session is anonymised onto the tombstone user. Months later the second GM, who also had a public note on that same session, deletes their account — their note collides with the tombstone's existing (session_id, visibility='public') row, and rather than being preserved, it is deleted outright. The campaign's historical record silently loses one GM's content, in a way indistinguishable from it never having existed.

Proposed fix
On a collision, merge instead of delete — for notes, concatenate the colliding content onto the tombstone's existing row (mirroring the pattern the bot's /note append already uses) rather than discarding it; for votes/attendance, where merging doesn't make semantic sense (only one vote per person is meaningful), keep the existing row and drop the redundant new one only after confirming it doesn't represent unique information worth retaining as a count, or accept the tally shrink as a documented, bounded trade-off if a genuine merge isn't feasible.

Acceptance criteria

  • A colliding SessionNote on account deletion has its content preserved (merged into the tombstone row) rather than deleted.
  • The collision behaviour for votes/attendance is a documented, deliberate decision rather than an incidental content loss.
  • Regression test: two accounts with colliding public notes on the same session both delete their accounts; the merged tombstone note contains both authors' content.
**Severity: LOW** Found in the August 2026 session lifecycle review (#319). When a user deletes their account, rows that would collide with an already-anonymised "deleted user" tombstone on a uniqueness constraint are deleted outright rather than anonymised — so if a second GM with a public note on the same session later deletes their own account, that second GM's note content is destroyed (not tombstoned like everyone else's), because it collides with the first deleted GM's now-anonymised note on the same `(session_id, visibility)` uniqueness pair. The same collision-driven deletion applies to votes and attendance rows, silently shrinking historical tallies. **Evidence** - `webapp/backend/app/services/account_deletion_service.py:119-158` (`_repoint_anonymize`) — builds a `collision` subquery checking whether re-pointing a row's `user_id` to the tombstone user would collide with a row the tombstone already owns on the given `conflict_cols`, and for any row that collides, deletes it (`collide = delete(model).where(...)`, executed before the anonymising update) rather than merging or preserving its content. - Applied to `SessionNote` (`:244`, conflict on `(session_id, visibility)`), `Vote` (`:233`), and `SessionAttendance` (`:236`) — any of these can collide once more than one account that used the deletion flow touches the same row-uniqueness key. **Failure scenario** Two different GMs who have both left the group each request account deletion at different times. The first GM's public note for a shared session is anonymised onto the tombstone user. Months later the second GM, who also had a public note on that same session, deletes their account — their note collides with the tombstone's existing `(session_id, visibility='public')` row, and rather than being preserved, it is deleted outright. The campaign's historical record silently loses one GM's content, in a way indistinguishable from it never having existed. **Proposed fix** On a collision, merge instead of delete — for notes, concatenate the colliding content onto the tombstone's existing row (mirroring the pattern the bot's `/note` append already uses) rather than discarding it; for votes/attendance, where merging doesn't make semantic sense (only one vote per person is meaningful), keep the existing row and drop the redundant new one only after confirming it doesn't represent unique information worth retaining as a count, or accept the tally shrink as a documented, bounded trade-off if a genuine merge isn't feasible. **Acceptance criteria** - [ ] A colliding `SessionNote` on account deletion has its content preserved (merged into the tombstone row) rather than deleted. - [ ] The collision behaviour for votes/attendance is a documented, deliberate decision rather than an incidental content loss. - [ ] Regression test: two accounts with colliding public notes on the same session both delete their accounts; the merged tombstone note contains both authors' content.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#412
No description provided.