You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
juancarlos.cavero
committed
fix(cloud): close the gaps the account-deletion review turned up
Eight findings from re-reading the erasure path, each one something that would
have shown up in production rather than in a test:
- The local purge missed two stores. SaaSShorts outputs (output/saas_<id>) and
generated thumbnails record ownership in their own in-memory dicts, not in
the .owner file, so neither was erased. Thumbnails are the serious half: the
hourly sweep deliberately skips their directory, so a deleted user's
generated images stayed on disk forever, publicly served at /thumbnails/.
- The purge ran on the event loop. rmtree over gigabytes of video stalled every
other request on the process; it goes through asyncio.to_thread now.
- Paths from those dicts are now resolved through _safe_under, so a session id
or output_dir containing ".." deletes nothing.
- Deleting a user made a webhook path reachable that never was: _apply_topup
takes the user id from Stripe metadata, so a top-up completing around the
deletion inserted a row pointing at a user that no longer exists. The FK
violation 500s the webhook, and since the event is only recorded after
handle_event returns, Stripe retries the same doomed insert for three days.
It confirms the row exists first.
- The "why are you leaving" box was free text stored in a record that
deliberately outlives the account, which is how personal data ends up in one
by accident. It is a closed list now, which also makes twenty deletions a
month countable instead of readable.
- Two concurrent deletes wrote two erasure records and sent two goodbye emails.
The DELETE's rowcount now decides which call is the real one; the UI holds a
ref so a double click doesn't fire a second Stripe cancel and R2 purge.
- The delete button armed itself when the confirm box was empty, if the page
ever rendered before /api/me resolved.
- The goodbye email claimed we keep "a one-way hash of your email and nothing
else", which was not true of the Stripe reference beside it. Fixed in the
email and in the privacy policy, EN and ES.
Also: skip the Upload-Post call when no managed key is configured, and say
plainly in the in-flight comment that it covers metered work only.
tests/test_account_local_purge.py covers tenant isolation across all three
stores and both traversal attempts; test_account_erasure.py gains the
confirmation gate, the API-key refusal, and the already-cancelled-at-Stripe
case that would otherwise trap a user forever.
0 commit comments