feat: marketplace modules and isolated container management
This commit is contained in:
@@ -23,7 +23,7 @@ Die Plattform darf **niemals** von einem einzelnen Modul abhängig sein. Wird ei
|
||||
Internet
|
||||
│
|
||||
▼ HTTPS (Produktion; lokal :8080)
|
||||
Docker Container "mpm-platform" (Benutzer: app, kein Root)
|
||||
Docker Container "mpm-platform" (Supervisor mit engen Capabilities)
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Supervisor (Prozessmanager, Crash-Recovery) │
|
||||
│ │ │
|
||||
@@ -40,10 +40,13 @@ persistentes Volume "postgres-data")
|
||||
|
||||
Grundsätze:
|
||||
|
||||
- **Ein** Applikationscontainer; Module laufen als interne Prozesse darin (kein Docker-in-Docker, kein Docker-Socket).
|
||||
- MPM bleibt der Managementcontainer. Neue Module laufen in eigenen Compose-Stacks mit optionalen Datenbankservices.
|
||||
- Nur der MPM-Backendprozess erhält Zugriff auf den Docker-Socket; Modulcontainer erhalten ihn nie.
|
||||
- Docker-Socket-Zugriff entspricht weitreichender Kontrolle über den Docker-Host. Installationsrechte müssen deshalb vertrauenswürdigen Administratoren vorbehalten bleiben.
|
||||
- Interne Ports (z. B. 3000, 41001+) werden **nie** nach außen veröffentlicht; nur Nginx lauscht auf 8080.
|
||||
- PostgreSQL liegt außerhalb des Applikationscontainers → Container-Neustarts/Updates zerstören keine Nutzdaten.
|
||||
- Alle Prozesse laufen als unprivilegierter Benutzer `app`.
|
||||
- Supervisor und Nginx-Master erhalten nur die Container-Capabilities `SETUID`, `SETGID` und `KILL`.
|
||||
- Backend und Nginx-Worker laufen als `app`; Modulservices werden mit `no-new-privileges` gestartet und intern über ein eigenes Gateway-Netz geroutet.
|
||||
|
||||
## 3. Prozessmodell
|
||||
|
||||
@@ -63,7 +66,7 @@ Stürzt ein Modulprozess ab, startet Supervisor ihn automatisch neu – die Mana
|
||||
|
||||
```
|
||||
Browser ──POST /api/v1/auth/login──▶ Nginx ──▶ NestJS
|
||||
│ Validierung (Zod) + Rate Limit + Lockout + Argon2id
|
||||
│ Validierung (Zod) + IP-Rate-Limit + Argon2id
|
||||
│ Session in PostgreSQL anlegen (Token nur gehasht gespeichert)
|
||||
◀── Set-Cookie: mpm_session (HttpOnly, SameSite=Lax)
|
||||
Set-Cookie: mpm_csrf (lesbar für CSRF-Doppel-Submit)
|
||||
@@ -97,7 +100,7 @@ Alle Plattform-Tabellen liegen im Standard-Schema (ab Phase 3 Auslagerung in ein
|
||||
| Tabelle | Zweck |
|
||||
|---|---|
|
||||
| `roles` | Globale Rollen: `ADMIN`, `USER` |
|
||||
| `users` | Benutzer mit Argon2id-Hash, Rollen-FK, Lockout-Feldern |
|
||||
| `users` | Benutzer mit Argon2id-Hash, Rollen-FK und Fehlversuchs-Zähler |
|
||||
| `sessions` | Serverseitige Sessions (Token SHA-256-gehasht, CSRF-Token, Gleitende Verlängerung) |
|
||||
| `audit_logs` | Zentrales Audit (LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, …) |
|
||||
| `schema_migrations` | Angewandte Migrationen (eigener Runner mit Advisory-Lock) |
|
||||
@@ -110,7 +113,7 @@ Geplant (Phase 3+): `modules`, `user_module_permissions`, `module_settings`, `sy
|
||||
- **Serverseitige Sessions**: 256-Bit-Zufalls-Token im `HttpOnly`-Cookie; in der DB wird nur der SHA-256-Hash gespeichert. `Secure` (produktiv), `SameSite=Lax`, kurze Laufzeit (`SESSION_TTL_MINUTES`), gleitende Verlängerung.
|
||||
- **CSRF-Schutz**: Doppel-Submit – Server speichert pro Session ein CSRF-Token; bei jedem zustandsändernden Request muss der Header `X-CSRF-Token` (konstantzeitvergleich) übereinstimmen.
|
||||
- **Rate Limiting**: In-Memory-Fenster pro IP für Login (Standard: 10 Versuche / 5 Minuten).
|
||||
- **Account Lockout**: Nach `LOGIN_MAX_ATTEMPTS` Fehlversuchen wird das Konto für `LOGIN_LOCKOUT_MINUTES` gesperrt.
|
||||
- **Kein Account Lockout**: Fehlversuche sperren das Zielkonto nicht, damit Dritte keine gezielte Konto-DoS auslösen können.
|
||||
- **Keine User-Enumeration**: Unbekannter Benutzer, inaktiver Benutzer und falsches Passwort liefern dieselbe generische 401-Meldung.
|
||||
- **Audit-Log**: Jeder Login-Versuch (Erfolg/Misserfolg mit Grund), Logout.
|
||||
|
||||
@@ -210,10 +213,10 @@ Minimal-API je Modul (Phase 6): `GET /health`, `GET /api/manifest`, `GET /api/me
|
||||
|
||||
| # | Entscheidung | Begründung |
|
||||
|---|---|---|
|
||||
| 1 | Serverseitige Sessions statt reiner JWTs | Widerrufbar (Logout, Deaktivierung), kein Token-Diebstahl-Risiko, einfache Lockout-Logik; JWT/Access-Tokens für die Modul-Kommunikation ab Phase 6 |
|
||||
| 1 | Serverseitige Sessions statt reiner JWTs | Widerrufbar (Logout, Deaktivierung); Modulidentität wird über kurzlebige, signierte Gateway-Assertions übergeben |
|
||||
| 2 | Opaque 256-Bit-Token, DB speichert nur SHA-256-Hash | DB-Leck kompromittiert keine Sessions |
|
||||
| 3 | PostgreSQL außerhalb des Applikationscontainers | Persistenz bei Container-Updates, klare Trennung von Zustand und Compute |
|
||||
| 4 | Supervisor statt systemd im Container | Container-Standard, verwaltet mehrere Prozesse ohne Root, Crash-Recovery |
|
||||
| 4 | Supervisor statt systemd im Container | Restricted-capability supervisor starts least-privilege services and monitors crashes |
|
||||
| 5 | Nginx im Container als einziger öffentlicher Endpunkt | Zentrales Routing, Security-Header, interne Ports bleiben verborgen |
|
||||
| 6 | `pg` + eigener Migration-Runner statt ORM | Wenig Abhängigkeiten, volle SQL-Kontrolle, Migrationen transaktionssicher mit Advisory-Lock |
|
||||
| 7 | Zod statt class-validator | Ein Validierungs-Framework für Frontend und Backend, TypeScript-Inferenz |
|
||||
@@ -224,4 +227,4 @@ Minimal-API je Modul (Phase 6): `GET /health`, `GET /api/manifest`, `GET /api/me
|
||||
- Schichten: `components/ui` (Design-System) → `components/layout` (App-Shell) → `features/*` (Fachseiten) → `pages` (Fehlerseiten).
|
||||
- Datenzugriff ausschließlich über `lib/api-client` (CSRF-Header, 401-Behandlung) und TanStack Query.
|
||||
- Routing: öffentliche Login-Seite, geschützter Bereich (`RequireAuth`), Admin-Bereich (`RequireAdmin`, RBAC im Frontend nur UX – erzwungen wird serverseitig).
|
||||
- Responsiv: Sidebar wird auf Mobilgeräten zur Overlay-Navigation.
|
||||
- Responsiv: Sidebar wird auf Mobilgeräten zur Overlay-Navigation.
|
||||
|
||||
64
docs/MODULE-MARKETPLACE.md
Normal file
64
docs/MODULE-MARKETPLACE.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Modul-Marketplace: Verbindungen und Katalog
|
||||
|
||||
## Zielbild
|
||||
|
||||
Die Modulverwaltung bietet neben installierten Modulen einen Marketplace. Administratoren verbinden dort GitHub oder eine Gitea-/Forgejo-Instanz, wählen ausdrücklich Repositories als Katalogquellen aus und installieren veröffentlichte Modul-Releases per Klick.
|
||||
|
||||
```text
|
||||
OAuth-Verbindung -> Repository auswählen -> Release-Katalog -> Paket prüfen -> Installieren
|
||||
```
|
||||
|
||||
## Verbindungen
|
||||
|
||||
- OAuth Authorization Code mit `state` und PKCE (S256).
|
||||
- Tokens bleiben ausschließlich im Backend, werden verschlüsselt gespeichert, nie in API-Antworten aufgenommen und bei jeder Verwendung nur mit minimal notwendigen Leserechten eingesetzt.
|
||||
- Verbindung, Katalogquelle und installierte Module sind getrennte Datensätze. Trennen einer Verbindung entfernt keine installierten Module.
|
||||
- Selbst gehostete Gitea-/Forgejo-Instanzen benötigen eine OAuth-Anwendung auf der jeweiligen Instanz. Callback-URL ist die öffentliche MPM-URL plus `/api/v1/marketplace/oauth/<anbieter>/callback`.
|
||||
- OAuth-Client-Secrets und Verschlüsselungsschlüssel gehören in die Serverkonfiguration bzw. einen Secret Store, nie in das Frontend oder Repository.
|
||||
|
||||
## Katalog und Installation
|
||||
|
||||
- Der Katalog lädt öffentliche Repositories verbundener Forge-Konten. Installiert wird der aktuelle Stand des jeweiligen Standard-Branches.
|
||||
- MPM lädt das vom Forge erzeugte Quellarchiv serverseitig, entfernt den Archiv-Stammordner und erwartet `module.json` im Repository-Stamm.
|
||||
|
||||
## Container-Vertrag für Module
|
||||
|
||||
Installierbare Module müssen neben `module.json` eine Compose-Datei und einen App-Service enthalten:
|
||||
|
||||
```json
|
||||
{
|
||||
"composeFile": "compose.yml",
|
||||
"appService": "app"
|
||||
}
|
||||
```
|
||||
|
||||
Der App-Service muss den Manifest-Port im Container bereitstellen (`expose`, kein `ports`) und auf `0.0.0.0` lauschen. Datenbanken gehören als weitere Services in dieselbe Compose-Datei. Die Dienste teilen ein privates Compose-Netz; nur der App-Service wird zusätzlich an das MPM-Gateway angeschlossen. Für SQLite kann der App-Service `/var/lib/mpm-module` als persistenten Speicher unter `MPM_MODULE_DATA_DIR` verwenden. Datenbankcontainer definieren eigene projektlokale named volumes.
|
||||
|
||||
MPM startet/stoppt den gesamten Stack gemeinsam. Beim Entfernen löscht Compose alle App- und Datenbankcontainer samt Projekt-Netzwerk; benannte Datenvolumes bleiben standardmäßig erhalten, damit ein Entfernen der App keine Daten vernichtet. Pakete dürfen keine Host-Ports, Host-Verzeichnisse, externen Docker-Ressourcen, privilegierten Optionen oder Docker-Socket-Mounts anfordern. Die Modulverwaltung benötigt Zugriff auf den Docker-Socket des Hosts; deshalb dürfen nur vertrauenswürdige Administratoren Module installieren.
|
||||
- Vor der Installation prüft MPM Downloadgröße, Archivpfade, Symlinks, Manifest und Modul-ID. Die bestehende `ModuleInstaller`-Validierung bleibt die letzte Instanz.
|
||||
- Der Browser übermittelt keine Download-URL; MPM erstellt sie aus Anbieter, Besitzer, Repository und Standard-Branch.
|
||||
- Die Installation registriert das Modul. Das Starten bleibt ein separater Lifecycle-Schritt und erfolgt erst nach Bestätigung durch den Administrator.
|
||||
- Verbindungen, Quellrepository, Release-Tag und Prüfsumme werden im Audit-Log erfasst; Secrets und Tokens niemals.
|
||||
|
||||
## GitHub-Rechte
|
||||
|
||||
Für den ersten Verbindungs-Test wird eine GitHub OAuth App mit `read:user` verwendet. Das erlaubt MPM, das autorisierte Konto anzuzeigen; öffentliche Release-Kataloge können separat unauthentifiziert gelesen werden. Private GitHub-Repositories werden in dieser ersten Phase nicht abgerufen. Dafür soll später eine GitHub App mit `Contents: read` eingesetzt werden: OAuth Apps bieten für Quellcode keinen reinen Read-only-Scope und der klassische `repo`-Scope umfasst Schreibzugriff.
|
||||
|
||||
Gitea und Forgejo verwenden kompatible Release- und Repository-APIs, aber jede selbst gehostete Instanz bleibt eine eigene OAuth-Konfiguration und eine explizit vertrauenswürdige Quelle.
|
||||
|
||||
## Umsetzungsschritte
|
||||
|
||||
1. OAuth-Provider-Konfiguration und verschlüsselte Zugangsdaten samt Datenbankmigration.
|
||||
2. OAuth-Start/Callback mit zufälligem, einmalig verwendbarem `state`, Ablaufzeit, PKCE und Bindung an die anmeldende Admin-Session.
|
||||
3. Verbindungen anzeigen und trennen; niemals Tokens zurückgeben.
|
||||
4. Öffentliche Repositories verbundener Forge-Konten direkt als installierbaren Katalog anzeigen.
|
||||
5. Repository-Archive vom Standard-Branch laden und vor dem Installieren sicher normalisieren.
|
||||
6. Installation in die bestehende Modulregistrierung integrieren, auditieren und im UI anzeigen. Module starten nach der Installation nicht automatisch.
|
||||
|
||||
## Aktueller Umfang
|
||||
|
||||
Der Marketplace in der Modulverwaltung unterstützt öffentliche Repositories von GitHub, Gitea und Forgejo. Private Repositories, Suche über fremde Katalogserver und Updates installierter Module sind noch nicht enthalten.
|
||||
|
||||
## Voraussetzungen für den Betrieb
|
||||
|
||||
Für die OAuth-Anmeldung müssen je Provider/Instanz Client-ID und Client-Secret in MPM hinterlegt werden. Die öffentliche HTTPS-URL der Plattform muss stabil sein, da sie als OAuth-Callback registriert wird. Vor Aktivierung privater GitHub-Repositories ist die GitHub-App mit Read-only-Berechtigung zu konfigurieren.
|
||||
@@ -26,7 +26,7 @@ Definition of Done:
|
||||
- [x] PostgreSQL mit persistentem Volume
|
||||
- [x] Migrationen (eigener Runner mit Advisory-Lock) + Seed (Rollen, Admin)
|
||||
- [x] Login / Logout (Argon2id, serverseitige Sessions, HttpOnly-Cookies)
|
||||
- [x] CSRF-Schutz, Rate Limiting, Account Lockout
|
||||
- [x] CSRF-Schutz und IP-basiertes Rate Limiting
|
||||
- [x] User-Modell & Rollen (ADMIN/USER, RBAC-Guards)
|
||||
- [x] Audit-Log (LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT)
|
||||
- [x] Health-Endpoint (`/api/v1/health`)
|
||||
@@ -58,7 +58,7 @@ Definition of Done: Modul-Datenmodell, Manifest, Installation, Registrierung, St
|
||||
- [x] `modules`-Tabelle (Migration 002) mit Lifecycle-Status und eindeutigen Ports/Slugs
|
||||
- [x] Manifest-Vertrag `module.json` (Zod: ID, Name, Version, Slug, Runtime, Entrypoint, Port 41000–41999, Healthcheck, apiVersion)
|
||||
- [x] ZIP-Installer mit Manifest-Validierung, Größenlimit (10 MB) und **Zip-Slip-Schutz**
|
||||
- [x] Modul-Prozess-Manager: Start/Stop (SIGTERM→SIGKILL) als Kindprozesse, minimale ENV (keine Plattform-Secrets), eigene Log-Dateien
|
||||
- [x] Modul-Lifecycle: Compose-Containerstacks pro Modul, Datenbanken als separate Services und persistente Named Volumes
|
||||
- [x] Healthcheck-Service mit Startup-Grace (10 Retries × 500 ms)
|
||||
- [x] Lifecycle: INSTALLED → STARTING → RUNNING → STOPPING → STOPPED, ERROR, DISABLED
|
||||
- [x] `GET/POST /api/v1/modules`, `POST :id/start|stop|restart`, `PATCH :id/enabled`, `GET :id/health`, `DELETE :id` (nur ADMIN)
|
||||
@@ -139,4 +139,4 @@ Definition of Done: Modulübersicht, Benutzerverwaltung, Rechteverwaltung, Syste
|
||||
## Nächste Schritte
|
||||
|
||||
- Einbindung bestehender Projekte als Module (ersetzt Phase 7; Infrastruktur steht seit Phase 3–8)
|
||||
- Phase 10 – Security Hardening: Dependency/Container-Scans, HSTS, Backup/Restore, Pen-Tests
|
||||
- Phase 10 – Security Hardening: Dependency/Container-Scans, HSTS, Backup/Restore, Pen-Tests
|
||||
|
||||
Reference in New Issue
Block a user