Right-to-Erasure Flow for Koder ID
RighttoErasure Flow for Koder ID
Origem
LGPD Art. 18 §IV (direito à eliminação) + GDPR Art. 17 (right to erasure) estabelecem que o titular pode requerer apagamento dos próprios dados. Lei 15.263/2025 (Nov 2025) + ANPD enforcement guidance trouxeram urgência operacional: fines até 2% receita anual / R$ 50M por breach.
policies/identity-data-retention.kmd § R5 definiu o *ontrato comportamental*de alto nível. Esta spec ratifica a implementação detalhada — endpoints, máquina de estados, schema, testes — pra que múltiplas surfaces (Koder ID admin, Koder Sign, Crescer/Vivver tenants) possam confiar num contrato estável.
Contrato (R-rules)
R1 — Endpoint trio
| Method | Path | Purpose | Auth |
|---|---|---|---|
DELETE |
/v1/me |
Inicia erasure | Bearer + ?confirm=DELETE_MY_ACCOUNT |
POST |
/v1/me/erasure/{id}/cancel |
Cancela durante grace | Bearer |
GET |
/v1/me/erasure/{id} |
Status do request | Bearer |
Tickets de implementação:
services/foundation/id/engine#090(Wave 2 — Go impl)
R2 — Resposta de iniciation
DELETE /v1/me?confirm=DELETE_MY_ACCOUNT retorna 202 Accepted:
{
"erasure_id": "ERS-2026-05-14-abc123",
"status": "pending",
"execute_at": "2026-05-15T12:34:56Z",
"grace_period_seconds": 86400,
"cancel_url": "/v1/me/erasure/ERS-2026-05-14-abc123/cancel",
"status_url": "/v1/me/erasure/ERS-2026-05-14-abc123"
}Erasure ID format: ERS-YYYY-MM-DD-<8-char-hex> (date prefix + hex of random nonce).
R3 — Confirm guard
Endpoint requer ?confirm=DELETE_MY_ACCOUNT query param *iteral* Caso ausente: retorna 400 com mensagem explicando o requirement. Previne acidental delete via DELETE method casual.
R4 — Cascade rules (per R5 da policy)
| Tabela | Action | Window |
|---|---|---|
users |
DELETE | imediato após grace |
user_identities |
DELETE | imediato após grace |
auth_flows (user_id=self) |
DELETE | imediato |
sso_sessions (user_id=self) |
REVOKE imediato; DELETE após grace | 24h grace |
lockouts (user_id=self) |
DELETE | imediato após grace |
auth_events (user_id=self) |
ANONIMIZAR | retention 6m, então DELETE |
mfa_devices (user_id=self) |
DELETE | imediato após grace |
passkeys (user_id=self) |
DELETE | imediato após grace |
pats (user_id=self) |
DELETE | imediato após grace |
workspaces_membership (user_id=self) |
DELETE membership | imediato após grace; workspace permanece |
*nonimização de auth_events*
user_id→hash(user_id + global_salt)(deterministic, allows pattern-finding sem revelar identity)ip_address→hash(ip + global_salt)(mesmo principle)user_agent→NULLdevice_id,geo_country/region/city,asn→NULL- Preservar:
event_type,timestamp,outcome(successfailurelocked),tenant_id(audit integrity)
*alt global* KODER_ID_ANONYMIZATION_SALT env var, rotacionado anualmente. Após rotação, eventos novos usam novo salt; antigos permanecem (hash binding constante por evento).
R5 — Multitenancy guard (crosstenant safety)
Cascade NUNCA atravessa tenant boundary. Workspace ownership:
- User é signatário em workspace alheio → membership deletada, workspace mantida
- User é OWNER de workspace → erasure *alha com 409*+ mensagem "transfira ownership ou delete workspace primeiro"
- Workspace órfã não é permitida (regra invariante)
Crosstenant isolation segue `specs/multitenancy/contract.kmd` T7.
R6 — Backup-restore guard
Após restore de backup:
- Worker
erasure_replayroda imediatamente após restore-completed signal - Varre
erasure_requestscomstatus=executed - Re-aplica cascade pra cada
user_idlistado - Logs em
audit_eventscom type=backup_restore_erasure_replay
Mecanismo: tabela erasure_requests NÃO é purgada — preserva audit trail permanente do exercício do direito. Sibling de auth_events anonimização mas pra request-level metadata.
R7 — Cancel during grace
POST /v1/me/erasure/{id}/cancel durante grace window:
- Verifica request
status=pendingANDexecute_at > now() - Marca
status=cancelled+cancelled_at=now() - Restaura sessões revogadas (se logout forçado já aconteceu, user precisa
re-login mas mantém account)
- Retorna
200 OKcom{"status": "cancelled"}
Após grace expirar: cancel não é mais possível (404 + mensagem "erasure already executed").
R8 — Schema erasure_requests
CREATE TABLE erasure_requests (
id TEXT PRIMARY KEY, -- ERS-YYYY-MM-DD-xxxxxxxx
user_id TEXT NOT NULL, -- subject of erasure
tenant_id TEXT NOT NULL, -- tenant scope (multi-tenancy)
initiated_at TIMESTAMP NOT NULL,
execute_at TIMESTAMP NOT NULL, -- initiated_at + 24h
status TEXT NOT NULL, -- pending|executed|cancelled
cancelled_at TIMESTAMP, -- nullable
executed_at TIMESTAMP, -- nullable
cancel_reason TEXT, -- nullable, opcional user input
initiator_ip TEXT, -- audit hint, hashed per anonimization rules pós-erasure
initiator_ua TEXT -- nullable pós-erasure
);
CREATE INDEX idx_erasure_pending ON erasure_requests(status, execute_at) WHERE status='pending';R9 — Worker cadence
Goroutine erasure_worker em services/identity/internal/retention/:
- Cadence: 5 minutos (env
KODER_ID_ERASURE_POLL_INTERVAL, default5m) - Query:
WHERE status='pending' AND execute_at <= now() LIMIT 100 - Process: transaction-atomic per request (rollback all cascades on partial failure)
- Em case de failure: increment
retry_count, retry after 30min; após 3 falhasalert ops via
audit_eventstype=erasure_failed
R10 — Companion: data export GET /v1/me/data-export (R7 da policy)
NÃO coberto por esta spec — separar em ticket #091 (TBD) + specs/identity/data-export.kmd companion.
Test contract (T-rules)
T1 — Happy path
- User authenticated faz
DELETE /v1/me?confirm=DELETE_MY_ACCOUNT - Recebe
202comerasure_id+execute_at24h future - Sessões existentes do user:
revokedimediato - Fixture time-travel +25h
- Worker executa:
usersrow gone,auth_eventsanonimizados GET /v1/me/erasure/{id}retornastatus=executed
T2 — Cancel during grace
- T1 steps 1-3
- T+1h:
POST /v1/me/erasure/{id}/cancel - Recebe
200comstatus=cancelled - User re-login → account intacta
auth_eventshistóricos preservados (sem anonimização)
T3 — Confirm guard
DELETE /v1/me(sem query string) → 400 + mensagem confirm requirementDELETE /v1/me?confirm=wrong→ 400DELETE /v1/me?confirm=DELETE_MY_ACCOUNT(literal) → 202
T4 — Multitenancy guard (crosstenant safety)
- UserA pertence a workspace owned by UserB
- UserA: `DELETE v1me?confirm=DELETEMY_ACCOUNT` → 202 success
- Após execute:
users.user_agone, workspace mantida (User_B ainda dono) workspaces_membershipentry de User_A: deletada
T5 — Workspace owner guard
- User_C é owner do workspace W
- UserC: `DELETE v1me?confirm=DELETEMY_ACCOUNT`
- Recebe
409 Conflict+ mensagem "transfira ownership ou delete workspace primeiro" - Erasure não criado em
erasure_requests
T6 — Backup restore replay
- T1 happy path completes (user deleted)
- Backup taken before T1
- Restore from backup
erasure_replayworker varre + re-aplica- User row again gone após replay
N1 — Negative: double-delete idempotência
- T1 steps 1-2 (request created, pending)
- Segundo
DELETE /v1/me?confirm=DELETE_MY_ACCOUNTdurante pending → 409 comexisting_erasure_id - Não cria segundo request
N2 — Negative: cancel after execute
- T1 complete (executed)
POST /v1/me/erasure/{id}/cancel→ 404 + mensagem "already executed"
N3 — Negative: salt-free anonimização blocked
- Env
KODER_ID_ANONYMIZATION_SALTausente - Worker boot detecta + falha boot com erro explícito (não silencioso)
- Previne anonimização determinística com salt vazio (= no anonimização real)
Implementação
Wave 1 (atual) — Spec ratification
- [x]
meta/docs/stack/specs/identity/erasure-flow.kmd(este arquivo) - [x] Move
services/foundation/id/engine#090pra in-progress - [x] Lock
services-foundation-id-erasureplaced
Wave 2 (next /k-go cycle)
- [ ] Migration:
erasure_requeststable schema (kdbnext) - [ ] Model + repository em
services/identity/internal/repository/erasure.go - [ ] Service method:
IdentityService.RequestErasure/CancelErasure/ExecuteErasure - [ ] Handler: DELETEcancelstatus no
me_http.go - [ ] Worker:
services/identity/internal/retention/erasure_worker.go(mirror sso pattern) - [ ] Wire worker into
services/identity/cmd/main.go - [ ] Tests T1
T6 + N1N3 (Go test files) - [ ] CHANGELOG entry
Wave 3 (follow-up)
- [ ] Companion spec
specs/identity/data-export.kmd+ impl (#091) - [ ] UI surface em
services/foundation/id/account-ui(delete account screen) - [ ] Notification email "your erasure request was initiated" + cancel link
- [ ] Admin dashboard pra ver pending requests (oversight)
Non-scope
- Erasure de dados em *utros*componentes Koder (Koder Sign signatures,
Koder Drive files) — cada componente owns sua cascade. Esta spec cobre só Koder ID engine. Cross-component coordination via event bus + Koder ID emite
koder.id.user.erased(id hashed only). - Workspace deletion (R5 §3 só cobre membership; full workspace erasure
precisaria de spec própria).
- Hard delete pra audit logs após 6m anonimization (cobertura existente em
policies/identity
dataretention.kmd § R2).
Mudanças desde origem
| Data | Mudança | Razão |
|---|---|---|
| 2026 |
Ratificada Draft → Status ratified-2026-05-14 |
Wave 1 do ticket #090; Agent run #098 surface LGPD urgência (Lei 15.263/2025) |