feat(hosts): Schluessel auf dem Host anzeigen und ausrollen (#016)

Die Hostseite listet, welche Public und Private Keys sshkv dort
verwaltet - direkt oder ueber eine Hostgruppe - mit Zustand als Punkt
und Text. Ein Dry-Run je Host zeigt das Diff aller Benutzer dieses
Hosts und rollt nach Bestaetigung aus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 10:19:27 +02:00
co-authored by Claude Opus 5.5
parent f66d87ad11
commit 30f271b23a
7 changed files with 151 additions and 13 deletions
+64 -6
View File
@@ -18,6 +18,9 @@ import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import { hostDetail, type HostDetail } from "@/server/queries/hosts";
import { BootstrapAktionen } from "./BootstrapAktionen";
import { sshdHinweise } from "@/lib/sshd-hinweise";
import { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel";
import { VerwendungsZustand } from "@/components/ausrollen/VerwendungsZustand";
import { verwendungenAufHost } from "@/server/queries/ziele";
import { HostKeyAktionen } from "./HostKeyAktionen";
import { HostKeyEntscheidung } from "./HostKeyEntscheidung";
import { bootstrapBefehlFuer } from "@/server/queries/bootstrap";
@@ -95,6 +98,8 @@ export default async function HostSeite({ params }: { params: Promise<{ id: stri
const archiviert = host.archiviertAm !== null;
const verwalten = darf(benutzer.rolle, "hostsVerwalten") && !archiviert;
const darfEntscheiden = darf(benutzer.rolle, "hostKeyEntscheiden") && !archiviert;
const darfAusrollen = darf(benutzer.rolle, "publicKeyAusrollen") && !archiviert;
const verwendungen = await verwendungenAufHost(host.id);
const jetzt = new Date();
return (
@@ -217,7 +222,7 @@ export default async function HostSeite({ params }: { params: Promise<{ id: stri
<Karte
titel="Schlüssel auf diesem Host"
untertitel={`${host.publicVerwendungen} Public-Key-Verwendungen · ${host.privateVerwendungen} Private-Key-Verwendungen`}
untertitel="Public Keys in authorized_keys und Private Keys, die sshkv hier verwaltet"
>
{host.zielkonten.length > 0 && (
<p className="text-muted mb-4 text-[14px]">
@@ -230,11 +235,64 @@ export default async function HostSeite({ params }: { params: Promise<{ id: stri
))}
</p>
)}
<Leerzustand
icon={<KeyRound className="size-5" />}
titel="Folgt mit #016"
text="Hier erscheinen die Public und Private Keys, die sshkv auf diesem Host verwaltet."
/>
{verwendungen.oeffentlich.length === 0 && verwendungen.privat.length === 0 ? (
<Leerzustand
icon={<KeyRound className="size-5" />}
titel="Noch keine Schlüssel zugewiesen"
text="Zuweisen geht über die Seite eines Schlüssels („+ Ausrollen“)."
/>
) : (
<ul className="flex flex-col">
{verwendungen.oeffentlich.map((v) => (
<li
key={v.id}
className="border-border flex flex-col gap-1 border-b py-3 first:pt-0 last:border-b-0 sm:flex-row sm:items-center sm:justify-between sm:gap-4"
>
<span className="min-w-0">
<a
href={`/schluessel/${v.schluessel.id}`}
className="text-text hover:text-accent font-medium"
>
{v.schluessel.name}
</a>
<span className="text-muted block font-mono text-[12.5px] break-all">
Public · {v.zielbenutzer}@{host.name}
{v.hostgruppe ? ` · über ${v.hostgruppe.name}` : ""}
{v.optionen ? ` · ${v.optionen}` : ""}
</span>
</span>
<VerwendungsZustand zustand={v.zustand} />
</li>
))}
{verwendungen.privat.map((v) => (
<li
key={v.id}
className="border-border flex flex-col gap-1 border-b py-3 first:pt-0 last:border-b-0 sm:flex-row sm:items-center sm:justify-between sm:gap-4"
>
<span className="min-w-0">
<a
href={`/schluessel/${v.schluessel.id}`}
className="text-text hover:text-accent font-medium"
>
{v.schluessel.name}
</a>
<span className="text-muted block font-mono text-[12.5px] break-all">
Private · {v.zielbenutzer}@{host.name}:{v.pfad}
</span>
</span>
<VerwendungsZustand zustand={v.zustand} />
</li>
))}
</ul>
)}
{darfAusrollen && verwendungen.oeffentlich.length > 0 && host.bootstrapAm && (
<div className="border-border mt-5 border-t pt-5">
<AusrollenPanel
auswahl={{ hostId: host.id }}
offen={verwendungen.oeffentlich.filter((v) => v.zustand !== "AUSGEROLLT").length}
/>
</div>
)}
</Karte>
{verwalten && (