Skip to content
bitzorcas
中EN

Guide

GDPR subject access and export

Follow SAR creation, processing, audit collection, transaction failure, delivery gaps, and a production contributor protocol.

Last updated

A subject access request should explain which personal data the platform holds, where it came from, why it is processed, who receives it, and how the subject can obtain it safely. The current implementation is a minimal demonstration path, not a complete export-delivery service.

1. Routes and statuses

RouteCallerBehavior
POST /api/privacy/sarauthenticated + gdpr.own-data.createcreate Submitted by RequestId
GET /api/privacy/sarauthenticatedlist the current user’s SARs
POST /api/privacy/sar/{id}/processprivacy.sar.updatereject or process synchronously

GdprSarStatus defines Submitted(0), Processing(1), Completed(2), Failed(3), and Rejected(4).

2. Creation and idempotency

CreateSar checks only that RequestId is non-empty. It queries by TenantId + RequestId and returns the existing record or inserts a new row.

Create a SAR from a client
var idempotencyKey = Guid.NewGuid().ToString("N");
using var response = await http.PostAsJsonAsync(
"/api/privacy/sar",
new
{
// Reuse this value for network retries.
requestId = idempotencyKey
},
cancellationToken);
var summary = await response.Content
.ReadFromJsonAsync<DataSubjectRequestSummary>(cancellationToken);
// A database uniqueness constraint remains the final concurrency guard.

There is no unique index on TenantId + RequestId, so concurrent requests can both pass the read and insert. Lookup also omits RequestType. A RequestId already used for erasure can be returned from CreateSar.

3. Processing path

SysDataSubjectRequestIAuditQueryPortIGdprStoreProcessSar.HandlerPrivacy operatorSysDataSubjectRequestIAuditQueryPortIGdprStoreProcessSar.HandlerPrivacy operatoralt[rejection requested][process]id + optional rejection reasonGetDsrById(id)Rejected + reason + completion timeProcessingtenant + user + page 1 + size 1000AuditPageserialize user id and audit itemsDetailsJson + Completed

This is a synchronous HTTP command under the StandardCommand timeout. There is no queue, background executor, deadline scan, checkpoint, or progress endpoint.

4. Current export content

SarExportData contains:

  • UserId;
  • ExportedAt;
  • Profile, which is only a serialized object containing UserId;
  • AuditRecords, which serializes AuditPage Items into another string.

The query is fixed at PageIndex: 1, PageSize: 1000. It never follows later pages. Audit providers also have current cross-category paging and mapping differences; see Auditing storage and query.

DetailsJson is an outer object containing two JSON strings, not a versioned manifest. Consumers need nested deserialization.

5. No delivery artifact exists

The model exposes ExportFileId, but business source never assigns it. There is no:

  • IFileStorage or Files package write;
  • download endpoint or one-time ticket;
  • encryption, key wrapping, checksum, or malware scan;
  • package expiry and physical deletion;
  • completion notification;
  • detail endpoint that returns DetailsJson to the subject.

Completed currently means that JSON was written to the workflow row. It does not mean that the subject received an accessible copy.

6. Failed status and transaction rollback

ProcessSar catches non-cancellation exceptions, sets Failed, writes RejectionReason = ex.Message, then returns Result.Failure. The transaction pipeline rolls back any command that returns a failed Result.

Audit query or serialization fails

In-memory row becomes Failed
raw exception message assigned

Return Result.Failure

TransactionPipeline rolls back

Database keeps pre-processing state

The raw exception message can also disclose infrastructure or data details. A durable, redacted failure state needs a transaction design that can commit workflow evidence while returning a safe API error.

7. Request-type confusion

GetDsrByIdAsync filters only ID and tenant. ProcessSar does not require RequestType == SarRequestType. Erasure CoolingOff has value 1 and is interpreted as SAR Processing, after which the handler can complete it as a SAR.

Require type and transition before collection
if (request.RequestType != GdprRequestType.Sar)
{
// Return a stable error without exposing another workflow's data.
return Result.Failure<SarSummary>(
GdprErrors.RequestTypeMismatch);
}
var transition = SarTransitions.TryStart(request.Status);
// The guard also checks the source status and concurrency version.
if (transition.IsFailure)
return Result.Failure<SarSummary>(transition.Error);

These are target types. The repository does not currently have GdprRequestType or a central transition guard.

8. Production contributor protocol

Each data owner should contribute through a narrow contract instead of allowing GDPR to query its database directly.

Target resumable export contributor
public interface IDataSubjectExportContributor
{
// OwnerCode is stable across manifests, metrics, and idempotency keys.
string OwnerCode { get; }
int SchemaVersion { get; }
// The owner interprets Cursor; orchestration persists and returns it unchanged.
Task<ExportContribution> CollectAsync(
VerifiedSubject subject,
ExportCursor? cursor,
CancellationToken cancellationToken);
}
public sealed record ExportContribution(
IReadOnlyList<ExportArtifact> Artifacts,
ExportCursor? NextCursor,
string ContentHash,
bool Completed,
IReadOnlyList<RetentionNotice> OmittedData);

The protocol needs stable cursors, bounds, schema versions, hashes, lawful omission notices, and repeatable execution. A contributor returns only data owned by its module.

Subject re-proofing

Create job + deadline

Collect per owner
cursor + checkpoint

Versioned manifest

Encrypt package + hash

Private Files object

One-time download ticket

Expiry deletion + evidence

The manifest should list every frozen owner, schema version, artifact hash, lawfully omitted data, creation time, and completion outcome.

10. Download security

Production delivery needs at least:

  • recent identity re-proofing or step-up MFA;
  • a ticket bound to tenant, subject, file, expiry, and one-time consumption;
  • a private object with no permanent URL in logs or notifications;
  • safe Content-Type, Content-Disposition, and no-store headers;
  • sensitive-data and malware scanning;
  • durable evidence for download and destruction.

11. Tests and source review

Current tests cover the SAR list handler and basic store parity, not Create/Process, paging, files, or failure transactions.

Terminal window
# Prove that SAR reads only the first audit page.
rg -n "PageIndex: 1|PageSize: 1000" src/Platform/Gdpr -g '*.cs'
# Prove that there is no file write or download endpoint.
rg -n "IFileStorage|ExportFileId =|download|Download" src/Platform/Gdpr -g '*.cs'
# Find request-type guards in handlers; current processing paths have none.
rg -n "RequestType.*SarRequestType" src/Platform/Gdpr/BitzOrcas.Platform.Gdpr.Application/Commands

New coverage must include concurrent idempotency, cross-type RequestIds, wrong-type IDs, more than 1,000 records, provider paging, durable failure, package integrity, ticket replay, and expiry deletion.

Back to GDPR overview

100%

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