feat(schluessel): Zuweisen und Ausrollen auf der Schluesselseite (#017)
Auf der Seite eines Schluessels: Public Key einem Host oder einer Gruppe zuweisen, Private Key (nur Admin) auf einen Quell-Host legen - mit ~/.ssh/config-Eintrag und dem Host-Key des Ziels in known_hosts -, Dry-Run fuer alle Ziele, anwenden und die Verbindung von A nach B testen. Der Kern-Anwendungsfall laeuft damit im Browser durch (E2E). Der Dry-Run nimmt Private-Key-Ziele nur fuer Admins auf: sonst koennte ein Operator einen vorbereiteten Private Key ausrollen lassen. Die Vorschau zeigt je Datei die Aktion; der Inhalt eines Private Keys erreicht den Browser nie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,17 @@ import { revalidatePath } from "next/cache";
|
||||
import { z } from "@/lib/validierung";
|
||||
import { BestaetigungFehler, RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { akteurVon, textfeld, type FormularZustand } from "@/server/actions/formular";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import {
|
||||
VerwendungsFehler,
|
||||
ausrollenStarten,
|
||||
privateVerwendungAnlegen,
|
||||
privateVerwendungEntfernen,
|
||||
publicVerwendungAnlegen,
|
||||
publicVerwendungEntfernen,
|
||||
trockenlaufStarten,
|
||||
} from "@/server/services/verwendungen";
|
||||
import { vorgangEinreihen } from "@/server/vorgaenge/vorgaenge";
|
||||
|
||||
/** Public-Key-Zuweisungen und Ausrollen (#016). Duenn: Recht, Eingabe, Service. */
|
||||
|
||||
@@ -82,6 +86,8 @@ export async function trockenlaufAktion(auswahl: AusrollAuswahl): Promise<Vorgan
|
||||
? { schluesselId: id.parse(auswahl.schluesselId) }
|
||||
: { hostId: id.parse(auswahl.hostId) },
|
||||
akteurVon(benutzer),
|
||||
// Private-Key-Ziele nur fuer Admins – ein Operator rollt so nie einen Private Key aus.
|
||||
darf(benutzer.rolle, "privateKeyAusrollen"),
|
||||
),
|
||||
};
|
||||
} catch (fehler) {
|
||||
@@ -99,3 +105,69 @@ export async function ausrollenAktion(trockenlaufId: string): Promise<VorgangsAn
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
/** Private Key einem Quell-Host zuweisen (#017) – nur Admins. */
|
||||
export async function privateZuweisenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("privateKeyAusrollen");
|
||||
const schluesselId = z.string().min(1).max(64).parse(textfeld(formular, "schluesselId"));
|
||||
const alias = (textfeld(formular, "alias") ?? "").trim();
|
||||
const zielHostId = textfeld(formular, "zielHostId");
|
||||
await privateVerwendungAnlegen(
|
||||
{
|
||||
schluesselId,
|
||||
hostId: textfeld(formular, "hostId") ?? "",
|
||||
zielbenutzer: (textfeld(formular, "zielbenutzer") ?? "").trim(),
|
||||
pfad: textfeld(formular, "pfad") ?? "",
|
||||
sshConfig: alias
|
||||
? {
|
||||
alias,
|
||||
hostName: (textfeld(formular, "hostName") ?? "").trim(),
|
||||
benutzer: (textfeld(formular, "sshBenutzer") ?? "").trim(),
|
||||
port: Number(textfeld(formular, "port") ?? "22"),
|
||||
}
|
||||
: null,
|
||||
knownHostsFuer: zielHostId ? [zielHostId] : [],
|
||||
beimWiderruf: textfeld(formular, "beimWiderruf") === "UMBENENNEN" ? "UMBENENNEN" : "LOESCHEN",
|
||||
},
|
||||
akteurVon(benutzer),
|
||||
);
|
||||
revalidatePath(`/schluessel/${schluesselId}`);
|
||||
return {};
|
||||
} catch (fehler) {
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function privateEntfernenAktion(
|
||||
verwendungId: string,
|
||||
schluesselId: string,
|
||||
): Promise<{ fehler?: string }> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("privateKeyAusrollen");
|
||||
await privateVerwendungEntfernen(z.string().min(1).max(64).parse(verwendungId), akteurVon(benutzer));
|
||||
revalidatePath(`/schluessel/${schluesselId}`);
|
||||
return {};
|
||||
} catch (fehler) {
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
/** Verbindungstest A → B (#020). Harmlos, aber nur fuer Rollen, die Hosts verwalten. */
|
||||
export async function verbindungTestenAktion(privatVerwendungId: string): Promise<VorgangsAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("hostsVerwalten");
|
||||
return {
|
||||
vorgangId: await vorgangEinreihen({
|
||||
art: "VERBINDUNGSTEST",
|
||||
angefordertVonId: benutzer.id,
|
||||
eingabe: { privatVerwendungId: z.string().min(1).max(64).parse(privatVerwendungId) },
|
||||
}),
|
||||
};
|
||||
} catch (fehler) {
|
||||
return alsAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export async function zielAuswahl() {
|
||||
const [hosts, gruppen] = await Promise.all([
|
||||
prisma.host.findMany({
|
||||
where: { archiviertAm: null },
|
||||
select: { id: true, name: true, bootstrapAm: true },
|
||||
select: { id: true, name: true, bootstrapAm: true, adresse: true, port: true },
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
prisma.hostgruppe.findMany({
|
||||
@@ -15,6 +15,7 @@ export async function zielAuswahl() {
|
||||
]);
|
||||
return {
|
||||
hosts: hosts.map((h) => ({ id: h.id, name: h.name, bereit: h.bootstrapAm !== null })),
|
||||
hostsDetails: hosts.map((h) => ({ id: h.id, name: h.name, adresse: h.adresse, port: h.port })),
|
||||
gruppen: gruppen.map((g) => ({ id: g.id, name: g.name, mitglieder: g._count.mitglieder })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ export async function publicVerwendungEntfernen(id: string, akteur: Akteur): Pro
|
||||
export async function trockenlaufStarten(
|
||||
auswahl: { schluesselId?: string; hostId?: string; verwendungIds?: string[]; ziele?: Ziel[] },
|
||||
akteur: Akteur,
|
||||
/** Private-Key-Ziele nur fuer Admins (Freigabe Frage 3) – der Aufrufer entscheidet per darf(). */
|
||||
mitPrivat = false,
|
||||
): Promise<string> {
|
||||
const ziele = auswahl.ziele ?? [
|
||||
...(await zieleVonVerwendungen({
|
||||
@@ -128,7 +130,7 @@ export async function trockenlaufStarten(
|
||||
...(auswahl.verwendungIds ? { ids: auswahl.verwendungIds } : {}),
|
||||
})),
|
||||
// Private Keys nur, wenn nicht gezielt einzelne Public-Verwendungen gemeint sind.
|
||||
...(auswahl.verwendungIds
|
||||
...(auswahl.verwendungIds || !mitPrivat
|
||||
? []
|
||||
: await zieleVonPrivatVerwendungen({
|
||||
...(auswahl.schluesselId ? { schluesselId: auswahl.schluesselId } : {}),
|
||||
|
||||
Reference in New Issue
Block a user