/** * Weist einem Konto eine Rolle zu oder nimmt sie ihm weg - von der * Kommandozeile aus. * * npx tsx prisma/rolle-zuweisen.ts * npx tsx prisma/rolle-zuweisen.ts --entfernen * npx tsx prisma/rolle-zuweisen.ts --liste * * Im Container: * docker compose exec backend npx tsx prisma/rolle-zuweisen.ts \ * name@firma.de Developer * * WOZU: Die Teilmengenregel verhindert, dass jemand ueber die Weboberflaeche * Rechte vergibt, die er selbst nicht haelt. Das ist gewollt - es schliesst * die Selbst-Erhoehung. Es hat aber eine Kehrseite: Nach einer * Neuinstallation haelt niemand `developer:access` oder `audit:admin`, und * dann kann diese Rechte auch niemand erstmalig vergeben. * * Dieses Skript ist der dokumentierte Ausweg. Es umgeht die Regel bewusst, * denn die Vertrauensgrenze stimmt: Wer eine Shell auf dieser Maschine hat, * hat ohnehin Zugriff auf die Datenbank. Ein gestohlener Web-Zugang hat das * nicht - und genau das ist der Unterschied, den die Regel schuetzen soll. * * Der Vorgang wird protokolliert und meldet die Traeger ab, damit die * Aenderung sofort wirkt und nicht spurlos bleibt. */ // Ueber src/lib/prisma.js, NICHT ueber einen eigenen PrismaClient: Dort wird // DATABASE_URL aus den DB_*-Teilen zusammengesetzt, falls sie fehlt. Bei // `docker compose exec npx tsx prisma/.ts` ist genau das der // Fall - der Entrypoint exportiert sie nur in den Serverprozess, eine neue // Shell erbt sie nicht. Ein eigener Client scheitert dort mit // "Environment variable not found: DATABASE_URL". import prisma from '../src/lib/prisma.js'; import { createAuditLog } from '../src/services/audit.service.js'; async function main(): Promise { const args = process.argv.slice(2); if (args.includes('--liste') || args.length === 0) { const rollen = await prisma.role.findMany({ orderBy: { name: 'asc' }, include: { _count: { select: { users: true } } }, }); console.log('Vorhandene Rollen:'); for (const r of rollen) { const kennz = [r.isSystem ? 'System' : null, r.isHidden ? 'versteckt' : null] .filter(Boolean) .join(', '); console.log(` ${r.name}${kennz ? ` (${kennz})` : ''} – ${r._count.users} Konten`); } if (args.length === 0) { console.log('\nAufruf: npx tsx prisma/rolle-zuweisen.ts [--entfernen]'); } return; } const entfernen = args.includes('--entfernen'); const [email, rollenName] = args.filter((a) => !a.startsWith('--')); if (!email || !rollenName) { console.error('Aufruf: npx tsx prisma/rolle-zuweisen.ts [--entfernen]'); process.exit(1); } const konto = await prisma.user.findUnique({ where: { email } }); if (!konto) { console.error(`Kein Konto mit der Adresse "${email}".`); process.exit(1); } const rolle = await prisma.role.findUnique({ where: { name: rollenName } }); if (!rolle) { console.error(`Keine Rolle mit dem Namen "${rollenName}". Vorhandene mit --liste ansehen.`); process.exit(1); } const vorhanden = await prisma.userRole.findUnique({ where: { userId_roleId: { userId: konto.id, roleId: rolle.id } }, }); if (entfernen) { if (!vorhanden) { console.log(`"${email}" hat die Rolle "${rollenName}" gar nicht. Nichts zu tun.`); return; } await prisma.userRole.delete({ where: { userId_roleId: { userId: konto.id, roleId: rolle.id } }, }); } else { if (vorhanden) { console.log(`"${email}" hat die Rolle "${rollenName}" bereits. Nichts zu tun.`); return; } await prisma.userRole.create({ data: { userId: konto.id, roleId: rolle.id } }); } // Sofort wirksam machen: Die Rechte stehen im Zugangstoken, sonst behielte // das Konto bis zu 15 Minuten den alten Stand. await prisma.user.update({ where: { id: konto.id }, data: { tokenInvalidatedAt: new Date() }, }); // Nachvollziehbar machen. Ein Eingriff von der Kommandozeile ist berechtigt, // aber er darf nicht unsichtbar sein - sonst waere das Skript selbst die // Luecke, die es schliessen soll. // // Ueber createAuditLog, NICHT ueber prisma.auditLog.create: Das Protokoll // ist eine Hash-Kette. Ein roh eingefuegter Datensatz haette kein `hash` // und keinen `previousHash` und wuerde bei der naechsten Pruefung als // Luecke erscheinen - das Skript wuerde also ausgerechnet dort Zweifel // saeen, wo es Klarheit schaffen soll. await createAuditLog({ userEmail: 'system (CLI)', userRole: 'Kommandozeile', action: entfernen ? 'DELETE' : 'CREATE', sensitivity: 'CRITICAL', resourceType: 'UserRole', resourceId: `${konto.id}:${rolle.id}`, resourceLabel: entfernen ? `Rolle "${rolle.name}" von ${email} entfernt (Kommandozeile)` : `Rolle "${rolle.name}" an ${email} vergeben (Kommandozeile)`, endpoint: 'prisma/rolle-zuweisen.ts', httpMethod: 'CLI', ipAddress: 'lokal', changesAfter: { konto: email, rolle: rolle.name, entfernt: entfernen }, }); console.log( entfernen ? `Rolle "${rolle.name}" von "${email}" entfernt.` : `Rolle "${rolle.name}" an "${email}" vergeben.`, ); console.log('Das Konto muss sich neu anmelden, damit die Änderung greift.'); } main() .catch((e) => { console.error('[rolle-zuweisen] Fehler:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });