Merge branch 'phase/030-gitea-abgleich-2'

This commit is contained in:
2026-10-12 17:13:18 +02:00
4 changed files with 71 additions and 12 deletions
+6 -4
View File
@@ -1,19 +1,21 @@
# #030 Issues nach Gitea übertragen # #030 Issues nach Gitea übertragen
**Status:** offen **Status:** erledigt
**Meilenstein:** v0.1.0 **Meilenstein:** v0.1.0
**Labels:** chore **Labels:** chore
**Geschätzt:** 1 h **Tatsächlich:** – **Geschätzt:** 1 h **Tatsächlich:** 0 h 10 min
## Ziel ## Ziel
Issue-Dateien, Labels und Meilensteine in Gitea anlegen (wartet auf Zugangsdaten). Issue-Dateien, Labels und Meilensteine in Gitea anlegen (wartet auf Zugangsdaten).
## Aufgaben ## Aufgaben
- [ ] `scripts/gitea-issues.ts` - [x] `scripts/gitea-issues.ts`
## Akzeptanzkriterien ## Akzeptanzkriterien
- [ ] wird zu Beginn der Phase verfeinert - [x] wird zu Beginn der Phase verfeinert
## Abhängigkeiten ## Abhängigkeiten
– –
## Notizen / Entscheidungen ## Notizen / Entscheidungen
- Repo `florian.wilde/sshkeyvault` auf git.wilde-server.de (von Florian angelegt), `main` und Tags v0.1.0–v0.5.0 gepusht.
- `pnpm issues:sync` liest Adresse und Repo aus dem Git-Remote und den Token aus den Git-Zugangsdaten; die Issue-Dateien bleiben die Quelle der Wahrheit.
+25
View File
@@ -0,0 +1,25 @@
# #031 Rotation des Deployment-Keys
**Status:** offen
**Meilenstein:** v1.0.0
**Labels:** sicherheit, backend
**Geschätzt:** 3 h **Tatsächlich:** –
## Ziel
Den Deployment-Key der Instanz geführt tauschen – regelmäßig oder im Notfall (sshkv kompromittiert, siehe `docs/betrieb/sicherheit.md`).
## Aufgaben
- [ ] Neuen Deployment-Key erzeugen, ohne den alten abzulösen (zweiter Instanz-Key im Übergang)
- [ ] Auf jedem eingerichteten Host den neuen Key neben den alten in den verwalteten Block des Anmeldebenutzers schreiben (Dry-Run, Ausrollen)
- [ ] Verbindung mit dem neuen Key je Host prüfen
- [ ] Erst wenn alle Hosts den neuen Key akzeptieren: alten aus allen Blöcken entfernen, alten Key verwerfen
- [ ] Nicht erreichbare Hosts sichtbar machen; Abschluss erst, wenn alle umgestellt sind (oder bewusst übergangen)
- [ ] Nur Admin mit frischer Bestätigung, alles protokolliert
## Akzeptanzkriterien
- [ ] Integrationstest: nach der Rotation meldet sich sshkv nur noch mit dem neuen Key an, der alte steht auf keinem Host mehr
- [ ] Aussperrschutz: zu keinem Zeitpunkt steht ein Host ohne funktionierenden Deployment-Key da
## Abhängigkeiten
#016, #021
## Notizen / Entscheidungen
+22
View File
@@ -0,0 +1,22 @@
# #032 Nacharbeiten aus Phase 4/5
**Status:** offen
**Meilenstein:** v1.0.0
**Labels:** test, chore
**Geschätzt:** 2 h **Tatsächlich:** –
## Ziel
Kleinere offene Punkte aus den Berichten der Phasen 4 und 5 erledigen.
## Aufgaben
- [ ] Massenwiderruf im Browser testen (E2E mit frischer Bestätigung über den Mock-OIDC)
- [ ] Laufende Rotation abbrechen können (Nachfolger widerrufen, Vorgänger wieder aktiv)
- [ ] Protokoll-IDs ohne Zweierschritte (Spalten-Default und Trigger ziehen beide `nextval`)
- [ ] „Zum Dry-Run“ aus der Zuweisungsmatrix direkt zur Dry-Run-Karte springen (Anker)
## Akzeptanzkriterien
- [ ] Alle Suiten grün
## Abhängigkeiten
#023, #024, #025, #027
## Notizen / Entscheidungen
+18 -8
View File
@@ -76,14 +76,15 @@ class Gitea {
constructor( constructor(
private readonly basis: string, private readonly basis: string,
private readonly repo: string, private readonly repo: string,
private readonly token: string, /** Fertiger Wert fuer den Authorization-Header (`token …` oder `Basic …`). */
private readonly autorisierung: string,
) {} ) {}
private async anfrage<T>(pfad: string, optionen: RequestInit = {}): Promise<T> { private async anfrage<T>(pfad: string, optionen: RequestInit = {}): Promise<T> {
const antwort = await fetch(`${this.basis}/api/v1/repos/${this.repo}${pfad}`, { const antwort = await fetch(`${this.basis}/api/v1/repos/${this.repo}${pfad}`, {
...optionen, ...optionen,
headers: { headers: {
Authorization: `token ${this.token}`, Authorization: this.autorisierung,
"Content-Type": "application/json", "Content-Type": "application/json",
...optionen.headers, ...optionen.headers,
}, },
@@ -149,10 +150,12 @@ function ausRemote(): { basis: string; repo: string } | null {
} }
/** /**
* Holt den Token aus den Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`). * Holt die Zugangsdaten aus Git (Schlüsselbund oder `~/.git-credentials`), damit sie nicht
* So genügt ein einziger Token für Push und API – er muss nicht zusätzlich in der .env stehen. * zusätzlich in der .env stehen müssen. Hinterlegt ist dort ein API-Token oder – wie bei
* sshkeyvault – das Konto-Passwort; Gitea nimmt beides an, das Passwort aber nur per Basic-Auth
* (#030).
*/ */
function tokenAusGitCredentials(basis: string): string | null { function zugangAusGitCredentials(basis: string): { benutzer: string; geheimnis: string } | null {
const host = new URL(basis).host; const host = new URL(basis).host;
// Erst die eingerichteten Helfer (z. B. Schlüsselbund), dann zusätzlich die Datei // Erst die eingerichteten Helfer (z. B. Schlüsselbund), dann zusätzlich die Datei
@@ -167,7 +170,8 @@ function tokenAusGitCredentials(basis: string): string | null {
stdio: ["pipe", "pipe", "ignore"], stdio: ["pipe", "pipe", "ignore"],
}); });
const passwort = /^password=(.*)$/m.exec(antwort); const passwort = /^password=(.*)$/m.exec(antwort);
if (passwort?.[1]) return passwort[1]; const benutzer = /^username=(.*)$/m.exec(antwort);
if (passwort?.[1]) return { benutzer: benutzer?.[1] ?? "", geheimnis: passwort[1] };
} catch { } catch {
// Nächste Möglichkeit versuchen. // Nächste Möglichkeit versuchen.
} }
@@ -186,8 +190,14 @@ async function main() {
const remote = ausRemote(); const remote = ausRemote();
const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, ""); const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, "");
const repo = process.env.GITEA_REPO ?? remote?.repo ?? ""; const repo = process.env.GITEA_REPO ?? remote?.repo ?? "";
const token = process.env.GITEA_TOKEN || (basis ? (tokenAusGitCredentials(basis) ?? "") : ""); const zugang = process.env.GITEA_TOKEN ? null : basis ? zugangAusGitCredentials(basis) : null;
const token = process.env.GITEA_TOKEN || zugang?.geheimnis || "";
const tokenQuelle = process.env.GITEA_TOKEN ? "Umgebungsvariable" : "Git-Zugangsdaten"; const tokenQuelle = process.env.GITEA_TOKEN ? "Umgebungsvariable" : "Git-Zugangsdaten";
// Ein Gitea-Token sind 40 Hex-Zeichen; alles andere ist ein Passwort und geht per Basic-Auth.
const autorisierung =
process.env.GITEA_TOKEN || /^[0-9a-f]{40}$/.test(token) || !zugang?.benutzer
? `token ${token}`
: `Basic ${Buffer.from(`${zugang.benutzer}:${token}`).toString("base64")}`;
const verzeichnis = path.join(process.cwd(), "docs", "issues"); const verzeichnis = path.join(process.cwd(), "docs", "issues");
const issues = await dateienLesen(verzeichnis); const issues = await dateienLesen(verzeichnis);
@@ -215,7 +225,7 @@ async function main() {
} }
console.log(`Ziel: ${basis}/${repo} (Token aus: ${tokenQuelle})`); console.log(`Ziel: ${basis}/${repo} (Token aus: ${tokenQuelle})`);
const gitea = new Gitea(basis, repo, token); const gitea = new Gitea(basis, repo, autorisierung);
const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id])); const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id]));
const benoetigteLabels = [...new Set(issues.flatMap((issue) => issue.labels))]; const benoetigteLabels = [...new Set(issues.flatMap((issue) => issue.labels))];