import { execFileSync } from 'child_process';
|
import * as fs from 'fs';
|
import * as path from 'path';
|
import { GEHEIMNIS_ORDNER } from './datenbank';
|
|
/**
|
* Bau-Geheimnisse (API-Schlüssel, Tokens …) nach einheitlichem Muster.
|
*
|
* Standard — bewusst wie bei gängigen Bau-Systemen, damit die Bibliothek nichts
|
* über einzelne Tools wissen muss:
|
*
|
* 1. Das Repository deklariert in `software-bibliothek.json`, welche Geheimnisse
|
* es braucht: "secrets": [{ "name": "ANTHROPIC_API_KEY", "description": "…" }]
|
* 2. Der Wert wird in der Bibliothek hinterlegt (nur lokal am Server pflegbar).
|
* 3. Beim Bau wird er als **Umgebungsvariable** gesetzt. Was das Tool daraus macht
|
* — Datei erzeugen, direkt einkompilieren — entscheidet sein eigenes Bau-Skript.
|
*
|
* Ablage getrennt vom Katalog und NIE im Software-Datensatz: `/api/software` ist aus
|
* dem Netzwerk lesbar. Auch der Ordner `secrets/` wird nirgends ausgeliefert —
|
* die Download-Route bedient ausschließlich `files/<id>/<datei>`.
|
*/
|
export interface Geheimniswert {
|
/** Name der Umgebungsvariablen, z. B. `ANTHROPIC_API_KEY`. */
|
name: string;
|
wert: string;
|
}
|
|
/** Was die Oberfläche zu sehen bekommt — nie der Wert selbst. */
|
export interface GeheimnisUebersicht {
|
name: string;
|
hinterlegt: boolean;
|
zeichen: number;
|
}
|
|
function ablagePfad(softwareId: number): string {
|
return path.join(GEHEIMNIS_ORDNER, `${softwareId}.json`);
|
}
|
|
/**
|
* Schränkt die Zugriffsrechte auf den Geheimnis-Ordner ein.
|
*
|
* Nötig, weil Unterordner von %ProgramData% die Leseberechtigung für die
|
* Gruppe „Benutzer" erben — sonst könnte jeder lokal angemeldete Benutzer die
|
* hinterlegten Schlüssel lesen. Node's `mode` greift unter Windows nicht auf
|
* ACLs, deshalb über `icacls`. Bezeichnung über die bekannten SIDs, damit es
|
* unabhängig von der Systemsprache funktioniert.
|
*/
|
export function sichereGeheimnisOrdner(): void {
|
if (process.platform !== 'win32') {
|
return;
|
}
|
const dienstbenutzer = `${process.env.USERDOMAIN ?? ''}\\${process.env.USERNAME ?? ''}`;
|
const rechte = [
|
'*S-1-5-18:(OI)(CI)F', // SYSTEM
|
'*S-1-5-32-544:(OI)(CI)F', // Administratoren
|
];
|
if (process.env.USERNAME) {
|
rechte.push(`${dienstbenutzer}:(OI)(CI)F`); // der Benutzer, unter dem der Dienst läuft
|
}
|
try {
|
execFileSync('icacls', [GEHEIMNIS_ORDNER, '/inheritance:r', '/grant:r', ...rechte], { stdio: 'pipe', windowsHide: true });
|
} catch (fehler) {
|
console.warn(`Zugriffsrechte des Schlüssel-Ordners konnten nicht eingeschränkt werden: ${String(fehler).slice(0, 200)}`);
|
}
|
}
|
|
export function ladeGeheimnisse(softwareId: number): Geheimniswert[] {
|
const pfad = ablagePfad(softwareId);
|
if (!fs.existsSync(pfad)) {
|
return [];
|
}
|
try {
|
const gelesen = JSON.parse(fs.readFileSync(pfad, 'utf8')) as Geheimniswert[];
|
return Array.isArray(gelesen) ? gelesen.filter((eintrag) => istGueltigerName(eintrag?.name)) : [];
|
} catch {
|
return [];
|
}
|
}
|
|
/** Nur Namen und Länge — der Wert verlässt den Server nicht. */
|
export function uebersichtGeheimnisse(softwareId: number): GeheimnisUebersicht[] {
|
return ladeGeheimnisse(softwareId).map((eintrag) => ({
|
name: eintrag.name,
|
hinterlegt: eintrag.wert.length > 0,
|
zeichen: eintrag.wert.length,
|
}));
|
}
|
|
/**
|
* Speichert die Werte. Ein leerer Wert bedeutet „unverändert lassen" — so kann die
|
* Oberfläche die Liste zurückschicken, ohne die Geheimnisse je gesehen zu haben.
|
*/
|
export function speichereGeheimnisse(softwareId: number, eingehende: Geheimniswert[]): GeheimnisUebersicht[] {
|
const bisherige = new Map(ladeGeheimnisse(softwareId).map((eintrag) => [eintrag.name, eintrag.wert]));
|
const zusammengefuehrt: Geheimniswert[] = [];
|
for (const eintrag of eingehende) {
|
const name = (eintrag?.name ?? '').trim();
|
if (!istGueltigerName(name)) {
|
continue;
|
}
|
const neuerWert = String(eintrag.wert ?? '');
|
zusammengefuehrt.push({ name, wert: neuerWert || bisherige.get(name) || '' });
|
}
|
fs.mkdirSync(GEHEIMNIS_ORDNER, { recursive: true });
|
fs.writeFileSync(ablagePfad(softwareId), JSON.stringify(zusammengefuehrt, null, 2), { mode: 0o600 });
|
return zusammengefuehrt.map((eintrag) => ({ name: eintrag.name, hinterlegt: eintrag.wert.length > 0, zeichen: eintrag.wert.length }));
|
}
|
|
export function loescheGeheimnisse(softwareId: number): void {
|
fs.rmSync(ablagePfad(softwareId), { force: true });
|
}
|
|
/** Umgebungsvariablen für den Bau — nur Einträge mit hinterlegtem Wert. */
|
export function alsUmgebung(geheimnisse: Geheimniswert[]): Record<string, string> {
|
const umgebung: Record<string, string> = {};
|
for (const eintrag of geheimnisse) {
|
if (eintrag.wert) {
|
umgebung[eintrag.name] = eintrag.wert;
|
}
|
}
|
return umgebung;
|
}
|
|
/** Nur was als Umgebungsvariable taugt — schützt zugleich vor Einschleusen. */
|
function istGueltigerName(name: string | undefined): boolean {
|
return !!name && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
}
|