The right to erasure is not a database-wide Delete. A production implementation distinguishes deletion, anonymization, statutory retention, immutable financial or security evidence, and external processor notification. It also records a provable outcome for each step.
1. Current routes
| Route | Current behavior |
|---|---|
POST /api/privacy/erasure | create CoolingOff with a UTC deadline 30 days later |
DELETE /api/privacy/erasure/{id} | owner cancels during CoolingOff |
POST /api/privacy/erasure/{id}/execute | operator rejects or anonymizes the Identity user |
Create and cancel evaluate gdpr.own-data.create/delete. Execute evaluates privacy.erasure.delete. These differ from catalog value privacy.erasure.manage.
2. Cooling-off
CreateErasure writes CoolingOff(1) directly and sets CoolingOffEndsAt = now + 30 days. The SmartEnum has Submitted(0), but creation does not use it.
var response = await http.PostAsJsonAsync( "/api/privacy/erasure", new { requestId = stableRequestId }, cancellationToken);
var request = await response.Content .ReadFromJsonAsync<DataSubjectRequestSummary>(cancellationToken);
// The current summary does not expose CoolingOffEndsAt.// A client cannot display the exact cancellation deadline from this response.No background job executes or escalates an expired request. There is no deadline queue or operator reminder.
3. Transitions are not closed
ExecuteErasure rejects only Completed and Cancelled. It checks the deadline only when status is CoolingOff. Submitted, Executing, Rejected, and a wrong request type can reach anonymization.
A central transition guard must validate request type, source status, deadline, and expected row version.
4. Request-type confusion is release-blocking
SAR and erasure share SysDataSubjectRequest and an integer StatusCode. Execute and cancel load by ID without checking RequestType:
- SAR Submitted(0) is erasure Submitted and can bypass cooling-off;
- SAR Processing(1) is erasure CoolingOff and can be cancelled;
- cross-type creation with the same RequestId can return the wrong row.
Hiding the wrong action in a UI is not a security control.
5. Actual erasure scope
GdprStore.AnonymizeUserAsync verifies that the request tenant equals Effective Tenant and invokes Identity’s IIdentityPersonalDataEraser. The implementation updates only SysUser:
| Field | Update |
|---|---|
| UserName / DisplayName | anon-{digest} |
anon-{digest}@erased.local | |
| normalized fields | corresponding anonymous values |
| Phone | empty string |
| AvatarUrl | null |
| PasswordHash | ERASED |
| SecurityStamp | new GUID |
| IsDeleted | true |
The digest is the first 24 hexadecimal characters of SHA256(tenantId:userId) and has no server secret. It is stable and useful for uniqueness, but a party that knows the IDs can recompute it.
6. Data not covered
The port does not orchestrate:
- RefreshToken, UserSession, UserToken, ExternalLogin, or FIDO2 credentials;
- PasswordHistory, UserDevice, LoginLog, or organization relations;
- Files, Documents, Comments, Chat, Tickets, Billing, and other business owners;
- search, caches, backups, analytics, or external processors;
- other
SysUserfields such as PhoneHash, confirmation flags, last login, organizational IDs, and ScimExternalId.
Changing SecurityStamp may invalidate some tokens. It is not explicit revocation of all credentials and sessions. Soft deletion is not physical destruction.
7. Transaction behavior
The command runs inside TransactionPipelineBehavior:
- set DSR to Executing;
- update the Identity user through the persistence abstraction;
- set DSR to Completed;
- commit after a successful Result.
With one database and one UoW adapter, these writes can commit atomically. Object storage, search, and external processors cannot join that local transaction. Future orchestration needs durable steps, Outbox, idempotent consumers, and recovery.
8. Legal holds and retention decisions
There is no Legal Hold or Retention Decision in current source. GDPR should not guess whether an invoice, order, or security record can be removed. Each owner returns a typed disposition.
public interface IDataErasureContributor{ // OwnerCode must remain stable after an execution plan is frozen. string OwnerCode { get; }
// The owner chooses deletion, anonymization, or lawful retention. Task<ErasureOutcome> ExecuteAsync( VerifiedSubject subject, ErasureExecutionContext context, CancellationToken cancellationToken);}
public sealed record ErasureOutcome( ErasureDisposition Disposition, // Deleted, Anonymized, or Retained int AffectedRecords, string EvidenceHash, string? RetentionReasonCode, DateTimeOffset? RetainUntil, bool Retryable);RetentionReasonCode should reference a governed policy registry, not store free-form legal advice.
9. Recommended orchestration
Completion means every required owner in the frozen set has a terminal outcome. It cannot mean merely that the last call did not throw.
10. Idempotency and recovery
A step key can include TenantId + ErasureRequestId + OwnerCode + ContractVersion. Persist:
- attempt, start, and completion times;
- a non-PII subject reference;
- owner contract version;
- disposition, affected count, and evidence hash;
- retryability, next attempt, and error code;
- operator, approval, and correlation.
Retries must not reopen Rejected or Cancelled requests and must re-check a newly introduced legal hold.
11. Test contract
The existing parity test proves that both ORMs soft-delete the Identity user and produce an @erased.local email. Missing coverage includes:
- SAR IDs rejected by every erasure handler;
- Rejected, Executing, and Submitted cannot execute;
- the exact cooling-off equality boundary;
- uniform cross-tenant ID behavior;
- policy when the Identity user does not exist;
- credential and session handling;
- owner partial failure, retry, and legal hold;
- transaction and Outbox consistency;
- deterministic pseudonym re-identification risk;
- backup and downstream processor expiry evidence.
12. Source review
# Inspect the exact fields changed by the current anonymizer.sed -n '1,180p' \ src/Platform/Identity/BitzOrcas.Identity.Infrastructure/Identity/IdentityPersonalDataEraser.cs
# Prove the absence or presence of type guards and owner orchestration.rg -n "RequestType|IDataErasureContributor|LegalHold" \ src/Platform/Gdpr/BitzOrcas.Platform.Gdpr.Application/Commands
# Inventory Identity records that still reference UserId.rg -n "UserId" src/Platform/Identity -g '*.cs'