DSAR (Data Subject Access Requests — access / rectification / erasure / objection)
Scope
DsarService (~1 516 lines) + DsarPublicController +
DsarSlaEscalationJob. Statutory 30-day response window
enforced via SLA escalation. Erasure is dual-approved
(distinct-officer confirmation) and cascades to prefs +
sessions + refresh tokens + api-keys + passkeys +
data-transfer consents + ratings (all wired via prior audit
phases). Missing: subject-portal path, per-email throttle on
public submit, step-up re-verify before execute-erasure,
retention of DSAR artefacts themselves, objection enforcement
downstream of the record row.
src/modules/dsar/dsar.module.ts(1 516 lines).DsarPublicController@Public()POST /dsar/public/requests.DsarSlaEscalationJob— hourly sweep flips overdue requests toexpired.- Prisma models:
DsarRequest,DsarRequestFulfilment,DsarExportPackage,DsarDownloadToken,DsarObjection,DsarRecipient,DsarManualTask.
Compliance envelope
- DPPA 2019 §17(1) — access / rectification / erasure — wired end-to-end for authenticated ops-side + public unauthenticated submissions.
- DPPA 2019 §17(2) — 30-day response window enforced.
- DPPA 2019 §17(5) — recipient duty present, escalation on failure absent.
- DPPA 2019 §16 — cross-border transfer disclosure — pulled into export ✓ but not disclosed pre-export for unresolved-subject flows.
- DPPA 2019 §20 — objection right — recorded, never enforced downstream.
- DPPA 2019 §11 — DSAR artefacts retained forever.
- AML §14 — rich audit catalogue; opaque cross-ref between approve + execute erasure events.
- BOU cybersecurity §5.4 — public submit unthrottled (only nginx-level).
- NIST SP 800-63B §5.4.4 — no re-verify at execute- erasure time.
Current state (2026-07-20)
What works today
- Dual-approval on erasure (distinct-officer).
- 30-day SLA + hourly escalation.
- Export includes DSAR-linked consent + prefs + api-keys + passkeys + ratings cascade (from prior campaign phases).
DsarDownloadToken72-h TTL + single-use consume.- Timing-attack-safe email lookup on public submit.
- Recipient propagation creates manual-task rows on callback failure.
- Rich audit catalogue:
dsar.request.created / .verified / .denied / .erase.approved / .erase / .export / .rectify / .download.token_minted / .download.consumed / .objection.recorded.
Migration 20260720080000_dsar_phase3 adds
DsarRequest.rectificationBatchId + rectifiedAt.
approveErasure() looks up the just-inserted approve-audit
event id and persists it into DsarRequest.metadata.eraseApproveEventId;
executeErasure() reads it back onto the execute-audit's
metadata as approveEventId — compliance walks approve →
execute as one linked pair.
DsarSlaEscalationJob.tick() now refuses to expire rows in
in_progress; instead emits dsar.expiry.deferred_by_in_flight_execute
audit so mid-execute rows aren't clobbered.
executeRectification() mints a randomUUID() batchId +
stamps rectifiedAt on the DsarRequest + carries both into
every per-field audit event's metadata.
Client-portal DSAR self-service (/client-portal/me/dsar/*)
was surveyed + confirmed already routing with
evidenceKind='subject_portal' (gap 10 closed as-is).
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/dsar-phase3.e2e-spec.ts (2 tests, both
pass). Full suite green: 104 suites / 698 tests.
DSAR module — ALL 12 GAPS CLOSED ✅
Migration 20260720070000_dsar_phase2 adds
DsarDownloadToken.tokenHash + partial unique index.
mintDownloadToken() persists sha256(token) alongside the
plaintext; consumeDownloadToken() verifies via hash sibling
first, plaintext fallback for legacy rows.
consumeDownloadToken() now runs the export BEFORE stamping
consumedAt — a network failure after export runs no longer
leaves the audit trail claiming "downloaded" with the subject
receiving nothing.
propagateErasureToRecipients() failed-status branch fires a
second audit event dsar.recipient_propagation.escalated
alongside dsar.erase.propagation.failed so DPO can grep the
chain directly.
exportUser() emits security.dsar_cross_border_disclosure
audit when the export carries any DataTransferConsent rows —
subject can see the cross-border processing chain the pack
that follows will contain.
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/dsar-phase2.e2e-spec.ts (2 tests, both
pass). Full suite green: 104 suites / 698 tests.
Migration 20260720060000_dsar_phase1 adds
DsarRequest.retentionUntil + retentionScrubbedAt + partial
index. New policy code data_sharing.dsar_request_retention_days
(default 2555 = 7 y) drives close/deny stamping; existing
retention job scrubs subject payload past the window.
POST /dsar/public/requests gains an in-memory sliding-window
throttle (3/hour per lowercased identifier + 10/hour per IP)
with dsar.public.submit.throttled audit + 429 response.
POST /dsar/requests/:id/execute-erasure now carries
@RequireStepUp(15) — irreversible operation requires a fresh
MFA proof. New DsarObjectionEnforcer service (@Global()
module to avoid circular deps) checks DsarObjection rows
before dispatch; wired into NotificationDispatchService.dispatchByTemplateToClient
— matching objection → skip + dsar.objection.enforced audit.
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/dsar-phase1.e2e-spec.ts (2 tests, both
pass). Full suite green: 104 suites / 698 tests.
Gaps
All twelve findings closed — see the shipped-note admonitions above.