The GDPR module manages consent history, subject access requests (SARs), and erasure requests. It already has provider-neutral persistence, SqlSugar/EF Core parity, a 30-day erasure cooling-off period, tenant predicates, and Identity user anonymization. It is not yet a complete commercial privacy-orchestration platform.
1. Current end-to-end path
Every [GenerateEndpoint] route uses RequireAuthorization() by default. Commands that implement IAuthorizedRequest also enter the framework authorization decision. Plain queries and RecordConsent only have the HTTP authentication boundary.
2. Current capability matrix
| Capability | Current implementation | Guarantee that does not exist |
|---|---|---|
| Grant consent | Append a SysConsent history row | No purpose registry, notice snapshot, active-consent uniqueness, or processor propagation |
| Revoke consent | Owner or role named admin sets RevokedAt | No withdrawal event or downstream acknowledgement |
| Create SAR | Query then insert by tenant and RequestId | No unique constraint; concurrency is not idempotent |
| Process SAR | Store UserId and at most 1,000 audit items | No domain collection, file, download, encryption, or expiry |
| Create erasure | Start a 30-day CoolingOff request | No identity re-proofing, approval, or legal hold |
| Execute erasure | Anonymize and soft-delete the current tenant’s SysUser row | No credentials, sessions, business owners, or external processors |
| Persistence | Provider-neutral IEntitySet<T> with dual-provider contracts | Does not prove workflow semantics |
| Activity audit | Shared pipeline records command result | Best-effort delivery, not an immutable compliance ledger |
3. HTTP use cases and actual decisions
| Method and route | Use case | Additional decision |
|---|---|---|
POST /api/privacy/consents | RecordConsent | no IAuthorizedRequest |
GET /api/privacy/consents | GetConsents | no IAuthorizedRequest |
DELETE /api/privacy/consents/{id} | RevokeConsent | gdpr.own-data.delete |
POST /api/privacy/sar | CreateSar | gdpr.own-data.create |
GET /api/privacy/sar | GetSarRequests | no IAuthorizedRequest |
POST /api/privacy/sar/{id}/process | ProcessSar | privacy.sar.update |
POST /api/privacy/erasure | CreateErasure | gdpr.own-data.create |
DELETE /api/privacy/erasure/{id} | CancelErasure | gdpr.own-data.delete |
POST /api/privacy/erasure/{id}/execute | ExecuteErasure | privacy.erasure.delete |
The decision code comes from ResourceDescriptor + AuthorizationAction. The source catalog instead declares privacy.consent.view, privacy.consent.manage, privacy.sar.manage, and privacy.erasure.manage. This mismatch can prevent roles from receiving the permission that the request actually evaluates.
4. Two state machines in one table
This is the intended model, not an enforced state machine. Both SmartEnums persist integers in StatusCode, while ID-based handlers do not check RequestType. A request passed to the wrong endpoint can have the same integer interpreted as a different workflow status.
5. Tenant data plane
GdprStore uses ICurrentTenant.Tenant.EffectiveTenantId as its final predicate. Application handlers generally pass currentUser.User.TenantId:
var actorTenant = currentUser.User.TenantId;
var rows = await store.GetConsentsByUserAsync( actorTenant, currentUser.User.UserId?.ToString() ?? "0", cancellationToken);
// The store also requires row.TenantId == EffectiveTenantId.// During operate-as, the intersection can be empty.The two values normally match for an ordinary user. Host operations, background execution, and tenant impersonation need explicit contracts and tests.
6. Persistence and composition
The module owns two tenant-scoped soft-delete tables:
SysConsentstores purpose, notice version, grant/revocation times, plus IP and User-Agent columns that current commands do not populate.SysDataSubjectRequeststores request type, status, cooling-off deadline, rejection reason,DetailsJson, and reservedExportFileId.
GdprStore depends on IEntitySet<T>, not SqlSugar or EF Core. With production persistence prerequisites, generated registration selects the configured ORM adapter. Shell mode retains an explicit unavailable port and fails closed on use.
7. Highest-priority delivery gaps
- Process handlers do not validate RequestType; status integers can cross workflow boundaries.
- RequestId lookup does not include type, and the table has no unique request index.
- ProcessSar writes Failed and then returns
Result.Failure; the transaction pipeline rolls the Failed update back. - Business code never assigns
ExportFileIdor produces a downloadable artifact. - Erasure changes only selected
SysUserfields; it does not orchestrate other data owners. - Permission catalog, runtime decisions, and feature
privacy.operationshave no closed-loop evidence. - There is no subject re-proofing, legal hold, deadline escalation, download authorization, or immutable per-step record.
8. Reading path
- Consent and evidence
- SAR and export
- Erasure workflow
- Storage, security, and authorization
- Testing and operations
Related foundations: Auditing, Identity, Multitenancy, and Authorization.
9. Minimal source review
# List all GDPR routes and handlers.rg -n "GenerateEndpoint|class Handler" src/Platform/Gdpr -g '*.cs'
# Inspect export fields and prove where business code assigns them.rg -n "ExportFileId|DetailsJson" src/Platform/Gdpr tests -g '*.cs'
# Detect the mixed gdpr/privacy authorization resources.rg -n 'new\("gdpr"|GdprPermissions\.' src/Platform/Gdpr -g '*.cs'