Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
import type { PrismaClient, Framework } from "@prisma/client";
import { hashPassword } from "@/server/password";
import { PERMISSIONS, ROLE_DEFS } from "@/server/rbac";
import { MODULES } from "@/lib/modules";
import { reconcilePackage, stampPackageVersion } from "../../prisma/import-policies";
import { resolvePackageForTenant } from "../../prisma/template-store";
import { importManaged } from "../../prisma/import-managed";
/**
* Mappt die Mandanten-Stammdaten (TenantSettings) auf die ISMS-Template-Variablen
* des Richtlinienmoduls (§4.1) — eine Pflegestelle, keine Doppeleingabe.
*/
export async function syncPolicyVariablesFromSettings(
prisma: PrismaClient,
tenantId: string,
s: {
orgName: string; orgShort?: string | null; ismsScope?: string | null; ismsScopeDescription?: string | null;
roleManagement?: string | null; roleIsb?: string | null; roleItLead?: string | null; roleDpo?: string | null;
}
) {
const map: Record<string, string | null | undefined> = {
ORG_NAME: s.orgName,
ORG_SHORT: s.orgShort,
ISMS_SCOPE: s.ismsScope,
ISMS_SCOPE_DESCRIPTION: s.ismsScopeDescription,
ROLE_MANAGEMENT: s.roleManagement,
ROLE_ISB: s.roleIsb,
ROLE_IT_LEAD: s.roleItLead,
ROLE_DPO: s.roleDpo,
};
for (const [key, value] of Object.entries(map)) {
if (value == null || value === "") continue;
await prisma.policyVariable.updateMany({ where: { tenantId, key }, data: { value } });
}
}
export interface ProvisionOpts {
name: string;
slug: string;
short?: string;
sector?: string;
admin: { email: string; name: string; password: string };
tisaxLevel?: "AL2" | "AL3";
seedPoliciesDir?: string;
actorId?: string | null;
/**
* AP2: Rahmenwerke, die der Mandant führt (Reihenfolge = Primär zuerst). Default
* `["TISAX"]` (rückwärtskompatibel). Steuert die TenantFramework-Zeilen, welche
* Mappings importiert und welche Sichtbarkeits-Flags (FLAG_FW_*) gesetzt werden.
*/
frameworks?: Framework[];
}
/**
* Legt einen Mandanten idempotent an bzw. aktualisiert ihn: Standard-Rollen +
* Permissions, erster Admin-User, Mandanten-Einstellungen, alle Module aktiv,
* optional Richtlinienpaket-Seed inkl. TISAX-Default (§3.7 Auto-Provisioning).
*/
export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts) {
const tisaxLevel = opts.tisaxLevel ?? "AL2";
// 1. Globaler Permission-Katalog
for (const key of PERMISSIONS) {
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
}
// 2. Mandant
const tenant = await prisma.tenant.upsert({
where: { slug: opts.slug },
update: { name: opts.name, short: opts.short ?? null, sector: opts.sector ?? null },
create: { name: opts.name, slug: opts.slug, short: opts.short ?? null, sector: opts.sector ?? null },
});
// 3. Rollen + Permissions
for (const [key, def] of Object.entries(ROLE_DEFS)) {
const role = await prisma.role.upsert({
where: { tenantId_key: { tenantId: tenant.id, key } },
update: { name: def.name },
create: { tenantId: tenant.id, key, name: def.name },
});
const perms = await prisma.permission.findMany({ where: { key: { in: [...def.permissions] } } });
for (const p of perms) {
await prisma.rolePermission.upsert({
where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } },
update: {},
create: { roleId: role.id, permissionId: p.id },
});
}
}
// 4. Erster Admin-User (Mandanten-Admin + ISB)
// Option C: globale Identity (Anmeldung) + Mitgliedschaft (User) im Mandanten.
// Expand/Contract: passwordHash bleibt vorerst auch auf der Membership (Legacy),
// bis WS1 den Login gegen Identity umstellt. Idempotent: bestehende Identity/
// Passwort wird NICHT überschrieben (analog Bootstrap-Semantik).
const passwordHash = await hashPassword(opts.admin.password);
const identity = await prisma.identity.upsert({
where: { email: opts.admin.email },
update: {},
create: { email: opts.admin.email, passwordHash },
});
const admin = await prisma.user.upsert({
where: { tenantId_email: { tenantId: tenant.id, email: opts.admin.email } },
update: { name: opts.admin.name, identityId: identity.id },
create: { tenantId: tenant.id, identityId: identity.id, email: opts.admin.email, name: opts.admin.name },
});
const adminRoles = await prisma.role.findMany({ where: { tenantId: tenant.id, key: { in: ["tenant-admin", "isb"] } } });
for (const r of adminRoles) {
await prisma.userRole.upsert({
where: { userId_roleId: { userId: admin.id, roleId: r.id } },
update: {},
create: { userId: admin.id, roleId: r.id },
});
}
// 5. Mandanten-Einstellungen (Quelle der ISMS-Variablen)
await prisma.tenantSettings.upsert({
where: { tenantId: tenant.id },
update: {},
create: {
tenantId: tenant.id,
orgName: opts.name,
orgShort: opts.short ?? null,
sector: opts.sector ?? null,
roleManagement: "Geschäftsführung",
roleIsb: "Informationssicherheitsbeauftragte(r) (ISB)",
roleItLead: "IT-Leitung",
roleDpo: "Datenschutzbeauftragte(r) (DSB)",
tisaxLevel,
},
});
// 6. Alle Module aktivieren
for (const m of MODULES) {
await prisma.tenantModule.upsert({
where: { tenantId_moduleKey: { tenantId: tenant.id, moduleKey: m.key } },
update: {},
create: { tenantId: tenant.id, moduleKey: m.key, enabled: true },
});
}
// 6b. Framework-Zugehörigkeit (AP2): der Mandant führt die in `frameworks` genannten
// Rahmenwerke (Default TISAX). Erstes = Primär. Idempotent je (tenant, framework).
const frameworks: Framework[] = opts.frameworks?.length ? [...new Set(opts.frameworks)] : ["TISAX"];
const runsTisax = frameworks.includes("TISAX");
for (const [i, framework] of frameworks.entries()) {
await prisma.tenantFramework.upsert({
where: { tenantId_framework: { tenantId: tenant.id, framework } },
update: { isPrimary: i === 0 },
create: { tenantId: tenant.id, framework, isPrimary: i === 0 },
});
}
// 7. Richtlinienpaket JE FRAMEWORK importieren (falls Seed-Verzeichnis übergeben)
if (opts.seedPoliciesDir) {
// Neue Mandanten erhalten die neueste veröffentlichte DB-Vorlage (Datei-Fallback).
// Anforderungen sind framework-scoped, die geteilten Inhalte werden nur beim ersten
// Framework abgeglichen (reconcileShared, Falle 1.1).
for (const [i, framework] of frameworks.entries()) {
const { pkg } = await resolvePackageForTenant(prisma, tenant.id, opts.seedPoliciesDir, framework);
await reconcilePackage(prisma, tenant.id, pkg, { framework, reconcileShared: i === 0 });
await stampPackageVersion(prisma, tenant.id, pkg.version, framework);
}
await importManaged(prisma, tenant.id);
// AL-/Schutzbedarf-Flags sind TISAX-Konzepte → nur bei TISAX-Mandanten setzen
// (reiner ISO-Mandant: keine AL-Flags, Übergabe D2/§1.3).
if (runsTisax) {
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_HIGH_PROTECTION" }, data: { value: "true" } });
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_VERY_HIGH_PROTECTION" }, data: { value: tisaxLevel === "AL3" ? "true" : "false" } });
}
// WICHTIG (Reihenfolge, Übergabe AP2): die Framework-Sichtbarkeits-Flags NACH dem
// Import setzen. reconcilePackage legt sie beim ersten Import mit dem Schema-Default
// an (TISAX=true, ISO=false) und überschreibt bestehende Werte NIE — würden wir vorher
// setzen, bliebe der Default stehen und ein ISO-Mandant sähe die VDA-ISA-Sicht.
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_FW_TISAX" }, data: { value: runsTisax ? "true" : "false" } });
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_FW_ISO27001" }, data: { value: frameworks.includes("ISO_27001") ? "true" : "false" } });
const settings = await prisma.tenantSettings.findUnique({ where: { tenantId: tenant.id } });
if (settings) await syncPolicyVariablesFromSettings(prisma, tenant.id, settings);
}
await prisma.auditLog.create({
data: { tenantId: tenant.id, scope: "platform", actorId: opts.actorId ?? null, action: "provision", entity: "tenant", entityId: tenant.id, after: { name: opts.name, tisaxLevel, frameworks } },
});
return tenant;
}