// 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> { 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) => 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); });