Ein Abgleich liest je Host alle authorized_keys in einem SSH-Aufruf, als jeweiliger Benutzer, und vergleicht sie mit dem Soll: fehlende, von Hand geaenderte und unbekannte Keys, falsche Rechte, beschaedigte Bloecke und veraenderte Private Keys. Befunde werden geoeffnet und wieder geschlossen, Zustaende von Verwendungen und Host nachgezogen. Auf dem Host aendert der Abgleich nichts. Der Zeitplan liegt in pg-boss und feuert auch bei mehreren Workern nur einmal. Auf der Hostseite stehen Abweichungen und der Ist-Zustand je Benutzer; unbekannte Keys lassen sich uebernehmen - nur bekannt machen oder mit Dry-Run in den verwalteten Block holen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
import { Badge } from "@/components/ui/Badge";
|
||
import { Karte } from "@/components/ui/Karte";
|
||
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||
import { VorgangsKnopf } from "@/components/vorgang/VorgangsKnopf";
|
||
import { datumZeit } from "@/lib/format";
|
||
import { abgleichAktion } from "@/server/actions/abgleich";
|
||
import type { IstZustand } from "@/server/queries/abgleich";
|
||
import { UebernehmenDialog } from "./UebernehmenDialog";
|
||
|
||
/** Abweichungen und Ist-Zustand eines Hosts (#021, #022). */
|
||
const ART_TEXT: Record<string, string> = {
|
||
UNBEKANNTER_SCHLUESSEL: "Unbekannter Schlüssel",
|
||
FEHLENDER_SCHLUESSEL: "Schlüssel fehlt",
|
||
ZUSAETZLICH_IM_BLOCK: "Block von Hand geändert",
|
||
DATEI_GEAENDERT: "Block beschädigt",
|
||
RECHTE: "Rechte",
|
||
HOSTKEY_GEAENDERT: "Host-Key geändert",
|
||
PRIVATE_KEY_FEHLT: "Private Key fehlt/verändert",
|
||
};
|
||
|
||
const HERKUNFT: Record<string, { text: string; variante: "ok" | "neutral" | "gefahr" }> = {
|
||
verwaltet: { text: "verwaltet", variante: "ok" },
|
||
bekannt: { text: "bekannt", variante: "neutral" },
|
||
fremd: { text: "unbekannt", variante: "gefahr" },
|
||
};
|
||
|
||
export function AbgleichKarten({
|
||
hostId,
|
||
zustand,
|
||
darfAbgleichen,
|
||
darfUebernehmen,
|
||
darfVerwalten,
|
||
}: {
|
||
hostId: string;
|
||
zustand: IstZustand;
|
||
darfAbgleichen: boolean;
|
||
darfUebernehmen: boolean;
|
||
darfVerwalten: boolean;
|
||
}) {
|
||
return (
|
||
<>
|
||
<Karte
|
||
titel="Abweichungen"
|
||
untertitel="Unterschiede zwischen Soll und Host beim letzten Abgleich"
|
||
aktionen={
|
||
darfAbgleichen ? (
|
||
<VorgangsKnopf aktion={abgleichAktion.bind(null, hostId)} beschriftung="Jetzt abgleichen" />
|
||
) : null
|
||
}
|
||
>
|
||
{zustand.abweichungen.length === 0 ? (
|
||
<p className="text-ok text-[14px]">● Keine offenen Abweichungen.</p>
|
||
) : (
|
||
<ul className="flex flex-col">
|
||
{zustand.abweichungen.map((a) => (
|
||
<li
|
||
key={a.id}
|
||
className="border-border flex flex-col gap-1 border-b py-3 first:pt-0 last:border-b-0 last:pb-0"
|
||
>
|
||
<span className="text-danger text-[14px] font-medium">
|
||
● {ART_TEXT[a.art] ?? a.art}
|
||
{a.zielbenutzer && (
|
||
<span className="text-muted font-mono text-[13px]"> · {a.zielbenutzer}</span>
|
||
)}
|
||
</span>
|
||
<span className="text-text text-[14px]">{a.text}</span>
|
||
<span className="text-muted font-mono text-[12px] break-all">
|
||
{a.fingerprint ? `${a.fingerprint} · ` : ""}seit {datumZeit(a.eroeffnetAm)}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</Karte>
|
||
|
||
<Karte
|
||
titel="Ist-Zustand je Benutzer"
|
||
untertitel="Alle Schlüssel in authorized_keys – auch die, die sshkv nicht verwaltet"
|
||
>
|
||
{zustand.beobachtungen.length === 0 ? (
|
||
<Leerzustand
|
||
titel="Noch nicht abgeglichen"
|
||
text="Nach dem ersten Abgleich steht hier, was wirklich auf dem Host liegt."
|
||
/>
|
||
) : (
|
||
<div className="flex flex-col gap-6">
|
||
{zustand.beobachtungen.map((b) => (
|
||
<section key={b.zielbenutzer} className="flex flex-col gap-2">
|
||
<h3 className="text-text font-mono text-[14px] font-medium">
|
||
{b.zielbenutzer}{" "}
|
||
<span className="text-muted text-[12px]">· {datumZeit(b.beobachtetAm)}</span>
|
||
</h3>
|
||
{b.eintraege.length === 0 ? (
|
||
<p className="text-muted text-[13.5px]">Datei leer.</p>
|
||
) : (
|
||
<ul className="flex flex-col gap-2">
|
||
{b.eintraege.map((e) => {
|
||
const herkunft = HERKUNFT[e.herkunft]!;
|
||
return (
|
||
<li
|
||
key={`${e.fingerprint}-${e.imBlock}`}
|
||
className="border-border rounded-box flex flex-col gap-2 border p-3 sm:flex-row sm:items-center sm:justify-between"
|
||
>
|
||
<div className="min-w-0">
|
||
<p className="font-mono text-[12.5px] break-all">
|
||
<span className="text-muted">{e.typ}</span> {e.fingerprint}
|
||
</p>
|
||
<p className="text-muted font-mono text-[12px] break-all">
|
||
{e.kommentar || "(ohne Kommentar)"}
|
||
{e.optionen ? ` · ${e.optionen}` : ""}
|
||
{e.imBlock ? " · im verwalteten Block" : ""}
|
||
</p>
|
||
</div>
|
||
<div className="flex shrink-0 items-center gap-2">
|
||
<Badge variante={herkunft.variante}>{herkunft.text}</Badge>
|
||
{e.herkunft === "fremd" && darfUebernehmen && (
|
||
<UebernehmenDialog
|
||
hostId={hostId}
|
||
zielbenutzer={b.zielbenutzer}
|
||
fingerprint={e.fingerprint}
|
||
kommentar={e.kommentar}
|
||
personen={zustand.personen}
|
||
darfVerwalten={darfVerwalten}
|
||
/>
|
||
)}
|
||
</div>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Karte>
|
||
</>
|
||
);
|
||
}
|