GDPR workflow data is itself sensitive. Request status reveals that a person is exercising a right, DetailsJson can aggregate personal data from several systems, and rejection reasons can contain protected facts. Storage and authorization deserve a stronger boundary than an ordinary business list.
1. Projects and dependencies
| Project | Responsibility |
|---|---|
BitzOrcas.Platform.Gdpr.Contracts | DTOs, requests, SAR/erasure SmartEnums |
BitzOrcas.Platform.Gdpr.Application | routed requests, catalogs, and IGdprStore |
BitzOrcas.Platform.Gdpr.Infrastructure | provider-neutral store and two owner-local records |
| Identity Contracts/Infrastructure | personal-data erasure port and user-row adapter |
GdprModule has governance code privacy and declares Authorization and Identity dependencies. Infrastructure references Identity Contracts but no concrete ORM.
2. Table shapes
SysConsent
[BitzTable(... IsTenant = true, IsSoftDelete = true)] with an index on TenantId + UserId + Purpose. The index is not unique, and there is no business concurrency contract.
SysDataSubjectRequest
Another tenant soft-delete table containing:
RequestId(36)andUserId(36);RequestTypeand sharedStatusCode;- request, completion, and cooling-off times;
ExportFileId(36);RejectionReason(500);DetailsJsonwith unbounded metadata length.
Indexes cover TenantId + UserId + RequestType and StatusCode. There is no unique RequestId index or Tenant/Status/Deadline work-queue index.
3. Provider-neutral store
GdprStore depends on two IEntitySet<T> instances and the Identity eraser. Generated persistence metadata maps the owner-local records. Integration tests compare basic SqlSugar and EF Core behavior.
public sealed class GdprStore( IEntitySet<DataSubjectRequestWorkflowRecord> requests, ICurrentTenant currentTenant){ // Resolve the data plane from trusted tenant context, never the request body. public Task<DataSubjectRequestWorkflowRecord?> FindAsync( string id, CancellationToken cancellationToken) { var tenantId = currentTenant.Tenant.EffectiveTenantId;
// Keep the tenant predicate in the expression tree for both adapters. return requests.FirstOrDefaultAsync( row => row.Id == id && row.TenantId == tenantId, cancellationToken); }}Do not inject SqlSugarClient or DbContext into this module. Extend the dual-adapter contract whenever a new query shape is added.
4. Fail-closed composition
Core Runtime declares PersistenceDefaultPort(typeof(IGdprStore)). Generated persistence registration supplies GdprStore only after production prerequisites and provider selection.
Shell mode resolves an explicit unavailable proxy. That proves there is no silent in-memory fallback, not that production is ready. A readiness check should confirm:
- selected provider registration;
- both tables and required indexes;
- Identity erasure port;
- transaction and encryption services;
- object storage and notifications when export delivery is enabled.
5. Tenant intersection
List methods compare both the caller argument and Effective Tenant:
row => row.TenantId == requestedTenantId && row.TenantId == currentTenant.Tenant.EffectiveTenantId && row.UserId == subjectUserId;This blocks a caller-supplied foreign tenant. The application passes Actor Tenant, however, so operate-as produces an empty intersection. Inserts ignore the tenant in NewConsentRecord and NewDataSubjectRequestRecord and always write Effective Tenant.
A complete contract separates:
- ActorTenantId;
- EffectiveTenantId;
- SubjectUserId;
- ImpersonationGrantId.
6. Actual decision codes
RBAC builds lowercase {module}.{resource}.{action}. Current requests and catalog values differ:
| Scenario | Runtime decision | Closest catalog value |
|---|---|---|
| create SAR/erasure | gdpr.own-data.create | none |
| withdraw/cancel | gdpr.own-data.delete | none |
| process SAR | privacy.sar.update | privacy.sar.manage |
| execute erasure | privacy.erasure.delete | privacy.erasure.manage |
| list own records | authentication only | unused privacy.consent.view |
The catalog is not a deployable permission list until these values converge and HTTP authorization tests prove it.
7. Feature declaration is not enforcement
GdprFeatures.Operations = "privacy.operations" is disabled by default and collected by governance generation. GDPR requests do not reference this value or implement an explicit feature-gated contract.
The catalog proves declaration, not runtime denial. Test the same endpoint with the tenant feature on and off before relying on it for licensing or rollout.
8. Transaction boundary
GDPR write commands use TransactionPipelineBehavior:
- successful Result commits;
- failed Result rolls back;
- exception rolls back and rethrows;
- shared Activity audit attempts best-effort enqueue after the handler result.
This explains why ProcessSar cannot persist Failed while returning a failed Result. It also means Activity audit is not evidence atomically committed with the workflow.
9. Sensitive fields and logs
DetailsJson, RejectionReason, and export objects need explicit classification. ProcessSar currently stores ex.Message in RejectionReason. Audit providers claim masking through a hard-coded boolean rather than provider evidence.
Recommended controls:
- persist a stable ErrorCode; keep redacted diagnostics in a restricted log;
- version and encrypt Details payloads;
- keep Details, keys, and internal reasons out of list DTOs;
- log only RecordId and CorrelationId, never export bodies;
- authorize and audit detail/download access separately.
10. Retention
The two GDPR tables have no owner-specific retention job. Soft deletion does not physically purge rows, and Details has no expiry.
| Data | Typical policy dimension |
|---|---|
| consent history | evidence period after withdrawal |
| DSR workflow metadata | minimal processing proof |
| plaintext export package | short expiry and physical deletion |
| failure diagnostics | redacted, restricted, short-lived |
| legal hold | suspend deletion with authority and release |
Do not use the audit module’s current category-defective retention port for GDPR exports.
11. Index and concurrency target
-- Scope idempotency by tenant and workflow type.CREATE UNIQUE INDEX UX_Dsr_RequestON SysDataSubjectRequest (TenantId, RequestType, RequestId);
-- Support tenant work queues and deadline scans.CREATE INDEX IX_Dsr_WorkQueueON SysDataSubjectRequest (TenantId, RequestType, StatusCode, CoolingOffEndsAt);Use an aggregate Version or dedicated concurrency token for compare-and-swap transitions.
12. Source review
# Verify that GDPR remains independent of concrete ORMs.rg -n "SqlSugar|DbContext|EntityFrameworkCore" src/Platform/Gdpr -g '*.cs'
# Compare request resources with the permission catalog.rg -n "ResourceDescriptor|PermissionDefinition|GdprPermissions" \ src/Platform/Gdpr -g '*.cs'
# Inventory sensitive detail access.rg -n "DetailsJson|RejectionReason|ExportFileId" src/Platform/Gdpr -g '*.cs'