Die drei neuen Wartungsskripte bauten sich einen eigenen PrismaClient und gingen damit an src/lib/prisma.ts vorbei. Dort wird DATABASE_URL aus den DB_*-Teilen zusammengesetzt, falls sie fehlt - und genau das ist bei `docker compose exec <dienst> npx tsx prisma/<skript>.ts` der Fall: Der Entrypoint exportiert sie nur in den Serverprozess, eine neue Shell erbt sie nicht. Ergebnis war "Environment variable not found: DATABASE_URL". Der Kommentar in src/lib/prisma.ts beschreibt genau diesen Fall; die Loesung war da, ich habe sie nur umgangen. Betroffen war auch rolle-zuweisen.ts. Das ist der Weg, den die Startwache im Klartext nennt, wenn kein Konto mehr die Pflichtrechte haelt - er haette in dem Moment versagt, fuer den er gebaut ist. Geprueft mit entzogener DATABASE_URL und nur DB_*-Teilen in der Umgebung: rechte-report, rolle-zuweisen --liste und sync-roles laufen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
5.4 KiB
TypeScript
147 lines
5.4 KiB
TypeScript
/**
|
||
* Weist einem Konto eine Rolle zu oder nimmt sie ihm weg - von der
|
||
* Kommandozeile aus.
|
||
*
|
||
* npx tsx prisma/rolle-zuweisen.ts <email> <Rollenname>
|
||
* npx tsx prisma/rolle-zuweisen.ts <email> <Rollenname> --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 <dienst> npx tsx prisma/<skript>.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<void> {
|
||
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 <email> <Rollenname> [--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 <email> <Rollenname> [--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();
|
||
});
|