docs/specs/2026-10-02-account-delete-design.md

v1.43.1
gitbay/docs/specs/2026-10-02-account-delete-design.md rendered · source · history · blame · raw

111 lines · 5069 bytes

  1# Account deletion
  2
  3Closes #322. An account deletes itself. Today only `admin user delete`
  4exists, and it refuses an account that anchors issues, merge requests,
  5comments, reviews or an org with no other admin.
  6
  7## Decision
  8
  9`account delete` is a write on every surface. It takes the typed
 10username, mails a confirmation link to the primary verified address,
 11and on confirmation marks the account for deletion seven days out.
 12During those seven days the account is refused everywhere except the
 13two ways a person signs in, either of which cancels. After seven days
 14a reaper purges it: owned content goes, authored content on other
 15owners' repositories moves to a `ghost` account.
 16
 17## States
 18
 19| state | set by | SSH (full scope) | SSH (other scopes), API tokens | web session | web login link |
 20|---|---|---|---|---|---|
 21| requested | `account delete --confirm <name>` | normal | normal | normal | normal |
 22| scheduled | the mailed link | cancels, then normal | refused | ended | cancels, then normal |
 23| purged | the reaper | no account | no account | no account | no account |
 24
 25- *requested* is a row in `account_deletions` (user_id, token_hash,
 26  requested_at, expires_at; the link lives 24 hours). Nothing about the
 27  account changes. A second request replaces the first.
 28- *scheduled* sets `users.delete_after` (now + 7 days) and ends every
 29  web session. A full-scope SSH session or a completed login link clears
 30  `delete_after`, is audited `account.delete.cancelled`, and prints or
 31  shows "deletion of your account was cancelled". Runner-, read- and
 32  deploy-scoped keys and API tokens are refused with "this account is
 33  scheduled for deletion on <date>; sign in to cancel" so automation
 34  cannot cancel by accident.
 35- *purged*: the reaper, on the tick that already runs
 36  `ReapPendingUsers`, takes every account with `delete_after` in the
 37  past.
 38
 39## Purge
 40
 41One function in `internal/control` (it needs the repository root), run
 42from the reaper, in this order:
 43
 441. Re-check the org rule. If the account is now the only admin of an
 45   org, skip, leave `delete_after` set, audit
 46   `account.delete.blocked`, and show it in `admin user show`. An
 47   instance admin resolves it.
 482. Delete every repository the account owns through `deleteRepo`, the
 49   same path as `repo delete` (open MRs sourced from them are marked
 50   source-gone; the directories go).
 513. In one transaction: reassign `issues.author_id`,
 52   `merge_requests.author_id`, `issue_comments.author_id`,
 53   `mr_comments.author_id`, `mr_diff_comments.author_id` and
 54   `mr_reviews.reviewer_id` to the ghost; then `DeleteUser`, whose
 55   anchor check now passes. Everything else is already `CASCADE` (keys,
 56   emails, tokens, sessions, snippets, memberships, watches, reactions,
 57   assignments, inbox, review requests) or `SET NULL` (audit actor,
 58   event actor, signatures, statuses, merged/closed by, releases).
 59   Grants and the profile backfill row go in the same transaction, as
 60   `DeleteUser` already does.
 614. Audit `account.delete.purged` with the username.
 62
 63The username is free again after the purge. User ids are never reused
 64(#306).
 65
 66## The ghost
 67
 68A real `users` row named `ghost`, created by the first purge that needs
 69it and flagged `users.ghost = 1`: it has no keys, emails or sessions,
 70cannot be signed in to, own anything, be granted access, or be deleted.
 71`ghost` is added to `internal/policy/names.go` so nobody can register
 72it. If an instance already has a real account named `ghost`, the purge
 73refuses with a message telling the operator to rename it; gitbay.org
 74has none. Its profile page reads "This account stands in for deleted
 75users." Content shows `ghost` as its author, the way GitHub shows it.
 76
 77## Surfaces
 78
 79- `account delete --confirm <username>`: refuses a mismatched name
 80  (exit 2), an account with no verified address (exit 4, "add and
 81  verify an address first"), and the only admin of an org (exit 4,
 82  naming the orgs). Prints where the mail went and suggests
 83  `account export`.
 84- `account delete --cancel`: clears a request or a schedule (the
 85  scheduled case is reachable only from a full-scope key, which already
 86  cancels on connect; this exists for the requested state).
 87- Web: a "Delete account" section at the bottom of `/settings` with
 88  the `confirmfield` partial, posting the same command. The mailed link
 89  opens `/settings/delete?token=<token>`, a page that names the purge date
 90  and has one button; the GET changes nothing.
 91- The mail names the purge date, says how to cancel, and suggests
 92  `account export`.
 93- `admin user show` reports `delete_after` and a blocked purge.
 94
 95## Migration
 96
 97`account_deletions`; `users.delete_after TEXT`; `users.ghost INTEGER
 98NOT NULL DEFAULT 0`.
 99
100## Docs
101
102Parity row; Users wiki page (a "Deleting your account" section);
103`/privacy` text; Terms wiki page's "Leaving" section, which today says
104to mail the operator.
105
106## Not doing
107
108- Deleting authored content on other owners' repositories. Decided on
109  #322: it moves to the ghost.
110- An admin-initiated version with the grace period. `admin user delete`
111  stays as it is.