// DSGVO-Grundgerüst: Auskunft (Art. 15/20), Löschung/Anonymisierung (Art. 17) // und der restore-feste Tombstone (KONZEPT §6). Arbeitet auf einem WEGWERF- // Mandanten (Slug zz-dsgvo-*) — demo/demo2 bleiben unberührt. // // Lauf: npx tsx scripts/test-backup-dsgvo.ts import "dotenv/config"; import { prisma } from "../src/server/db"; import { exportTenant } from "../src/server/backup/export"; import { restoreTenant } from "../src/server/backup/restore"; import { exportSubject } from "../src/server/dsgvo/export"; import { deleteSubject } from "../src/server/dsgvo/deletion"; let failures = 0; const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; const SLUG = "zz-dsgvo-test"; const EMAIL = "zz-dsgvo-person@example.test"; async function cleanup() { const t = await prisma.tenant.findUnique({ where: { slug: SLUG }, select: { id: true } }); const identityIds = new Set(); if (t) { for (const u of await prisma.user.findMany({ where: { tenantId: t.id }, select: { identityId: true } })) { identityIds.add(u.identityId); } await prisma.tombstoneEntry.deleteMany({ where: { tenantId: t.id } }); await prisma.deletionCertificate.deleteMany({ where: { tenantId: t.id } }); await prisma.auditLog.deleteMany({ where: { tenantId: t.id } }); await prisma.asset.deleteMany({ where: { tenantId: t.id } }); await prisma.userRole.deleteMany({ where: { user: { tenantId: t.id } } }); await prisma.user.deleteMany({ where: { tenantId: t.id } }); await prisma.role.deleteMany({ where: { tenantId: t.id } }); await prisma.tenant.delete({ where: { id: t.id } }); } // Identity per E-Mail (frischer Lauf) UND per gesammelter Id (ggf. schon anonymisiert). const byEmail = await prisma.identity.findUnique({ where: { email: EMAIL }, select: { id: true } }); if (byEmail) identityIds.add(byEmail.id); for (const id of identityIds) { if ((await prisma.user.count({ where: { identityId: id } })) === 0) { await prisma.identity.delete({ where: { id } }).catch(() => {}); } } } async function main() { await cleanup(); // Fixture: Mandant + Identity + Mitgliedschaft + Rolle + Asset + AuditLog. const tenant = await prisma.tenant.create({ data: { name: "DSGVO Test", slug: SLUG } }); const identity = await prisma.identity.create({ data: { email: EMAIL, passwordHash: "x", status: "ACTIVE" }, }); const user = await prisma.user.create({ data: { tenantId: tenant.id, identityId: identity.id, email: EMAIL, name: "Test Person" }, }); const role = await prisma.role.create({ data: { tenantId: tenant.id, key: "member", name: "Member" } }); await prisma.userRole.create({ data: { userId: user.id, roleId: role.id } }); await prisma.asset.create({ data: { tenantId: tenant.id, name: "Server der Person", type: "SYSTEM", ownerId: user.id, createdBy: user.id }, }); await prisma.auditLog.create({ data: { tenantId: tenant.id, actorId: user.id, action: "create", entity: "asset" }, }); // 1. Auskunft (Art. 15/20). const subj = await exportSubject(tenant.id, identity.id); ok(subj.identity?.email === EMAIL, "Auskunft enthält Identity-Metadaten (E-Mail)"); ok(!("passwordHash" in (subj.identity ?? {})), "Auskunft enthält KEINE Secrets (passwordHash)"); ok(subj.memberships.length === 1, "Auskunft enthält die Mitgliedschaft"); ok(!!subj.references["Asset.ownerId"], "Auskunft findet Asset über ownerId"); ok(!!subj.references["AuditLog.actorId"], "Auskunft findet AuditLog über actorId"); // Snapshot MIT Original-PII (für den Tombstone-on-Restore-Beweis). const snap = await exportTenant(tenant.id, { persist: false, reason: "dsgvo-test" }); // 2. Löschung/Anonymisierung (Art. 17). const del = await deleteSubject(tenant.id, identity.id, { reason: "test" }); ok(del.anonymizedMemberships === 1, "Löschung anonymisiert die Mitgliedschaft"); ok(del.identityAnonymized, "Globale Identity anonymisiert (keine Rest-Mitgliedschaft)"); const userAfter = await prisma.user.findUnique({ where: { id: user.id } }); ok(userAfter?.name === "Gelöschte Person" && userAfter?.email !== EMAIL, "User-Zeile ist anonymisiert (Name/E-Mail Tombstone)"); const idnAfter = await prisma.identity.findUnique({ where: { id: identity.id } }); ok(idnAfter?.email !== EMAIL && idnAfter?.status === "DISABLED", "Identity anonymisiert + DISABLED"); const cert = await prisma.deletionCertificate.findUnique({ where: { id: del.certificateId } }); ok(cert?.scope === "person", "Löschnachweis (DeletionCertificate) erstellt"); const tomb = await prisma.tombstoneEntry.findFirst({ where: { tenantId: tenant.id, model: "User", targetValue: user.id } }); ok(tomb?.action === "anonymize", "Tombstone-Eintrag gesetzt (restore-fest)"); // 3. BEWEIS Tombstone-on-Restore: alter Snapshot bringt PII NICHT zurück. const res = await restoreTenant(tenant.id, { artifact: snap.artifact, manifest: snap.manifest }, { skipPreRestore: true }); ok(res.tombstonesReapplied >= 1, `Restore wendet Tombstones erneut an (${res.tombstonesReapplied})`); const userReborn = await prisma.user.findUnique({ where: { id: user.id } }); ok(userReborn != null, "User-Zeile nach Restore wieder vorhanden (referenzielle Struktur)"); ok(userReborn?.name === "Gelöschte Person" && userReborn?.email !== EMAIL, "PII bleibt trotz altem Snapshot getilgt (Tombstone erneut angewandt)"); } main() .then(async () => { await cleanup(); await prisma.$disconnect(); if (failures > 0) { console.error(`\n✗ ${failures} Testfall/-fälle fehlgeschlagen.`); process.exit(1); } console.log("\nOK"); }) .catch(async (e) => { console.error(e); await cleanup().catch(() => {}); await prisma.$disconnect(); process.exit(1); });