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:
@@ -14,7 +14,17 @@ import type { DiffZeile } from "@/lib/ssh/zeilen-diff";
|
||||
* schreibt: der Plan traegt die Hashes, und das Ausrollen bricht ab, wenn sich seitdem auf
|
||||
* dem Host oder an den Zuweisungen etwas geaendert hat.
|
||||
*/
|
||||
type DateiPlan = {
|
||||
art: "privateKey" | "publicKey" | "config" | "knownHosts";
|
||||
pfad: string;
|
||||
aktion: "anlegen" | "aendern" | "keine" | "entfernen";
|
||||
diff?: DiffZeile[];
|
||||
hinweis?: string;
|
||||
};
|
||||
|
||||
type PlanZiel = {
|
||||
art?: "public" | "privat";
|
||||
dateien?: DateiPlan[];
|
||||
hostName: string;
|
||||
zielbenutzer: string;
|
||||
pfad?: string;
|
||||
@@ -77,6 +87,8 @@ export function AusrollenPanel({ auswahl, offen }: { auswahl: AusrollAuswahl; of
|
||||
<p className="border-danger-border bg-danger-soft text-danger rounded-box border px-4 py-3 font-mono text-[13px]">
|
||||
{ziel.zielbenutzer}@{ziel.hostName}: {ziel.fehler}
|
||||
</p>
|
||||
) : ziel.art === "privat" ? (
|
||||
<PrivatPlanAnzeige ziel={ziel} />
|
||||
) : (
|
||||
<DiffBox
|
||||
datei={`${ziel.zielbenutzer}@${ziel.hostName}:${ziel.pfad ?? "~/.ssh/authorized_keys"}`}
|
||||
@@ -95,8 +107,9 @@ export function AusrollenPanel({ auswahl, offen }: { auswahl: AusrollAuswahl; of
|
||||
<div className="border-border flex flex-col gap-3 border-t pt-4">
|
||||
<label className="text-muted flex min-h-11 items-center gap-3 text-[14px]">
|
||||
<input type="checkbox" checked disabled className="accent-accent size-5" />
|
||||
Backup jeder Datei anlegen (
|
||||
<code className="font-mono text-[12.5px]">authorized_keys.sshkv.bak.…</code>)
|
||||
Backup jeder geänderten Datei anlegen (
|
||||
<code className="font-mono text-[12.5px]">….sshkv.bak.…</code>) – außer bei Private Keys, von
|
||||
denen keine zweite Kopie liegen bleiben soll
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
@@ -131,3 +144,49 @@ export function AusrollenPanel({ auswahl, offen }: { auswahl: AusrollAuswahl; of
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AKTION_TEXT: Record<DateiPlan["aktion"], string> = {
|
||||
anlegen: "wird angelegt",
|
||||
aendern: "wird geändert",
|
||||
entfernen: "wird entfernt",
|
||||
keine: "unverändert",
|
||||
};
|
||||
|
||||
/**
|
||||
* Private-Key-Ziel (#017): die Dateien unter ~/.ssh einzeln. Vom Private Key steht nur die
|
||||
* Aktion da – sein Inhalt erreicht den Browser nie.
|
||||
*/
|
||||
function PrivatPlanAnzeige({ ziel }: { ziel: PlanZiel }) {
|
||||
const dateien = ziel.dateien ?? [];
|
||||
return (
|
||||
<div className="border-border bg-bg rounded-box flex flex-col gap-3 border p-4">
|
||||
<p className="text-muted font-mono text-[12.5px] break-all">
|
||||
{ziel.zielbenutzer}@{ziel.hostName} · Private Key, ~/.ssh/config, known_hosts
|
||||
</p>
|
||||
<ul className="flex flex-col gap-1 font-mono text-[12.5px]">
|
||||
{dateien
|
||||
.filter((d) => d.art === "privateKey" || d.art === "publicKey")
|
||||
.map((d) => (
|
||||
<li
|
||||
key={d.pfad}
|
||||
className={
|
||||
d.aktion === "keine" ? "text-muted" : d.aktion === "entfernen" ? "text-danger" : "text-ok"
|
||||
}
|
||||
>
|
||||
{d.aktion === "keine" ? " " : d.aktion === "entfernen" ? "−" : "+"} {d.pfad} –{" "}
|
||||
{AKTION_TEXT[d.aktion]}
|
||||
{d.art === "privateKey" && d.aktion !== "keine" && d.aktion !== "entfernen"
|
||||
? " (Inhalt wird nicht angezeigt)"
|
||||
: ""}
|
||||
{d.hinweis ? ` · ${d.hinweis}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{dateien
|
||||
.filter((d) => (d.art === "config" || d.art === "knownHosts") && d.aktion !== "keine" && d.diff)
|
||||
.map((d) => (
|
||||
<DiffBox key={d.pfad} datei={`${ziel.zielbenutzer}@${ziel.hostName}:${d.pfad}`} zeilen={d.diff!} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user