Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
5.7 KiB
TypeScript
119 lines
5.7 KiB
TypeScript
// 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<string>();
|
|
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);
|
|
});
|