Merge branch 'phase/017-private-key-ausrollen'
# Conflicts: # src/worker/handler.ts
This commit is contained in:
@@ -94,7 +94,12 @@ export async function sollFuer(hostId: string, zielbenutzer: string): Promise<So
|
||||
return { eintraege, pflicht, verwendungIds: { drin, raus } };
|
||||
}
|
||||
|
||||
export type Ziel = { readonly hostId: string; readonly zielbenutzer: string };
|
||||
/** Ein Ziel des Ausrollens: authorized_keys (public) oder ~/.ssh-Dateien eines Quell-Hosts (privat). */
|
||||
export type Ziel = {
|
||||
readonly hostId: string;
|
||||
readonly zielbenutzer: string;
|
||||
readonly art?: "public" | "privat";
|
||||
};
|
||||
|
||||
/** Alle (Host, Benutzer)-Paare, die eine Public-Key-Verwendung betrifft – Gruppen aufgeloest. */
|
||||
export async function zieleVonVerwendungen(where: {
|
||||
@@ -140,3 +145,28 @@ export async function zieleVonVerwendungen(where: {
|
||||
}
|
||||
return [...ziele.values()];
|
||||
}
|
||||
|
||||
/** Ziele der Private-Key-Verwendungen (#017): je Quell-Host und Benutzer ein Ziel. */
|
||||
export async function zieleVonPrivatVerwendungen(where: {
|
||||
schluesselId?: string;
|
||||
hostId?: string;
|
||||
}): Promise<Ziel[]> {
|
||||
const verwendungen = await prisma.privateVerwendung.findMany({
|
||||
where: {
|
||||
...(where.schluesselId ? { schluesselId: where.schluesselId } : {}),
|
||||
...(where.hostId ? { hostId: where.hostId } : {}),
|
||||
zustand: { not: "ENTFERNT" },
|
||||
host: { archiviertAm: null },
|
||||
},
|
||||
select: { hostId: true, zielbenutzer: true },
|
||||
});
|
||||
const ziele = new Map<string, Ziel>();
|
||||
for (const v of verwendungen) {
|
||||
ziele.set(`${v.hostId}\u0000${v.zielbenutzer}`, {
|
||||
hostId: v.hostId,
|
||||
zielbenutzer: v.zielbenutzer,
|
||||
art: "privat",
|
||||
});
|
||||
}
|
||||
return [...ziele.values()];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { prisma } from "@/lib/db";
|
||||
import { BENUTZERNAME_MUSTER } from "@/lib/shell";
|
||||
import { OptionenFehler, optionenPruefen } from "@/lib/ssh/optionen";
|
||||
import { protokollieren } from "@/server/services/protokoll";
|
||||
import { zieleVonVerwendungen, type Ziel } from "@/server/services/soll";
|
||||
import { zieleVonPrivatVerwendungen, zieleVonVerwendungen, type Ziel } from "@/server/services/soll";
|
||||
import { SshConfigFehler, privatPfadPruefen, sshConfigPruefen } from "@/lib/ssh/ssh-config";
|
||||
import { vorgangEinreihen } from "@/server/vorgaenge/vorgaenge";
|
||||
|
||||
/**
|
||||
@@ -120,13 +121,20 @@ export async function trockenlaufStarten(
|
||||
auswahl: { schluesselId?: string; hostId?: string; verwendungIds?: string[]; ziele?: Ziel[] },
|
||||
akteur: Akteur,
|
||||
): Promise<string> {
|
||||
const ziele =
|
||||
auswahl.ziele ??
|
||||
(await zieleVonVerwendungen({
|
||||
const ziele = auswahl.ziele ?? [
|
||||
...(await zieleVonVerwendungen({
|
||||
...(auswahl.schluesselId ? { schluesselId: auswahl.schluesselId } : {}),
|
||||
...(auswahl.hostId ? { hostId: auswahl.hostId } : {}),
|
||||
...(auswahl.verwendungIds ? { ids: auswahl.verwendungIds } : {}),
|
||||
}));
|
||||
})),
|
||||
// Private Keys nur, wenn nicht gezielt einzelne Public-Verwendungen gemeint sind.
|
||||
...(auswahl.verwendungIds
|
||||
? []
|
||||
: await zieleVonPrivatVerwendungen({
|
||||
...(auswahl.schluesselId ? { schluesselId: auswahl.schluesselId } : {}),
|
||||
...(auswahl.hostId ? { hostId: auswahl.hostId } : {}),
|
||||
})),
|
||||
];
|
||||
if (ziele.length === 0) throw new VerwendungsFehler("Es gibt nichts auszurollen.");
|
||||
return vorgangEinreihen({
|
||||
art: "TROCKENLAUF",
|
||||
@@ -150,3 +158,115 @@ export async function ausrollenStarten(trockenlaufId: string, akteur: Akteur): P
|
||||
eingabe: { trockenlaufId },
|
||||
});
|
||||
}
|
||||
|
||||
export type PrivateZuweisung = {
|
||||
readonly schluesselId: string;
|
||||
readonly hostId: string;
|
||||
readonly zielbenutzer: string;
|
||||
readonly pfad: string;
|
||||
/** Optionaler Block in ~/.ssh/config des Zielbenutzers. */
|
||||
readonly sshConfig?: { alias: string; hostName: string; benutzer: string; port: number } | null;
|
||||
/** Hosts, deren Host-Key in known_hosts gepflegt wird. */
|
||||
readonly knownHostsFuer?: string[];
|
||||
readonly beimWiderruf?: "LOESCHEN" | "UMBENENNEN";
|
||||
};
|
||||
|
||||
/**
|
||||
* Private Key einem Quell-Host zuweisen (#017). Nur Admins (Recht privateKeyAusrollen, prueft
|
||||
* die Aktion). Ein passphrasengeschuetzter Key ist fuer automatische Anmeldungen ungeeignet –
|
||||
* die Oberflaeche warnt, verboten ist er nicht (Freigabe Frage 6).
|
||||
*/
|
||||
export async function privateVerwendungAnlegen(eingabe: PrivateZuweisung, akteur: Akteur): Promise<string> {
|
||||
if (!BENUTZERNAME_MUSTER.test(eingabe.zielbenutzer))
|
||||
throw new VerwendungsFehler("Ungültiger Zielbenutzer.");
|
||||
let pfad: string;
|
||||
try {
|
||||
pfad = privatPfadPruefen(eingabe.pfad);
|
||||
if (eingabe.sshConfig) sshConfigPruefen({ ...eingabe.sshConfig, identityFile: pfad });
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof SshConfigFehler) throw new VerwendungsFehler(fehler.message);
|
||||
throw fehler;
|
||||
}
|
||||
const schluessel = await prisma.schluessel.findUnique({
|
||||
where: { id: eingabe.schluesselId },
|
||||
select: {
|
||||
status: true,
|
||||
fingerprint: true,
|
||||
kategorie: true,
|
||||
privatGeloeschtAm: true,
|
||||
masterKeyVersion: true,
|
||||
},
|
||||
});
|
||||
if (!schluessel) throw new VerwendungsFehler("Diesen Schlüssel gibt es nicht.");
|
||||
if (schluessel.status !== "AKTIV")
|
||||
throw new VerwendungsFehler("Nur aktive Schlüssel lassen sich ausrollen.");
|
||||
if (schluessel.kategorie !== "DIENST")
|
||||
throw new VerwendungsFehler("Private Keys werden nur für Service-Schlüssel ausgerollt.");
|
||||
// masterKeyVersion ist genau dann gesetzt, wenn ein Private Key gespeichert ist (DB-CHECK).
|
||||
if (schluessel.masterKeyVersion === null) {
|
||||
throw new VerwendungsFehler(
|
||||
schluessel.privatGeloeschtAm
|
||||
? "Der Private Key wurde aus SSHKeyVault gelöscht."
|
||||
: "Zu diesem Schlüssel ist kein Private Key gespeichert.",
|
||||
);
|
||||
}
|
||||
const host = await prisma.host.findUnique({
|
||||
where: { id: eingabe.hostId },
|
||||
select: { name: true, archiviertAm: true },
|
||||
});
|
||||
if (!host || host.archiviertAm) throw new VerwendungsFehler("Den Host gibt es nicht (mehr).");
|
||||
|
||||
try {
|
||||
const verwendung = await prisma.privateVerwendung.create({
|
||||
data: {
|
||||
schluesselId: eingabe.schluesselId,
|
||||
hostId: eingabe.hostId,
|
||||
zielbenutzer: eingabe.zielbenutzer,
|
||||
pfad,
|
||||
sshConfig: eingabe.sshConfig ?? undefined,
|
||||
knownHostsFuer: eingabe.knownHostsFuer ?? [],
|
||||
beimWiderruf: eingabe.beimWiderruf ?? "LOESCHEN",
|
||||
angelegtVonId: akteur.id,
|
||||
},
|
||||
});
|
||||
await protokollieren({
|
||||
akteur,
|
||||
aktion: "verwendung.private.angelegt",
|
||||
ergebnis: "ERFOLG",
|
||||
hostName: host.name,
|
||||
zielbenutzer: eingabe.zielbenutzer,
|
||||
fingerprint: schluessel.fingerprint,
|
||||
details: { pfad, sshConfig: eingabe.sshConfig ?? null },
|
||||
});
|
||||
return verwendung.id;
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof Prisma.PrismaClientKnownRequestError && fehler.code === "P2002") {
|
||||
throw new VerwendungsFehler(
|
||||
"An diesem Pfad liegt für diesen Benutzer bereits ein verwalteter Schlüssel.",
|
||||
);
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
|
||||
export async function privateVerwendungEntfernen(id: string, akteur: Akteur): Promise<void> {
|
||||
const verwendung = await prisma.privateVerwendung.findUnique({
|
||||
where: { id },
|
||||
include: { schluessel: { select: { fingerprint: true } }, host: { select: { name: true } } },
|
||||
});
|
||||
if (!verwendung || verwendung.zustand === "ENTFERNT")
|
||||
throw new VerwendungsFehler("Diese Zuweisung gibt es nicht.");
|
||||
const nieAusgerollt = verwendung.zustand === "GEPLANT" && !verwendung.zuletztAngewendetAm;
|
||||
await prisma.privateVerwendung.update({
|
||||
where: { id },
|
||||
data: { zustand: nieAusgerollt ? "ENTFERNT" : "ENTFERNEN" },
|
||||
});
|
||||
await protokollieren({
|
||||
akteur,
|
||||
aktion: "verwendung.private.entfernen",
|
||||
ergebnis: "ERFOLG",
|
||||
hostName: verwendung.host.name,
|
||||
zielbenutzer: verwendung.zielbenutzer,
|
||||
fingerprint: verwendung.schluessel.fingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user