Ein Durchgang fuer den Kern-Anwendungsfall: Vorlage und Zweck, Host A, Host B mit Zielbenutzer, Zusammenfassung, dann Schluessel erzeugen, Benutzer anlegen, zuweisen, Dry-Run, ausrollen und Verbindung testen. Die Vorlage Deployment-Zugang fuellt deploy, Ablaufdatum, from= und Alias vor; bei root als Ziel warnt der Assistent ausdruecklich. Die Ausfuehrung ist wiederaufnehmbar: wird mittendrin eine frische Bestaetigung fuer das Zielkonto noetig, geschieht sie in einem neuen Tab, und nichts wird doppelt angelegt. Zielkonten laufen als eigene Vorgaenge (neue Migration) und merken sich, ob sshkv den Benutzer angelegt hat - nur dann loescht ein Widerruf ihn spaeter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { deploymentKeySicherstellen } from "@/worker/geheimnisse";
|
|
import { hostVerbinden } from "@/worker/ssh/host";
|
|
import { befehlAusfuehren } from "@/worker/ssh/verbindung";
|
|
import { zielkontoAnwenden, zielkontoEntfernen } from "@/worker/ssh/zielkonto";
|
|
import { datenbankLeeren } from "../hilfen/db";
|
|
import { eingerichteterHost, masterKeyAnlegen } from "./hilfen/host";
|
|
import { befehlAlsRoot, zielB, zielBereit } from "./hilfen/ziele";
|
|
|
|
/** Zielkonten und sudoers gegen ein echtes sshd (#018). */
|
|
|
|
let aufraeumen: () => void;
|
|
beforeAll(async () => {
|
|
await zielBereit(zielB);
|
|
aufraeumen = masterKeyAnlegen();
|
|
});
|
|
afterAll(() => aufraeumen());
|
|
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
await deploymentKeySicherstellen();
|
|
await befehlAlsRoot(
|
|
zielB,
|
|
"userdel -r sshkv 2>/dev/null; userdel -r deploy 2>/dev/null; rm -f /etc/sudoers.d/sshkv /etc/sudoers.d/sshkv-deploy; true",
|
|
);
|
|
});
|
|
|
|
async function verbunden() {
|
|
const host = await eingerichteterHost(zielB, "ziel-b");
|
|
return { host, verbindung: await hostVerbinden(host) };
|
|
}
|
|
|
|
describe("Zielkonto", () => {
|
|
it("legt den Benutzer an und erlaubt genau die freigegebenen Befehle", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
const erstes = await zielkontoAnwenden(verbindung, host, {
|
|
benutzer: "deploy",
|
|
modus: "BEFEHLE",
|
|
befehle: ["/usr/bin/id -un"],
|
|
});
|
|
expect(erstes.angelegt).toBe(true);
|
|
expect(
|
|
(
|
|
await zielkontoAnwenden(verbindung, host, {
|
|
benutzer: "deploy",
|
|
modus: "BEFEHLE",
|
|
befehle: ["/usr/bin/id -un"],
|
|
})
|
|
).angelegt,
|
|
).toBe(false);
|
|
const erlaubt = await befehlAusfuehren(
|
|
verbindung,
|
|
"sudo -n runuser -u deploy -- sudo -n /usr/bin/id -un",
|
|
);
|
|
expect(erlaubt.stdout.trim()).toBe("root");
|
|
const verboten = await befehlAusfuehren(
|
|
verbindung,
|
|
"sudo -n runuser -u deploy -- sudo -n /usr/bin/whoami",
|
|
);
|
|
expect(verboten.code).not.toBe(0);
|
|
const rechte = await befehlAlsRoot(
|
|
zielB,
|
|
"stat -c '%a %U' /etc/sudoers.d/sshkv-deploy /home/deploy/.ssh",
|
|
);
|
|
expect(rechte.stdout).toBe("440 root\n700 deploy\n");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("wechselt auf vollen Zugriff und wieder auf keinen", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" });
|
|
expect(
|
|
(await befehlAusfuehren(verbindung, "sudo -n runuser -u deploy -- sudo -n whoami")).stdout.trim(),
|
|
).toBe("root");
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "KEINER" });
|
|
expect(
|
|
(
|
|
await befehlAlsRoot(zielB, "test -e /etc/sudoers.d/sshkv-deploy && echo da || echo weg")
|
|
).stdout.trim(),
|
|
).toBe("weg");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("ueberschreibt keine fremde sudoers-Datei", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
await befehlAlsRoot(
|
|
zielB,
|
|
"echo 'deploy ALL=(ALL) ALL' > /etc/sudoers.d/sshkv-deploy && chmod 440 /etc/sudoers.d/sshkv-deploy",
|
|
);
|
|
try {
|
|
await expect(
|
|
zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" }),
|
|
).rejects.toThrow(/nicht von sshkv/);
|
|
expect((await befehlAlsRoot(zielB, "cat /etc/sudoers.d/sshkv-deploy")).stdout).toBe(
|
|
"deploy ALL=(ALL) ALL\n",
|
|
);
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("entfernt sudoers und Benutzer beim Widerruf", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" });
|
|
await zielkontoEntfernen(verbindung, host, "deploy", true);
|
|
const rest = await befehlAlsRoot(zielB, "id deploy 2>&1; ls /etc/sudoers.d/");
|
|
expect(rest.stdout).toMatch(/no such user/);
|
|
expect(rest.stdout).not.toContain("sshkv-deploy");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Zielkonto als Vorgang", () => {
|
|
it("legt an, merkt sich die Herkunft und entfernt den Benutzer wieder", async () => {
|
|
const { prisma } = await import("@/lib/db");
|
|
const { vorgangLaufen } = await import("./hilfen/host");
|
|
const host = await eingerichteterHost(zielB, "ziel-b");
|
|
const anlegen = await vorgangLaufen("ZIELKONTO_ANLEGEN", {
|
|
hostId: host.id,
|
|
eingabe: { benutzer: "deploy", modus: "VOLL", befehle: [] },
|
|
});
|
|
expect(anlegen.fehler).toBeNull();
|
|
const konto = await prisma.zielkonto.findFirstOrThrow({ where: { hostId: host.id } });
|
|
expect(konto).toMatchObject({ benutzername: "deploy", vonSshkvAngelegt: true, sudoModus: "VOLL" });
|
|
expect(konto.sudoersInhalt).toContain("deploy ALL=(ALL:ALL) NOPASSWD: ALL");
|
|
|
|
const entfernen = await vorgangLaufen("ZIELKONTO_ENTFERNEN", {
|
|
hostId: host.id,
|
|
eingabe: { benutzer: "deploy" },
|
|
});
|
|
expect(entfernen.fehler).toBeNull();
|
|
expect((await befehlAlsRoot(zielB, "id deploy 2>&1")).stdout).toMatch(/no such user/);
|
|
expect(
|
|
(await prisma.zielkonto.findFirstOrThrow({ where: { hostId: host.id } })).entferntAm,
|
|
).not.toBeNull();
|
|
});
|
|
});
|