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,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);
|
||||
});
|
||||
Reference in New Issue
Block a user