Right-to-Erasure Flow for Koder ID

ratified-2026-05-14

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_idhash(user_id + global_salt) (deterministic, allows pattern-finding sem revelar identity)
  • ip_addresshash(ip + global_salt) (mesmo principle)
  • user_agentNULL
  • device_id, geo_country/region/city, asnNULL
  • 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:

  1. Worker erasure_replay roda imediatamente após restore-completed signal
  2. Varre erasure_requests com status=executed
  3. Re-aplica cascade pra cada user_id listado
  4. Logs em audit_events com 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=pending AND execute_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 OK com {"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, default 5m)
  • 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 falhas

    alert ops via audit_events type=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

  1. User authenticated faz DELETE /v1/me?confirm=DELETE_MY_ACCOUNT
  2. Recebe 202 com erasure_id + execute_at 24h future
  3. Sessões existentes do user: revoked imediato
  4. Fixture time-travel +25h
  5. Worker executa: users row gone, auth_events anonimizados
  6. GET /v1/me/erasure/{id} retorna status=executed

T2 — Cancel during grace

  1. T1 steps 1-3
  2. T+1h: POST /v1/me/erasure/{id}/cancel
  3. Recebe 200 com status=cancelled
  4. User re-login → account intacta
  5. auth_events históricos preservados (sem anonimização)

T3 — Confirm guard

  1. DELETE /v1/me (sem query string) → 400 + mensagem confirm requirement
  2. DELETE /v1/me?confirm=wrong → 400
  3. DELETE /v1/me?confirm=DELETE_MY_ACCOUNT (literal) → 202

T4 — Multitenancy guard (crosstenant safety)

  1. UserA pertence a workspace owned by UserB
  2. UserA: `DELETE v1me?confirm=DELETEMY_ACCOUNT` → 202 success
  3. Após execute: users.user_a gone, workspace mantida (User_B ainda dono)
  4. workspaces_membership entry de User_A: deletada

T5 — Workspace owner guard

  1. User_C é owner do workspace W
  2. UserC: `DELETE v1me?confirm=DELETEMY_ACCOUNT`
  3. Recebe 409 Conflict + mensagem "transfira ownership ou delete workspace primeiro"
  4. Erasure não criado em erasure_requests

T6 — Backup restore replay

  1. T1 happy path completes (user deleted)
  2. Backup taken before T1
  3. Restore from backup
  4. erasure_replay worker varre + re-aplica
  5. User row again gone após replay

N1 — Negative: double-delete idempotência

  1. T1 steps 1-2 (request created, pending)
  2. Segundo DELETE /v1/me?confirm=DELETE_MY_ACCOUNT durante pending → 409 com existing_erasure_id
  3. Não cria segundo request

N2 — Negative: cancel after execute

  1. T1 complete (executed)
  2. POST /v1/me/erasure/{id}/cancel → 404 + mensagem "already executed"

N3 — Negative: salt-free anonimização blocked

  1. Env KODER_ID_ANONYMIZATION_SALT ausente
  2. Worker boot detecta + falha boot com erro explícito (não silencioso)
  3. 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#090 pra in-progress
  • [x] Lock services-foundation-id-erasure placed

Wave 2 (next /k-go cycle)

  • [ ] Migration: erasure_requests table 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 T1T6 + 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/identitydataretention.kmd § R2).

Mudanças desde origem

Data Mudança Razão
20260514 Ratificada Draft → Status ratified-2026-05-14 Wave 1 do ticket #090; Agent run #098 surface LGPD urgência (Lei 15.263/2025)

Source: ../home/koder/dev/koder/meta/docs/stack/specs/identity/erasure-flow.kmd