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
+142
View File
@@ -0,0 +1,142 @@
// Nachweis der Rechte-Begrenzung bei der Rollenverwaltung (Sicherheitsbefund F-13).
//
// F-13: Ein `role:manage`-Inhaber (z. B. tenant-admin) konnte über `createRole`/
// `updateRolePermissions` eine Rolle mit BELIEBIGEN Katalog-Rechten bauen und sie sich
// über `setUserRoles` selbst zuweisen — obwohl der tenant-admin bewusst NICHT über
// `policy:approve`/`risk:accept`/`soa:write` verfügt. Der Fix begrenzt vergebbare Rechte
// auf die eigene effektive Rechtemenge (autoritativ aus der DB) und unterbindet die
// Selbstzuweisung höher privilegierter Rollen.
//
// Dieser Test repliziert exakt die autoritative Query und die Begrenzungslogik aus
// src/server/actions/tenant-users.ts (actorEffectivePermissions / assertGrantableWithinActor /
// Selbstzuweisungs-Prüfung) und weist nach, dass die Entscheidungen korrekt kippen.
// (Ein direkter Action-Aufruf würde eine NextAuth-Session voraussetzen; die sicherheits-
// relevante Logik ist die Query + die Mengenprüfung.)
//
// Lauf: npx tsx scripts/test-tenant-users-authz.ts
import "dotenv/config";
import { prisma } from "../src/server/db";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
const SLUG = "zz-f13-authz-test";
// Repliziert actorEffectivePermissions() aus tenant-users.ts.
async function effectivePermissions(userId: string): Promise<Set<string>> {
const account = await prisma.user.findFirst({
where: { id: userId, status: "ACTIVE" },
select: {
userRoles: {
select: { role: { select: { rolePermissions: { select: { permission: { select: { key: true } } } } } } },
},
},
});
if (!account) throw new Error("Konto ist nicht aktiv.");
return new Set(account.userRoles.flatMap((ur) => ur.role.rolePermissions.map((rp) => rp.permission.key)));
}
// Repliziert assertGrantableWithinActor() aus tenant-users.ts (Verstoß = excess nicht leer).
const excessOf = (perms: string[], effective: Set<string>) => perms.filter((p) => !effective.has(p));
async function cleanup() {
const t = await prisma.tenant.findUnique({ where: { slug: SLUG } });
if (!t) return;
await prisma.user.deleteMany({ where: { tenantId: t.id } });
await prisma.role.deleteMany({ where: { tenantId: t.id } });
await prisma.tenant.delete({ where: { id: t.id } });
// Verwaiste Test-Identity(s) entfernen (Membership wurde eben gelöscht).
await prisma.identity.deleteMany({ where: { email: { endsWith: "@zz-authz.test" }, memberships: { none: {} } } });
}
async function ensurePerm(key: string) {
return prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
}
async function main() {
await cleanup();
const tenant = await prisma.tenant.create({ data: { name: "F13 AuthZ Test", slug: SLUG } });
// tenant-admin-typische Rechte (bewusst OHNE policy:approve / risk:accept / soa:write).
const adminPermKeys = ["tenant:manage", "user:read", "user:manage", "role:manage", "report:read"];
const adminPerms = await Promise.all(adminPermKeys.map(ensurePerm));
const escalationTargets = await Promise.all(["policy:approve", "risk:accept", "soa:write"].map(ensurePerm));
const adminRole = await prisma.role.create({
data: {
tenantId: tenant.id,
key: "tenant-admin",
name: "Mandanten-Admin",
rolePermissions: { create: adminPerms.map((p) => ({ permissionId: p.id })) },
},
});
// Option C: Mitgliedschaft braucht eine globale Identity (Anmeldung).
const actorIdentity = await prisma.identity.create({ data: { email: "f13-admin@zz-authz.test", passwordHash: "x" } });
const actor = await prisma.user.create({
data: {
tenantId: tenant.id, identityId: actorIdentity.id, email: "f13-admin@zz-authz.test", name: "F13 Admin",
status: "ACTIVE", userRoles: { create: [{ roleId: adminRole.id }] },
},
});
// Höher privilegierte Rolle (ISB-artig), die der Actor sich NICHT selbst geben darf.
const isbRole = await prisma.role.create({
data: {
tenantId: tenant.id, key: "isb", name: "ISB / CISO",
rolePermissions: { create: escalationTargets.map((p) => ({ permissionId: p.id })) },
},
});
const effective = await effectivePermissions(actor.id);
// (1) Effektive Menge stimmt und enthält bewusst NICHT die kritischen Rechte.
ok(effective.has("role:manage"), "(1) Actor hat role:manage");
ok(!effective.has("policy:approve") && !effective.has("risk:accept") && !effective.has("soa:write"),
"(1) Actor hat bewusst KEIN policy:approve/risk:accept/soa:write");
// (2) createRole/updateRolePermissions: Delegation ist bewusst ERLAUBT (pragmatische
// F-13-Variante) — ein Admin darf Rollen mit Rechten oberhalb seines Niveaus für ANDERE
// anlegen (z. B. ISB klonen+bearbeiten). Die Escalation-Abwehr sitzt in der Selbst-
// zuweisung (4). Hier nur festhalten, dass die kritischen Rechte tatsächlich außerhalb
// des eigenen Niveaus liegen (sonst wäre der Test bedeutungslos).
ok(excessOf(["policy:approve", "risk:accept"], effective).length === 2,
"(2) policy:approve/risk:accept liegen außerhalb des Actor-Niveaus (Delegation dennoch erlaubt)");
// (3) Die für ANDERE delegierbaren Rechte sind nicht künstlich beschnitten.
ok(excessOf(["user:read", "report:read"], effective).length === 0,
"(3) Vergabe eigener Rechte (user:read/report:read) ist ohnehin zulässig");
// (4) Selbstzuweisung der höher privilegierten ISB-Rolle → blockiert (Kern-Abwehr).
const isbPerms = new Set(
(await prisma.rolePermission.findMany({ where: { roleId: isbRole.id }, select: { permission: { select: { key: true } } } }))
.map((r) => r.permission.key),
);
const selfAssignExcess = [...isbPerms].filter((p) => !effective.has(p));
ok(selfAssignExcess.length > 0,
"(4) Selbstzuweisung der ISB-Rolle bringt Rechte oberhalb des eigenen Niveaus → blockiert");
// (5) Selbstzuweisung der eigenen (bereits gehaltenen) Admin-Rolle → erlaubt (⊆ effektiv).
const adminRolePerms = new Set(
(await prisma.rolePermission.findMany({ where: { roleId: adminRole.id }, select: { permission: { select: { key: true } } } }))
.map((r) => r.permission.key),
);
ok([...adminRolePerms].filter((p) => !effective.has(p)).length === 0,
"(5) Selbstzuweisung der eigenen Admin-Rolle bleibt zulässig (keine Eskalation)");
await cleanup();
if (failures === 0) console.log("\nOK — alle F-13-Nachweise (1)-(5) erfüllt.");
else console.log(`\n${failures} FEHLER.`);
process.exit(failures === 0 ? 0 : 1);
}
main().catch(async (e) => {
console.error(e);
await cleanup().catch(() => {});
process.exit(1);
});