Skip to content
bitzorcas
中EN

Reference

GDPR storage, tenancy, security, and authorization

Explain the provider-neutral store, tables, tenant predicates, permission mismatch, transaction behavior, and sensitive data boundaries.

Last updated

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

ProjectResponsibility
BitzOrcas.Platform.Gdpr.ContractsDTOs, requests, SAR/erasure SmartEnums
BitzOrcas.Platform.Gdpr.Applicationrouted requests, catalogs, and IGdprStore
BitzOrcas.Platform.Gdpr.Infrastructureprovider-neutral store and two owner-local records
Identity Contracts/Infrastructurepersonal-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) and UserId(36);
  • RequestType and shared StatusCode;
  • request, completion, and cooling-off times;
  • ExportFileId(36);
  • RejectionReason(500);
  • DetailsJson with 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.

Keep store extensions provider-neutral
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:

Current store tenant intersection
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:

ScenarioRuntime decisionClosest catalog value
create SAR/erasuregdpr.own-data.createnone
withdraw/cancelgdpr.own-data.deletenone
process SARprivacy.sar.updateprivacy.sar.manage
execute erasureprivacy.erasure.deleteprivacy.erasure.manage
list own recordsauthentication onlyunused 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:

  1. persist a stable ErrorCode; keep redacted diagnostics in a restricted log;
  2. version and encrypt Details payloads;
  3. keep Details, keys, and internal reasons out of list DTOs;
  4. log only RecordId and CorrelationId, never export bodies;
  5. 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.

DataTypical policy dimension
consent historyevidence period after withdrawal
DSR workflow metadataminimal processing proof
plaintext export packageshort expiry and physical deletion
failure diagnosticsredacted, restricted, short-lived
legal holdsuspend 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

Illustrative target constraints
-- Scope idempotency by tenant and workflow type.
CREATE UNIQUE INDEX UX_Dsr_Request
ON SysDataSubjectRequest (TenantId, RequestType, RequestId);
-- Support tenant work queues and deadline scans.
CREATE INDEX IX_Dsr_WorkQueue
ON SysDataSubjectRequest
(TenantId, RequestType, StatusCode, CoolingOffEndsAt);

Use an aggregate Version or dedicated concurrency token for compare-and-swap transitions.

12. Source review

Terminal window
# 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'

Back to GDPR overview

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%