Benutzer hinterlegen ihre öffentlichen Schlüssel einmal, Admins zusätzlich Team-Schlüssel. Sie kommen in jeden Gast: im Assistenten vorausgewählt und abwählbar, per API und CLI automatisch, wenn die Anfrage keine nennt. Die Schlüssel kommen über die Schnittstelle SchluesselQuelle – dort lässt sich später eine externe Verwaltung anbinden. Vorgezogen aus #015 auf die Frage, ob Schlüssel ins Template gehören: nein, in die Verwaltung. Refs #015 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { z } from "@/lib/validierung";
|
|
import type { Aufrufer } from "@/server/auth/zugang";
|
|
import { darf } from "@/server/auth/rechte";
|
|
import { AnwendungsFehler, keinRecht, nichtGefunden } from "@/server/fehler";
|
|
import { protokollieren } from "@/server/services/protokoll";
|
|
import { lokaleQuelle } from "@/server/schluessel/lokal";
|
|
import {
|
|
fingerabdruck,
|
|
istOeffentlicherSchluessel,
|
|
type GastSchluessel,
|
|
type SchluesselQuelle,
|
|
} from "@/server/schluessel/quelle";
|
|
|
|
/** Alle angebundenen Quellen. Eine externe Verwaltung kommt hier dazu. */
|
|
export function schluesselQuellen(): SchluesselQuelle[] {
|
|
return [lokaleQuelle];
|
|
}
|
|
|
|
export async function schluesselFuer(benutzer: {
|
|
id: string;
|
|
benutzername: string;
|
|
}): Promise<GastSchluessel[]> {
|
|
const listen = await Promise.all(schluesselQuellen().map((q) => q.schluesselFuer(benutzer)));
|
|
// Derselbe Schlüssel aus zwei Quellen nur einmal.
|
|
const gesehen = new Set<string>();
|
|
return listen.flat().filter((s) => !gesehen.has(s.fingerabdruck) && gesehen.add(s.fingerabdruck));
|
|
}
|
|
|
|
const Eingabe = z.object({
|
|
name: z.string().trim().min(1, "Name fehlt.").max(80),
|
|
oeffentlich: z
|
|
.string()
|
|
.trim()
|
|
.refine(
|
|
istOeffentlicherSchluessel,
|
|
"Kein öffentlicher SSH-Schlüssel (ssh-ed25519 …, ssh-rsa …, ecdsa-sha2-… ). Den .pub-Inhalt einfügen, nie den privaten Schlüssel.",
|
|
),
|
|
team: z.boolean().default(false),
|
|
});
|
|
|
|
export async function schluesselAnlegen(
|
|
roh: unknown,
|
|
aufrufer: Aufrufer,
|
|
ip: string | null,
|
|
): Promise<GastSchluessel> {
|
|
const eingabe = Eingabe.parse(roh);
|
|
if (eingabe.team && !darf(aufrufer.benutzer.rolle, "benutzerVerwalten")) throw keinRecht();
|
|
const abdruck = fingerabdruck(eingabe.oeffentlich);
|
|
const benutzerId = eingabe.team ? null : aufrufer.benutzer.id;
|
|
if (await prisma.sshSchluessel.findFirst({ where: { benutzerId, fingerabdruck: abdruck } })) {
|
|
throw new AnwendungsFehler(409, "vergeben", "Diesen Schlüssel gibt es hier schon.");
|
|
}
|
|
const zeile = await prisma.$transaction(async (tx) => {
|
|
const neu = await tx.sshSchluessel.create({
|
|
data: { benutzerId, name: eingabe.name, oeffentlich: eingabe.oeffentlich, fingerabdruck: abdruck },
|
|
});
|
|
await protokollieren(
|
|
{
|
|
benutzerId: aufrufer.benutzer.id,
|
|
quelle: aufrufer.quelle,
|
|
aktion: eingabe.team ? "Team-Schlüssel hinzugefügt" : "SSH-Schlüssel hinzugefügt",
|
|
ziel: `schluessel:${abdruck}`,
|
|
details: { name: eingabe.name },
|
|
ip,
|
|
},
|
|
tx,
|
|
);
|
|
return neu;
|
|
});
|
|
return {
|
|
id: zeile.id,
|
|
name: zeile.name,
|
|
oeffentlich: zeile.oeffentlich,
|
|
fingerabdruck: abdruck,
|
|
bereich: benutzerId ? "persoenlich" : "team",
|
|
quelle: "lokal",
|
|
};
|
|
}
|
|
|
|
export async function schluesselLoeschen(id: string, aufrufer: Aufrufer, ip: string | null): Promise<void> {
|
|
const zeile = await prisma.sshSchluessel.findUnique({ where: { id } });
|
|
if (!zeile) throw nichtGefunden("Den Schlüssel");
|
|
const eigener = zeile.benutzerId === aufrufer.benutzer.id;
|
|
if (!eigener && !(zeile.benutzerId === null && darf(aufrufer.benutzer.rolle, "benutzerVerwalten")))
|
|
throw keinRecht();
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.sshSchluessel.delete({ where: { id } });
|
|
await protokollieren(
|
|
{
|
|
benutzerId: aufrufer.benutzer.id,
|
|
quelle: aufrufer.quelle,
|
|
aktion: zeile.benutzerId ? "SSH-Schlüssel entfernt" : "Team-Schlüssel entfernt",
|
|
ziel: `schluessel:${zeile.fingerabdruck}`,
|
|
ip,
|
|
},
|
|
tx,
|
|
);
|
|
});
|
|
}
|