Merge branch 'phase/032-nacharbeiten'
This commit is contained in:
@@ -6,7 +6,12 @@ import { akteurVon } from "@/server/actions/formular";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { BestaetigungFehler, RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { FachFehler } from "@/server/services/fachfehler";
|
||||
import { rotationAltWiderrufen, rotationStarten, rotationUebernehmen } from "@/server/services/rotation";
|
||||
import {
|
||||
rotationAbbrechen,
|
||||
rotationAltWiderrufen,
|
||||
rotationStarten,
|
||||
rotationUebernehmen,
|
||||
} from "@/server/services/rotation";
|
||||
import { VerwendungsFehler } from "@/server/services/verwendungen";
|
||||
|
||||
/**
|
||||
@@ -69,3 +74,22 @@ export async function rotationAltWiderrufenAktion(schluesselId: string): Promise
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Laufende Rotation abbrechen (#032): merkt den neuen Schluessel zum Widerruf vor. Gleiche Rechte
|
||||
* wie die Rotation selbst; Private Keys entfernt nur, wer sie ausrollen darf (Service).
|
||||
*/
|
||||
export async function rotationAbbrechenAktion(schluesselId: string): Promise<RotationsAntwort> {
|
||||
try {
|
||||
const { benutzer } = await berechtigt();
|
||||
const altId = id.parse(schluesselId);
|
||||
await rotationAbbrechen(altId, akteurVon(benutzer), {
|
||||
privatErlaubt: darf(benutzer.rolle, "privateKeyAusrollen"),
|
||||
});
|
||||
revalidatePath(`/schluessel/${altId}`);
|
||||
revalidatePath(`/schluessel/${altId}/rotieren`);
|
||||
return {};
|
||||
} catch (fehler) {
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,9 @@ export const SCHLUESSEL_DETAIL_SELECT = {
|
||||
person: { select: { id: true, name: true } },
|
||||
vorlage: { select: { id: true, name: true } },
|
||||
erstelltVon: { select: { name: true } },
|
||||
vorgaenger: { select: { id: true, name: true, status: true } },
|
||||
nachfolger: { select: { id: true, name: true, status: true } },
|
||||
vorgaenger: { select: { id: true, name: true, status: true, widerrufEingeleitetAm: true } },
|
||||
// Ein Nachfolger im Widerruf ist eine abgebrochene Rotation (#032).
|
||||
nachfolger: { select: { id: true, name: true, status: true, widerrufEingeleitetAm: true } },
|
||||
publicVerwendungen: {
|
||||
where: { zustand: { not: "ENTFERNT" } },
|
||||
orderBy: { angelegtAm: "asc" },
|
||||
|
||||
@@ -45,7 +45,7 @@ const ALT_SELECT = {
|
||||
vorlageId: true,
|
||||
masterKeyVersion: true,
|
||||
widerrufEingeleitetAm: true,
|
||||
nachfolger: { select: { id: true, name: true, status: true } },
|
||||
nachfolger: { select: { id: true, name: true, status: true, widerrufEingeleitetAm: true } },
|
||||
_count: { select: { privateVerwendungen: { where: { zustand: { in: [...AKTIVE] } } } } },
|
||||
} satisfies Prisma.SchluesselSelect;
|
||||
|
||||
@@ -92,6 +92,13 @@ function gesperrtPruefen(alt: Alt) {
|
||||
export async function rotationStarten(altId: string, akteur: Akteur): Promise<string> {
|
||||
const alt = await altLaden(altId);
|
||||
gesperrtPruefen(alt);
|
||||
if (alt.nachfolger && rotationAbgebrochen(alt.nachfolger)) {
|
||||
// vorgaengerId ist eindeutig und bleibt fuer die Historie stehen (#032): ein zweiter
|
||||
// Nachfolger passt nicht in die Kette.
|
||||
throw new RotationsFehler(
|
||||
"Die Rotation dieses Schlüssels wurde abgebrochen; ein zweiter Nachfolger ist nicht vorgesehen. Bitte einen neuen Schlüssel anlegen, genauso zuweisen und diesen danach widerrufen.",
|
||||
);
|
||||
}
|
||||
if (alt.nachfolger) throw new RotationsFehler("Für diesen Schlüssel gibt es schon einen Nachfolger.");
|
||||
const name = rotationsName(alt.name, rotationsDatum().anzeige, NAME_MAX);
|
||||
const slug = await eindeutigerSlug(null, name);
|
||||
@@ -111,8 +118,19 @@ export async function rotationStarten(altId: string, akteur: Akteur): Promise<st
|
||||
return vorgangEinreihen({ art: "SCHLUESSEL_ERZEUGEN", angefordertVonId: akteur.id, eingabe });
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Nachfolger im Widerruf (oder widerrufen) heisst: die Rotation wurde abgebrochen (#032).
|
||||
* Ein eigenes Feld braucht es dafuer nicht – der Widerruf des Nachfolgers ist der Abbruch.
|
||||
*/
|
||||
export function rotationAbgebrochen(
|
||||
neu: { readonly status: string; readonly widerrufEingeleitetAm: Date | null } | null,
|
||||
): boolean {
|
||||
return neu !== null && (neu.status === "WIDERRUFEN" || neu.widerrufEingeleitetAm !== null);
|
||||
}
|
||||
|
||||
async function nachfolgerVon(alt: Alt) {
|
||||
if (!alt.nachfolger) throw new RotationsFehler("Erst den neuen Schlüssel erzeugen.");
|
||||
if (rotationAbgebrochen(alt.nachfolger)) throw new RotationsFehler("Die Rotation wurde abgebrochen.");
|
||||
if (alt.nachfolger.status !== "AKTIV") throw new RotationsFehler("Der neue Schlüssel ist nicht aktiv.");
|
||||
return alt.nachfolger;
|
||||
}
|
||||
@@ -255,7 +273,8 @@ export async function rotationsStand(altId: string) {
|
||||
const alt = await altLaden(altId);
|
||||
const gesperrt = rotationGesperrt({ ...alt, privatZuweisungen: alt._count.privateVerwendungen });
|
||||
const neu = alt.nachfolger;
|
||||
if (!neu) return { alt, gesperrt, neu: null, schritt: 0 as const };
|
||||
if (!neu) return { alt, gesperrt, neu: null, schritt: 0 as const, abgebrochen: false };
|
||||
const abgebrochen = rotationAbgebrochen(neu);
|
||||
|
||||
const [altPublic, altPrivat, neuPublic, neuPrivat, tests] = await Promise.all([
|
||||
prisma.publicVerwendung.count({ where: { schluesselId: alt.id, zustand: { in: [...AKTIVE] } } }),
|
||||
@@ -287,7 +306,7 @@ export async function rotationsStand(altId: string) {
|
||||
: !testsGruen
|
||||
? (3 as const)
|
||||
: (4 as const);
|
||||
return { alt, gesperrt, neu, schritt, neuOffen, tests, testsGruen };
|
||||
return { alt, gesperrt, neu, schritt, neuOffen, tests, testsGruen, abgebrochen };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,6 +321,7 @@ export async function rotationAltWiderrufen(
|
||||
const stand = await rotationsStand(altId);
|
||||
if (!stand.neu) throw new RotationsFehler("Erst den neuen Schlüssel erzeugen.");
|
||||
if (stand.schritt === 5) return;
|
||||
if (stand.abgebrochen) throw new RotationsFehler("Die Rotation wurde abgebrochen.");
|
||||
if (stand.schritt < 4) {
|
||||
throw new RotationsFehler(
|
||||
stand.schritt === 3
|
||||
@@ -311,3 +331,46 @@ export async function rotationAltWiderrufen(
|
||||
}
|
||||
await widerrufEinleiten([altId], akteur, optionen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Laufende Rotation abbrechen (#032): der neue Schluessel wird ueber den normalen Widerruf (#024)
|
||||
* entfernt – mit Dry-Run, denn er kann schon ausgerollt sein. Der alte bleibt aktiv und
|
||||
* unveraendert; wo beide denselben Alias tragen, zeigt die config nach dem Anwenden wieder auf
|
||||
* die alte Datei (juengste *verbleibende* Zuweisung gewinnt, src/worker/ablaeufe/privat.ts).
|
||||
*
|
||||
* `vorgaengerId` des neuen Schluessels bleibt fuer die Historie stehen; dass abgebrochen wurde,
|
||||
* erkennt man am Widerruf des Nachfolgers und am Protokolleintrag. Ist der alte Schluessel schon
|
||||
* im Widerruf (Schritt 5), gibt es kein Zurueck mehr – sonst waeren am Ende beide halb entfernt.
|
||||
*/
|
||||
export async function rotationAbbrechen(
|
||||
altId: string,
|
||||
akteur: Akteur,
|
||||
optionen: { readonly privatErlaubt: boolean },
|
||||
): Promise<void> {
|
||||
const alt = await altLaden(altId);
|
||||
const neu = alt.nachfolger;
|
||||
if (!neu) throw new RotationsFehler("Für diesen Schlüssel läuft keine Rotation.");
|
||||
if (alt.status === "ROTIERT") throw new RotationsFehler("Die Rotation ist bereits abgeschlossen.");
|
||||
// Idempotent: ein zweiter Klick (oder ein Neuladen) setzt den laufenden Abbruch nur fort.
|
||||
if (rotationAbgebrochen(neu)) return;
|
||||
if (alt.widerrufEingeleitetAm) {
|
||||
throw new RotationsFehler(
|
||||
"Der alte Schlüssel wird bereits entfernt – die Rotation lässt sich nicht mehr abbrechen.",
|
||||
);
|
||||
}
|
||||
await widerrufEinleiten([neu.id], akteur, {
|
||||
privatErlaubt: optionen.privatErlaubt,
|
||||
zusaetzlich: async (tx) => {
|
||||
await protokollieren(
|
||||
{
|
||||
akteur,
|
||||
aktion: "schluessel.rotation.abgebrochen",
|
||||
ergebnis: "ERFOLG",
|
||||
fingerprint: alt.fingerprint,
|
||||
details: { name: alt.name, nachfolgerId: neu.id, nachfolger: neu.name },
|
||||
},
|
||||
tx,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const SCHLUESSEL_KOPF = {
|
||||
status: true,
|
||||
fingerprint: true,
|
||||
widerrufEingeleitetAm: true,
|
||||
nachfolger: { select: { id: true, status: true } },
|
||||
nachfolger: { select: { id: true, status: true, widerrufEingeleitetAm: true } },
|
||||
} satisfies Prisma.SchluesselSelect;
|
||||
|
||||
type SchluesselKopf = Prisma.SchluesselGetPayload<{ select: typeof SCHLUESSEL_KOPF }>;
|
||||
@@ -167,6 +167,11 @@ export async function zielkontoKandidaten(schluesselIds: readonly string[]): Pro
|
||||
export type EinleitenOptionen = {
|
||||
/** Darf der Akteur Private Keys entfernen (privateKeyAusrollen)? Prueft die Aktion per darf(). */
|
||||
readonly privatErlaubt: boolean;
|
||||
/**
|
||||
* Laeuft in derselben Transaktion wie das Vormerken – etwa der Protokolleintrag eines
|
||||
* Rotationsabbruchs (#032), damit es keinen Abbruch ohne Eintrag gibt.
|
||||
*/
|
||||
readonly zusaetzlich?: (tx: Prisma.TransactionClient) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -264,6 +269,7 @@ export async function widerrufEinleiten(
|
||||
tx,
|
||||
);
|
||||
}
|
||||
await optionen.zusaetzlich?.(tx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,7 +412,9 @@ export async function widerrufAbschliessen(
|
||||
continue;
|
||||
}
|
||||
// Ein aktiver Nachfolger heisst: der Widerruf ist der letzte Schritt einer Rotation (#023).
|
||||
const status = s.nachfolger?.status === "AKTIV" ? "ROTIERT" : "WIDERRUFEN";
|
||||
// Ein Nachfolger im Widerruf ist eine abgebrochene Rotation (#032) – ersetzt ist dann nichts.
|
||||
const status =
|
||||
s.nachfolger?.status === "AKTIV" && !s.nachfolger.widerrufEingeleitetAm ? "ROTIERT" : "WIDERRUFEN";
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const geaendert = await tx.schluessel.updateMany({
|
||||
where: { id: s.id, status: { in: ["AKTIV", "ABGELAUFEN"] } },
|
||||
|
||||
Reference in New Issue
Block a user