Pular para o conteúdo

erasure-flow

identity specs/identity/erasure-flow.kmd

Corpo da especificação

Right-to-Erasure 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 contrato 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

MethodPathPurposeAuth
DELETE/v1/meInicia erasureBearer + ?confirm=DELETE_MY_ACCOUNT
POST/v1/me/erasure/{id}/cancelCancela durante graceBearer
GET/v1/me/erasure/{id}Status do requestBearer

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 literal. Caso ausente: retorna 400 com mensagem explicando o requirement. Previne acidental delete via DELETE method casual.

R4 — Cascade rules (per R5 da policy)

TabelaActionWindow
usersDELETEimediato após grace
user_identitiesDELETEimediato após grace
auth_flows (user_id=self)DELETEimediato
sso_sessions (user_id=self)REVOKE imediato; DELETE após grace24h grace
lockouts (user_id=self)DELETEimediato após grace
auth_events (user_id=self)ANONIMIZARretention 6m, então DELETE
mfa_devices (user_id=self)DELETEimediato após grace
passkeys (user_id=self)DELETEimediato após grace
pats (user_id=self)DELETEimediato após grace
workspaces_membership (user_id=self)DELETE membershipimediato após grace; workspace permanece

Anonimizaçã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 (success/failure/locked), tenant_id (audit integrity)

Salt 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 — Multi-tenancy guard (cross-tenant safety)

Cascade NUNCA atravessa tenant boundary. Workspace ownership:

  • User é signatário em workspace alheio → membership deletada, workspace mantida
  • User é OWNER de workspace → erasure falha com 409 + mensagem "transfira ownership ou delete workspace primeiro"
  • Workspace órfã não é permitida (regra invariante)

Cross-tenant isolation segue specs/multi-tenancy/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 — Multi-tenancy guard (cross-tenant safety)

  1. User_A pertence a workspace owned by User_B
  2. User_A: DELETE /v1/me?confirm=DELETE_MY_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. User_C: DELETE /v1/me?confirm=DELETE_MY_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

  • meta/docs/stack/specs/identity/erasure-flow.kmd (este arquivo)
  • Move services/foundation/id/engine#090 pra in-progress
  • 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: DELETE/cancel/status 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 + N1-N3 (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 outros 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-data-retention.kmd § R2).

Mudanças desde origem

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

Referências