Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user