Gegenbuch: Dienstkonto statt Token, .env mit gueltigen Werten

Blocker behoben: Ich hatte ein dauerhaftes API-Token vorausgesetzt - das gibt
es in OpenCRM nicht. Zugangstoken leben 15 Minuten, der Gegenbuch-Container
waere nach dem ersten Durchlauf gestorben. Aufgefallen erst durch die Frage des
Betreibers, woher er den Token nimmt.

Loesung: Das Gegenbuch meldet sich bei jedem Lauf selbst an, mit einem eigenen
Benutzerkonto, dessen Rolle ausschliesslich audit:read traegt. Damit kann es
nur Pruefwerte lesen - keine Kundendaten, keine Aenderungen. CRM_TOKEN bleibt
fuer Tests moeglich, ist aber nicht mehr der Normalweg. Fehlerfaelle (falsches
Passwort, Anmelde-Bremse, CRM nicht erreichbar) werden unterschieden und im
Klartext gemeldet.

.env.example nennt jetzt zu jedem Schalter die gueltigen Werte - bisher liess
sich nur raten, ob es prod oder production heisst. Einschliesslich des Falls
"erst nur Staging testen, Prod spaeter dazunehmen".

Beide READMEs um "Zugang einrichten" ergaenzt: Rolle mit nur audit:read,
Benutzer damit, Zugangsdaten in die .env. Mit dem Hinweis, dass jede Anmeldung
im Audit-Log erscheint - gewollt, denn so faellt auch auf, wenn das Gegenbuch
aufhoert zu arbeiten.

Verifiziert gegen eine Attrappe, die wie das echte CRM eine Anmeldung verlangt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 19:31:46 +02:00
co-authored by Claude Opus 5
parent 89d617ab70
commit 1eb65809ec
7 changed files with 167 additions and 26 deletions
+59 -3
View File
@@ -132,8 +132,20 @@ if (!SIGN && process.env.NOTARY_INSECURE_ACK !== 'mir-ist-klar-dass-das-ungeschu
);
process.exit(1);
}
if (!CRM_URL || !CRM_TOKEN) {
console.error('CRM_URL und CRM_TOKEN müssen gesetzt sein.');
const CRM_EMAIL = process.env.CRM_EMAIL;
const CRM_PASSWORD = process.env.CRM_PASSWORD;
if (!CRM_URL) {
console.error('CRM_URL muss gesetzt sein.');
process.exit(1);
}
if (!CRM_TOKEN && !(CRM_EMAIL && CRM_PASSWORD)) {
console.error(
'Es fehlt der Zugang zum CRM.\n' +
'Entweder CRM_EMAIL und CRM_PASSWORD eines Dienstkontos setzen (empfohlen –\n' +
'das Gegenbuch meldet sich dann bei jedem Lauf selbst an), oder ein bereits\n' +
'vorhandenes CRM_TOKEN mitgeben (läuft nach 15 Minuten ab, nur für Tests).',
);
process.exit(1);
}
@@ -180,13 +192,56 @@ if (SIGN && !PIN) {
}
}
// Access-Tokens leben nur 15 Minuten – ein dauerhaft hinterlegtes Token waere
// beim naechsten stuendlichen Lauf laengst abgelaufen. Deshalb meldet sich das
// Gegenbuch mit einem eigenen Dienstkonto an und holt sich pro Lauf ein
// frisches. Das Konto braucht ausschliesslich die Berechtigung `audit:read`.
let zugangsToken = CRM_TOKEN || null;
async function anmelden() {
let r;
try {
r = await fetch(`${CRM_URL}/api/auth/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: CRM_EMAIL, password: CRM_PASSWORD }),
});
} catch (e) {
console.error(
`Das CRM ist nicht erreichbar (${CRM_URL}).\n` +
` Grund: ${e instanceof Error ? e.message : String(e)}`,
);
process.exit(1);
}
if (r.status === 401) {
console.error(
'Anmeldung am CRM abgelehnt – E-Mail oder Passwort des Dienstkontos stimmen nicht.',
);
process.exit(1);
}
if (r.status === 429) {
console.error('Das CRM hat die Anmeldung wegen zu vieler Versuche gebremst. Später erneut.');
process.exit(1);
}
if (!r.ok) {
console.error(`Anmeldung am CRM fehlgeschlagen: HTTP ${r.status}`);
process.exit(1);
}
const j = await r.json();
if (!j.success || !j.data?.token) {
console.error(`Anmeldung am CRM fehlgeschlagen: ${j.error || 'kein Token in der Antwort'}`);
process.exit(1);
}
zugangsToken = j.data.token;
}
async function hole(pfad) {
// Fehler werden hier zu einer erklaerenden Zeile – frueher flog ein
// Node-Stacktrace hoch, also genau die kryptische erste Zeile, die fuer
// git-Meldungen schon abgestellt war.
let r;
try {
r = await fetch(`${CRM_URL}${pfad}`, { headers: { Authorization: `Bearer ${CRM_TOKEN}` } });
r = await fetch(`${CRM_URL}${pfad}`, { headers: { Authorization: `Bearer ${zugangsToken}` } });
} catch (e) {
console.error(
`Das CRM ist nicht erreichbar (${CRM_URL}).\n` +
@@ -672,6 +727,7 @@ if (Number.isFinite(MIN_SEQ) && bisher.length < MIN_SEQ) {
// 4) Abgleich mit dem CRM, VOR dem Anhaengen.
// ---------------------------------------------------------------------------
const letzter = bisher[bisher.length - 1];
if (!CRM_TOKEN) await anmelden();
const aktuell = await hole('/api/audit-logs/checkpoint');
if (letzter) {