Files
HiveGrid-Wireframes/hivegrid-cloud/docs/features/dsgvo-bulk-ops.md
T

173 lines
7.5 KiB
Markdown

# DSGVO Bulk-Ops (Self-Service Export, Deletion, Pseudonymisation)
**Phase:** 10 — Compliance Polish
**Status:** active
**Detail-MD:** `docs/improvement-plan/10-compliance-polish/dsgvo-bulk-ops.md`
This document describes the runtime architecture of the DSGVO bulk-ops
pipeline that delivers the user-facing rights under GDPR Art. 15, 17
and 20.
## Schema
The `User` model carries six DSGVO flags (`prisma/schema/accounts.prisma`):
| Column | Purpose |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `pendingDeletion` | True while the 30-day grace period is active; login is blocked. |
| `scheduledDeletionAt` | Deadline for the grace-period cron to trigger the cascade. |
| `deletionRequestedAt` | Timestamp the user (or admin) first hit the delete button. |
| `pseudonymized` | True after the cascade has rewritten PII to deterministic pseudonyms. |
| `pseudonymizedAt` | Timestamp of the pseudonymisation event. |
| `deletionRequestId` | Idempotency token shared with the worker pipeline and the AuditEvent metadata. |
The composite index `(pendingDeletion, scheduledDeletionAt)` keeps the
grace-period cron query O(log n).
## User-FK Map
`packages/db/src/dsgvo/user-fk-map.ts` enumerates every model that
holds a foreign key (or logical reference) to `User.id`, paired with an
explicit cascade strategy:
| Strategy | Meaning |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `hard-delete` | Row is deleted when the user is deleted. Used for join tables, ephemeral data, RBAC bindings. |
| `pseudonymize` | FK column is rewritten to the deterministic pseudonym hash. Used for audit-bound rows. |
| `retain-tombstone` | FK column is set to NULL (schema-level `SetNull`). Row outlives the user without a reference. |
`AuditEvent.actorId` is always `pseudonymize`, never `hard-delete`,
even when the user opted for deletion. SOC2-CC6.1 requires the audit
trail to survive a user-deletion event.
A regression test reads `prisma/schema/*.prisma` and asserts the map
is complete; future migrations that add a new `User`-FK without
updating the map fail CI before the cascade silently leaks PII.
## Pseudonymisation
`packages/db/src/dsgvo/pseudonymize.ts` produces a deterministic
identity set derived from SHA-256(userId | pepper):
```ts
pseudonymize("u1") === pseudonymize("u1") // true
// → { pseudoId, pseudoEmail: "deleted-user-<hash>@pseudonymized.invalid", ... }
```
Determinism keeps grouped reports coherent without exposing PII. No
external secret is mixed in by default — re-identification from the
pseudonym alone is intentionally impossible (Art. 4(5) DSGVO).
`anonymizeIp(ip)` strips the last IPv4 octet (/24) or the IPv6
interface identifier (/48) for `AuditEvent.actorIp` rewrites.
## Worker Pipeline
Three worker jobs (`apps/worker/src/jobs/dsgvo-*.ts`) execute the
heavy lifting:
### 1. `dsgvo-export.ts`
Streams a JSON bundle of every USER_FK_MAP table that opts into export
into the S3 multipart sink (`@toolctrl/exports#createS3MultipartUploadSink`).
Bundle layout:
```jsonc
{
"bundleVersion": "1.0",
"exportedAt": "2026-05-17T18:21:00Z",
"requestId": "uuid",
"user": { /* canonical User row */ },
"sections": {
"Log.userId": [ /* rows */ ],
"TenantUser.userId": [ /* rows */ ],
"...": []
}
}
```
After completion a presigned URL is minted (7-day validity), emailed
to the user and audited as `EXPORT_PERSONAL_DATA` with
`regulatoryScope=["gdpr"]`, `dataSensitivity="pii"`.
### 2. `dsgvo-deletion.ts`
Iterates USER_FK_MAP and applies the entry's declared strategy:
- `hard-delete` → `deleteMany({ where: { <column>: userId } })`.
- `pseudonymize` → `updateMany(..., { <column>: pseudonym.pseudoId })`.
- `retain-tombstone` → `updateMany(..., { <column>: null })`.
The canonical `User` row is then either hard-deleted (mode=`delete`)
or rewritten via `buildUserPseudonymizationUpdate` (mode=`pseudonymize`).
Audited as `DELETE_USER` / `PSEUDONYMIZE_USER`.
### 3. `dsgvo-verify.ts`
Runs ~1h after the cascade. Queries every `hard-delete` entry in
USER_FK_MAP for stragglers. Clean run → `outcome=success` AuditEvent.
Stragglers → `outcome=failure` AuditEvent + escalation hook.
## UI
| Route | Audience |
| ----------------------------------------------------------- | ------------------- |
| `/app/<tenant>/settings/privacy` | Self-service (user) |
| `/admin/accounts/users/<id>/privacy` | Platform admin |
Both routes share the four intents (`export`, `delete`, `pseudonymize`
[admin-only], `reactivate`). Every intent writes an AuditEvent with
`regulatoryScope=["gdpr"]`, `dataSensitivity="pii"` and either
`actorType="user"` or `actorType="admin"`.
Admin actions additionally require a `reason` text (10+ chars) which
is captured in `AuditEvent.metadata.reason` for compliance evidence.
## Login Guard
`AuthService.loginFromRequest` rejects logins for:
- `pendingDeletion = true` users with HTTP 423 Locked.
- `pseudonymized = true` users with HTTP 410 Gone.
Both surface i18n message keys (`auth.errors.accountPendingDeletion`,
`auth.errors.accountPseudonymized`) which the UI can render as a
"reactivate?" screen.
## Environment Variables
| Variable | Default | Purpose |
| ---------------------------------------- | -------------------------------- | ---------------------------------------------------------- |
| `DSGVO_GRACE_PERIOD_DAYS` | `30` | Days between deletion request and cascade execution. |
| `DSGVO_EXPORT_BUCKET` | (operator-defined) | S3 bucket for personal-data export bundles. |
| `DSGVO_EXPORT_SIGNED_URL_VALIDITY_HOURS` | `168` (7 days) | Presigned URL TTL for export downloads. |
## Retention Semantics
- **Hard-deleted rows are gone.** No backup-restore happens automatically.
- **Pseudonymised rows survive indefinitely** with no recoverable PII —
re-identification is only possible via the encrypted PseudonymizationMap
(out of scope for this initial implementation; reserved for a future
task).
- **AuditEvents always survive** with `actorId` replaced by the pseudonym
hash. The hash-chain (`packages/db/src/audit-hash.ts`) stays intact.
## Escalation Procedures
When the verifier worker reports stragglers, the operator should:
1. Inspect the failure AuditEvent (`action=VERIFY_USER_DELETION`,
`outcome=failure`) to find the offending models.
2. Manually delete via `pnpm dsgvo:reveal` (TBD) or Prisma Studio.
3. Re-run `dsgvo-verify` and confirm `outcome=success`.
4. If the cascade itself failed, restore from backup before the
deletion request and retry.
## See also
- `docs/improvement-plan/10-compliance-polish/dsgvo-bulk-ops.md` —
full task spec and acceptance criteria.
- `docs/features/compliance-audit.md` — companion #67 audit-export.
- `docs/compliance/gdpr.md` — policy-level overview.