feat(vorlagen): Platzhalter, Pruefung und Service fuer Vorlagen (#019)
Vorlagen werden beim Speichern so geprueft, wie sie spaeter benutzt werden: mit probeweise aufgeloesten Platzhaltern. Ein unbekannter Platzhalter ist ein Fehler, kein leerer Text - sonst stuende from="" in authorized_keys. Werte mit Anfuehrungszeichen, Kommas oder Leerraum koennen keine weiteren Optionen einschleusen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
import { BENUTZERNAME_MUSTER } from "@/lib/shell";
|
||||||
|
import { OptionenFehler, optionenPruefen } from "@/lib/ssh/optionen";
|
||||||
|
import { befehlPruefen, ZielkontoFehler } from "@/lib/ssh/zielkonto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorlagen (#019): Platzhalter ersetzen und Vorlagen pruefen – rein, damit Assistent,
|
||||||
|
* Formular und Tests dieselben Regeln haben.
|
||||||
|
*
|
||||||
|
* Platzhalter: {{quellHost}}, {{quellHostIp}}, {{zielHost}}, {{zielHostIp}}. Ein unbekannter
|
||||||
|
* Platzhalter ist ein Fehler, kein leerer Text – sonst stuende `from=""` in authorized_keys.
|
||||||
|
*/
|
||||||
|
export const PLATZHALTER = ["quellHost", "quellHostIp", "zielHost", "zielHostIp"] as const;
|
||||||
|
export type Platzhalter = (typeof PLATZHALTER)[number];
|
||||||
|
export type PlatzhalterWerte = Partial<Record<Platzhalter, string>>;
|
||||||
|
|
||||||
|
export class VorlagenFehler extends Error {
|
||||||
|
constructor(
|
||||||
|
nachricht: string,
|
||||||
|
readonly feld?: string,
|
||||||
|
) {
|
||||||
|
super(nachricht);
|
||||||
|
this.name = "VorlagenFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUSTER = /\{\{\s*([A-Za-z]+)\s*\}\}/g;
|
||||||
|
|
||||||
|
export function platzhalterIn(text: string): string[] {
|
||||||
|
return [...text.matchAll(MUSTER)].map((treffer) => treffer[1]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function platzhalterErsetzen(text: string, werte: PlatzhalterWerte): string {
|
||||||
|
return text.replace(MUSTER, (_, name: string) => {
|
||||||
|
if (!(PLATZHALTER as readonly string[]).includes(name)) {
|
||||||
|
throw new VorlagenFehler(`Unbekannter Platzhalter {{${name}}}.`);
|
||||||
|
}
|
||||||
|
const wert = werte[name as Platzhalter];
|
||||||
|
if (!wert) throw new VorlagenFehler(`Für {{${name}}} fehlt ein Wert.`);
|
||||||
|
if (/["\\\s,]/.test(wert))
|
||||||
|
throw new VorlagenFehler(`Der Wert für {{${name}}} enthält unzulässige Zeichen.`);
|
||||||
|
return wert;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eine IPv4/IPv6-Adresse oder ein Netz in CIDR-Schreibweise, wie sie in from="…" stehen darf. */
|
||||||
|
export function istIpAdresse(wert: string): boolean {
|
||||||
|
return (
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/.test(wert) ||
|
||||||
|
(/^[0-9a-f:]+(\/\d{1,3})?$/i.test(wert) && wert.includes(":"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VorlageEingabe = {
|
||||||
|
readonly name: string;
|
||||||
|
readonly beschreibung?: string | null;
|
||||||
|
readonly schluesselTyp: "ED25519" | "RSA4096";
|
||||||
|
readonly kategorie: "PERSON" | "DIENST";
|
||||||
|
readonly standardLaufzeitTage?: number | null;
|
||||||
|
readonly zielbenutzer: string;
|
||||||
|
readonly zielbenutzerAnlegen: boolean;
|
||||||
|
readonly sudoModus: "KEINER" | "BEFEHLE" | "VOLL";
|
||||||
|
readonly sudoBefehle: readonly string[];
|
||||||
|
readonly authorizedKeysOptionen?: string | null;
|
||||||
|
readonly sshConfigAlias?: string | null;
|
||||||
|
readonly knownHostsEintragen: boolean;
|
||||||
|
readonly privatNachAusrollenLoeschen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Beispielwerte, mit denen eine Vorlage beim Speichern probeweise aufgeloest wird. */
|
||||||
|
const PROBE: PlatzhalterWerte = {
|
||||||
|
quellHost: "srv-quelle",
|
||||||
|
quellHostIp: "10.0.0.1",
|
||||||
|
zielHost: "srv-ziel",
|
||||||
|
zielHostIp: "10.0.0.2",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Prueft eine Vorlage so, wie sie spaeter benutzt wird: mit aufgeloesten Platzhaltern. */
|
||||||
|
export function vorlagePruefen(eingabe: VorlageEingabe): VorlageEingabe {
|
||||||
|
if (!eingabe.name.trim() || eingabe.name.length > 80)
|
||||||
|
throw new VorlagenFehler("Bitte einen Namen angeben.", "name");
|
||||||
|
if (!BENUTZERNAME_MUSTER.test(eingabe.zielbenutzer)) {
|
||||||
|
throw new VorlagenFehler("Ungültiger Zielbenutzer.", "zielbenutzer");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
eingabe.standardLaufzeitTage != null &&
|
||||||
|
(eingabe.standardLaufzeitTage < 1 || eingabe.standardLaufzeitTage > 3650)
|
||||||
|
) {
|
||||||
|
throw new VorlagenFehler("Laufzeit zwischen 1 und 3650 Tagen.", "standardLaufzeitTage");
|
||||||
|
}
|
||||||
|
if (eingabe.kategorie === "PERSON" && (eingabe.zielbenutzerAnlegen || eingabe.sudoModus !== "KEINER")) {
|
||||||
|
throw new VorlagenFehler(
|
||||||
|
"Personen-Vorlagen legen keine Benutzer an und vergeben kein sudo.",
|
||||||
|
"kategorie",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!eingabe.zielbenutzerAnlegen && eingabe.sudoModus !== "KEINER") {
|
||||||
|
throw new VorlagenFehler(
|
||||||
|
"sudo-Rechte vergibt sshkv nur für Benutzer, die es selbst anlegt.",
|
||||||
|
"sudoModus",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (eingabe.sudoModus === "BEFEHLE") {
|
||||||
|
if (eingabe.sudoBefehle.length === 0)
|
||||||
|
throw new VorlagenFehler("Bitte mindestens einen Befehl angeben.", "sudoBefehle");
|
||||||
|
eingabe.sudoBefehle.forEach(befehlPruefen);
|
||||||
|
}
|
||||||
|
if (eingabe.authorizedKeysOptionen)
|
||||||
|
optionenPruefen(platzhalterErsetzen(eingabe.authorizedKeysOptionen, PROBE));
|
||||||
|
} catch (fehler) {
|
||||||
|
if (fehler instanceof ZielkontoFehler) throw new VorlagenFehler(fehler.message, "sudoBefehle");
|
||||||
|
if (fehler instanceof OptionenFehler || fehler instanceof VorlagenFehler) {
|
||||||
|
throw new VorlagenFehler(
|
||||||
|
fehler.message,
|
||||||
|
fehler instanceof VorlagenFehler && fehler.feld ? fehler.feld : "authorizedKeysOptionen",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw fehler;
|
||||||
|
}
|
||||||
|
if (eingabe.sshConfigAlias) {
|
||||||
|
let alias: string;
|
||||||
|
try {
|
||||||
|
alias = platzhalterErsetzen(eingabe.sshConfigAlias, PROBE);
|
||||||
|
} catch (fehler) {
|
||||||
|
throw new VorlagenFehler((fehler as Error).message, "sshConfigAlias");
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9._-]{1,64}$/.test(alias))
|
||||||
|
throw new VorlagenFehler("Ungültiger Alias.", "sshConfigAlias");
|
||||||
|
}
|
||||||
|
return eingabe;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AufgeloesteVorlage = {
|
||||||
|
readonly zielbenutzer: string;
|
||||||
|
readonly authorizedKeysOptionen: string | null;
|
||||||
|
readonly sshConfigAlias: string | null;
|
||||||
|
readonly ablaufAm: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Loest eine Vorlage fuer einen konkreten Assistenten-Lauf auf (#020). */
|
||||||
|
export function vorlageAufloesen(
|
||||||
|
vorlage: Pick<
|
||||||
|
VorlageEingabe,
|
||||||
|
"zielbenutzer" | "authorizedKeysOptionen" | "sshConfigAlias" | "standardLaufzeitTage"
|
||||||
|
>,
|
||||||
|
werte: PlatzhalterWerte,
|
||||||
|
jetzt = new Date(),
|
||||||
|
): AufgeloesteVorlage {
|
||||||
|
const optionen = vorlage.authorizedKeysOptionen
|
||||||
|
? optionenPruefen(platzhalterErsetzen(vorlage.authorizedKeysOptionen, werte)) || null
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
zielbenutzer: vorlage.zielbenutzer,
|
||||||
|
authorizedKeysOptionen: optionen,
|
||||||
|
sshConfigAlias: vorlage.sshConfigAlias ? platzhalterErsetzen(vorlage.sshConfigAlias, werte) : null,
|
||||||
|
ablaufAm: vorlage.standardLaufzeitTage
|
||||||
|
? new Date(jetzt.getTime() + vorlage.standardLaufzeitTage * 24 * 60 * 60 * 1000)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { VorlagenFehler, type VorlageEingabe } from "@/lib/vorlagen";
|
||||||
|
import { akteurVon, fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
|
||||||
|
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||||
|
import { vorlageAnlegen, vorlageArchivieren, vorlageBearbeiten } from "@/server/services/vorlagen";
|
||||||
|
|
||||||
|
/** Vorlagen-Aktionen (#019): nur Admins (Recht einstellungenVerwalten). */
|
||||||
|
|
||||||
|
function felder(formular: FormData): VorlageEingabe {
|
||||||
|
const tage = textfeld(formular, "standardLaufzeitTage");
|
||||||
|
return {
|
||||||
|
name: textfeld(formular, "name") ?? "",
|
||||||
|
beschreibung: textfeld(formular, "beschreibung") ?? null,
|
||||||
|
schluesselTyp: textfeld(formular, "schluesselTyp") === "RSA4096" ? "RSA4096" : "ED25519",
|
||||||
|
kategorie: textfeld(formular, "kategorie") === "PERSON" ? "PERSON" : "DIENST",
|
||||||
|
standardLaufzeitTage: tage ? Number(tage) : null,
|
||||||
|
zielbenutzer: (textfeld(formular, "zielbenutzer") ?? "").trim(),
|
||||||
|
zielbenutzerAnlegen: formular.get("zielbenutzerAnlegen") === "ja",
|
||||||
|
sudoModus:
|
||||||
|
(["KEINER", "BEFEHLE", "VOLL"] as const).find((m) => m === textfeld(formular, "sudoModus")) ?? "KEINER",
|
||||||
|
sudoBefehle: (textfeld(formular, "sudoBefehle") ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((b) => b.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
authorizedKeysOptionen: textfeld(formular, "authorizedKeysOptionen") ?? null,
|
||||||
|
sshConfigAlias: textfeld(formular, "sshConfigAlias") ?? null,
|
||||||
|
knownHostsEintragen: formular.get("knownHostsEintragen") === "ja",
|
||||||
|
privatNachAusrollenLoeschen: formular.get("privatNachAusrollenLoeschen") === "ja",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function alsZustand(fehler: unknown): FormularZustand {
|
||||||
|
if (fehler instanceof VorlagenFehler) {
|
||||||
|
return {
|
||||||
|
fehler: fehler.message,
|
||||||
|
...(fehler.feld ? { feldfehler: { [fehler.feld]: fehler.message } } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return fehlerZustand(fehler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vorlageSpeichernAktion(
|
||||||
|
_vorher: FormularZustand,
|
||||||
|
formular: FormData,
|
||||||
|
): Promise<FormularZustand> {
|
||||||
|
const id = textfeld(formular, "id");
|
||||||
|
try {
|
||||||
|
const { benutzer } = await rechtErforderlich("einstellungenVerwalten");
|
||||||
|
if (id) await vorlageBearbeiten(id, felder(formular), akteurVon(benutzer));
|
||||||
|
else await vorlageAnlegen(felder(formular), akteurVon(benutzer));
|
||||||
|
} catch (fehler) {
|
||||||
|
return alsZustand(fehler);
|
||||||
|
}
|
||||||
|
revalidatePath("/vorlagen");
|
||||||
|
redirect("/vorlagen");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vorlageArchivierenAktion(
|
||||||
|
_vorher: FormularZustand,
|
||||||
|
formular: FormData,
|
||||||
|
): Promise<FormularZustand> {
|
||||||
|
try {
|
||||||
|
const { benutzer } = await rechtErforderlich("einstellungenVerwalten");
|
||||||
|
await vorlageArchivieren(textfeld(formular, "id") ?? "", akteurVon(benutzer));
|
||||||
|
} catch (fehler) {
|
||||||
|
return alsZustand(fehler);
|
||||||
|
}
|
||||||
|
revalidatePath("/vorlagen");
|
||||||
|
redirect("/vorlagen");
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
import { vorlagePruefen, VorlagenFehler, type VorlageEingabe } from "@/lib/vorlagen";
|
||||||
|
import { protokollieren } from "@/server/services/protokoll";
|
||||||
|
|
||||||
|
/** Vorlagen anlegen, bearbeiten, archivieren (#019). Eingebaute lassen sich nur bearbeiten. */
|
||||||
|
type Akteur = { readonly id: string; readonly name: string };
|
||||||
|
|
||||||
|
function daten(eingabe: VorlageEingabe) {
|
||||||
|
const v = vorlagePruefen(eingabe);
|
||||||
|
return {
|
||||||
|
name: v.name.trim(),
|
||||||
|
beschreibung: v.beschreibung?.trim() || null,
|
||||||
|
schluesselTyp: v.schluesselTyp,
|
||||||
|
kategorie: v.kategorie,
|
||||||
|
standardLaufzeitTage: v.standardLaufzeitTage ?? null,
|
||||||
|
zielbenutzer: v.zielbenutzer,
|
||||||
|
zielbenutzerAnlegen: v.zielbenutzerAnlegen,
|
||||||
|
sudoModus: v.sudoModus,
|
||||||
|
sudoBefehle: v.sudoModus === "BEFEHLE" ? v.sudoBefehle.map((b) => b.trim().replace(/\s+/g, " ")) : [],
|
||||||
|
authorizedKeysOptionen: v.authorizedKeysOptionen?.trim() || null,
|
||||||
|
sshConfigAlias: v.sshConfigAlias?.trim() || null,
|
||||||
|
knownHostsEintragen: v.knownHostsEintragen,
|
||||||
|
privatNachAusrollenLoeschen: v.privatNachAusrollenLoeschen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function eindeutig(fehler: unknown): never {
|
||||||
|
if (fehler instanceof Prisma.PrismaClientKnownRequestError && fehler.code === "P2002") {
|
||||||
|
throw new VorlagenFehler("Eine Vorlage mit diesem Namen gibt es bereits.", "name");
|
||||||
|
}
|
||||||
|
throw fehler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vorlageAnlegen(eingabe: VorlageEingabe, akteur: Akteur): Promise<string> {
|
||||||
|
const werte = daten(eingabe);
|
||||||
|
const vorlage = await prisma.vorlage.create({ data: werte }).catch(eindeutig);
|
||||||
|
await protokollieren({
|
||||||
|
akteur,
|
||||||
|
aktion: "vorlage.angelegt",
|
||||||
|
ergebnis: "ERFOLG",
|
||||||
|
details: { name: werte.name },
|
||||||
|
});
|
||||||
|
return vorlage.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vorlageBearbeiten(id: string, eingabe: VorlageEingabe, akteur: Akteur): Promise<void> {
|
||||||
|
const werte = daten(eingabe);
|
||||||
|
const alt = await prisma.vorlage.findUnique({ where: { id } });
|
||||||
|
if (!alt || alt.archiviertAm) throw new VorlagenFehler("Diese Vorlage gibt es nicht (mehr).");
|
||||||
|
await prisma.vorlage.update({ where: { id }, data: werte }).catch(eindeutig);
|
||||||
|
const geaendert = Object.entries(werte)
|
||||||
|
.filter(([feld, wert]) => JSON.stringify(alt[feld as keyof typeof alt]) !== JSON.stringify(wert))
|
||||||
|
.map(([feld]) => feld);
|
||||||
|
await protokollieren({
|
||||||
|
akteur,
|
||||||
|
aktion: "vorlage.geaendert",
|
||||||
|
ergebnis: "ERFOLG",
|
||||||
|
details: { name: werte.name, geaendert },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vorlageArchivieren(id: string, akteur: Akteur): Promise<void> {
|
||||||
|
const vorlage = await prisma.vorlage.findUnique({ where: { id } });
|
||||||
|
if (!vorlage || vorlage.archiviertAm) throw new VorlagenFehler("Diese Vorlage gibt es nicht (mehr).");
|
||||||
|
if (vorlage.eingebaut) throw new VorlagenFehler("Eingebaute Vorlagen lassen sich nicht archivieren.");
|
||||||
|
await prisma.vorlage.update({ where: { id }, data: { archiviertAm: new Date() } });
|
||||||
|
await protokollieren({
|
||||||
|
akteur,
|
||||||
|
aktion: "vorlage.archiviert",
|
||||||
|
ergebnis: "ERFOLG",
|
||||||
|
details: { name: vorlage.name },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { EINGEBAUTE_VORLAGEN } from "../prisma/vorlagen";
|
||||||
|
import {
|
||||||
|
VorlagenFehler,
|
||||||
|
istIpAdresse,
|
||||||
|
platzhalterErsetzen,
|
||||||
|
vorlageAufloesen,
|
||||||
|
vorlagePruefen,
|
||||||
|
type VorlageEingabe,
|
||||||
|
} from "@/lib/vorlagen";
|
||||||
|
|
||||||
|
/** Vorlagen und Platzhalter (#019). */
|
||||||
|
|
||||||
|
const BASIS: VorlageEingabe = {
|
||||||
|
name: "Test",
|
||||||
|
schluesselTyp: "ED25519",
|
||||||
|
kategorie: "DIENST",
|
||||||
|
zielbenutzer: "deploy",
|
||||||
|
zielbenutzerAnlegen: true,
|
||||||
|
sudoModus: "KEINER",
|
||||||
|
sudoBefehle: [],
|
||||||
|
knownHostsEintragen: true,
|
||||||
|
privatNachAusrollenLoeschen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Platzhalter", () => {
|
||||||
|
it("ersetzt bekannte Platzhalter", () => {
|
||||||
|
expect(platzhalterErsetzen('from="{{quellHostIp}}"', { quellHostIp: "10.0.0.5" })).toBe(
|
||||||
|
'from="10.0.0.5"',
|
||||||
|
);
|
||||||
|
expect(platzhalterErsetzen("deploy-{{ zielHost }}", { zielHost: "srv-app01" })).toBe("deploy-srv-app01");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verweigert unbekannte, fehlende und gefaehrliche Werte", () => {
|
||||||
|
expect(() => platzhalterErsetzen("{{irgendwas}}", {})).toThrow(VorlagenFehler);
|
||||||
|
expect(() => platzhalterErsetzen("{{quellHostIp}}", {})).toThrow(/fehlt ein Wert/);
|
||||||
|
expect(() =>
|
||||||
|
platzhalterErsetzen('from="{{quellHostIp}}"', { quellHostIp: '1.2.3.4",command="id' }),
|
||||||
|
).toThrow(VorlagenFehler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("erkennt IP-Adressen", () => {
|
||||||
|
expect(istIpAdresse("10.0.0.5")).toBe(true);
|
||||||
|
expect(istIpAdresse("10.0.0.0/24")).toBe(true);
|
||||||
|
expect(istIpAdresse("fe80::1")).toBe(true);
|
||||||
|
expect(istIpAdresse("srv-app01")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Vorlage pruefen", () => {
|
||||||
|
it("nimmt alle eingebauten Vorlagen an", () => {
|
||||||
|
for (const vorlage of EINGEBAUTE_VORLAGEN) {
|
||||||
|
expect(() =>
|
||||||
|
vorlagePruefen({
|
||||||
|
...BASIS,
|
||||||
|
...vorlage,
|
||||||
|
beschreibung: vorlage.beschreibung ?? null,
|
||||||
|
kategorie: vorlage.kategorie as "PERSON" | "DIENST",
|
||||||
|
schluesselTyp: (vorlage.schluesselTyp ?? "ED25519") as "ED25519",
|
||||||
|
sudoBefehle: [],
|
||||||
|
zielbenutzerAnlegen: vorlage.zielbenutzerAnlegen ?? false,
|
||||||
|
sudoModus: (vorlage.sudoModus ?? "KEINER") as "KEINER",
|
||||||
|
knownHostsEintragen: vorlage.knownHostsEintragen ?? true,
|
||||||
|
privatNachAusrollenLoeschen: false,
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ zielbenutzer: "Root" }, "zielbenutzer"],
|
||||||
|
[{ authorizedKeysOptionen: 'from="{{unbekannt}}"' }, "authorizedKeysOptionen"],
|
||||||
|
[{ authorizedKeysOptionen: "restriktiv" }, "authorizedKeysOptionen"],
|
||||||
|
[{ sudoModus: "BEFEHLE" as const, sudoBefehle: [] }, "sudoBefehle"],
|
||||||
|
[{ sudoModus: "BEFEHLE" as const, sudoBefehle: ["systemctl restart x"] }, "sudoBefehle"],
|
||||||
|
[{ zielbenutzerAnlegen: false, sudoModus: "VOLL" as const }, "sudoModus"],
|
||||||
|
[{ sshConfigAlias: "a b {{zielHost}}" }, "sshConfigAlias"],
|
||||||
|
[{ kategorie: "PERSON" as const }, "kategorie"],
|
||||||
|
])("weist ab: %o", (fehler, feld) => {
|
||||||
|
try {
|
||||||
|
vorlagePruefen({ ...BASIS, ...fehler });
|
||||||
|
throw new Error("kein Fehler");
|
||||||
|
} catch (fehlerObjekt) {
|
||||||
|
expect(fehlerObjekt).toBeInstanceOf(VorlagenFehler);
|
||||||
|
expect((fehlerObjekt as VorlagenFehler).feld).toBe(feld);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Vorlage aufloesen", () => {
|
||||||
|
it("liefert Optionen, Alias und Ablaufdatum", () => {
|
||||||
|
const jetzt = new Date("2026-10-12T10:00:00Z");
|
||||||
|
expect(
|
||||||
|
vorlageAufloesen(
|
||||||
|
{
|
||||||
|
zielbenutzer: "deploy",
|
||||||
|
authorizedKeysOptionen: 'from="{{quellHostIp}}"',
|
||||||
|
sshConfigAlias: "deploy-{{zielHost}}",
|
||||||
|
standardLaufzeitTage: 90,
|
||||||
|
},
|
||||||
|
{ quellHostIp: "10.0.0.5", zielHost: "srv-app01" },
|
||||||
|
jetzt,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
zielbenutzer: "deploy",
|
||||||
|
authorizedKeysOptionen: 'from="10.0.0.5"',
|
||||||
|
sshConfigAlias: "deploy-srv-app01",
|
||||||
|
ablaufAm: new Date("2027-01-10T10:00:00Z"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user