Files
mpm/docs/ARCHITECTURE.md
2026-10-10 15:51:30 +02:00

232 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MPM – Architekturdokumentation
## 1. Leitentscheidung
Die Management-Plattform ist ein **Application Host und Gateway für Module** – nicht selbst die Fachanwendung.
```
Management-Plattform Module (ab Phase 3)
├── Identität & Login ├── eigene Oberfläche
├── Benutzer & Rollen ├── eigene Business-Logik
├── Rechte (plattformweit) ├── eigene API
├── Modul-Verwaltung ├── eigene Daten (eigenes Schema)
├── Routing / Gateway ├── eigene Migrationen
├── API & Administration └── optional eigene Modulrechte
└── Audit & Monitoring
```
Die Plattform darf **niemals** von einem einzelnen Modul abhängig sein. Wird ein Modul entfernt, bleiben Login, Benutzerverwaltung, Administration, Rechteverwaltung und alle anderen Module voll funktionsfähig.
## 2. Komponenten & Container-Layout
```
Internet
│
▼ HTTPS (Produktion; lokal :8080)
Docker Container "mpm-platform" (Supervisor mit engen Capabilities)
┌──────────────────────────────────────────────┐
│ Supervisor (Prozessmanager, Crash-Recovery) │
│ │ │
│ ├── nginx Reverse Proxy :8080 │
│ ├── platform-backend NestJS API 127.0.0.1:3000│
│ └── [ab Phase 3: Modul-Prozesse, │
│ z. B. Kalender 127.0.0.1:41001] │
└──────────────────────────────────────────────┘
│
▼
PostgreSQL "mpm-postgres" (eigener Container,
persistentes Volume "postgres-data")
```
Grundsätze:
- 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.
- 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
Supervisor verwaltet Start/Stop/Restart, Crash-Recovery und Logging aller Prozesse:
| Prozess | Adresse | Aufgabe |
|---|---|---|
| nginx | 0.0.0.0:8080 | Reverse Proxy, statisches Frontend, Security-Header |
| platform-backend | 127.0.0.1:3000 | Management-API `/api/v1` |
| Modul-Prozesse (ab Phase 3) | 127.0.0.1:41001+ | Fachanwendungen |
Stürzt ein Modulprozess ab, startet Supervisor ihn automatisch neu – die Management-Plattform läuft weiter.
## 4. Request-Flow
### Login
```
Browser ──POST /api/v1/auth/login──▶ Nginx ──▶ NestJS
│ 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)
```
### Authentifizierter Request
```
Browser ──▶ Nginx ──▶ SessionGuard (Session gültig? User aktiv?)
├── CsrfGuard (bei POST/PATCH/DELETE: X-CSRF-Token)
├── RolesGuard (RBAC: ADMIN/USER)
└── Controller/Service (Business-Logik)
```
### Modul-Routing (ab Phase 4, geplant)
```
Browser ──▶ MPM-Host (/api/v1/auth/module-open/slug)
└── Einmal-Ticket ──▶ eigener Modul-Host (/<slug>) ──▶ Management-Gateway
├── User identifizieren (Session)
├── Permission Check (user_module_permissions)
├── DENIED → 403
└── ALLOWED → Modul-Gateway → Modulprozess
```
Ein Modul vertraut **niemals** allein auf die URL; die Plattform übergibt die Identität sicher an das Modul (Modul-API-Vertrag, Phase 6). Der Modulhost bedient keine Management-API. Ein kurzlebiges, einmalig verwendbares Ticket stellt dort eine an die Plattformsession gebundene Modulsession aus. Modulpfade für Assets und API-Aufrufe müssen unter `/<slug>/` liegen; root-relative `/api/` ist auf dem Modulhost gesperrt. In Produktion verwenden Plattform-Cookies den `__Host-`-Präfix.
## 5. Datenmodell (Phase 1)
Alle Plattform-Tabellen liegen im Standard-Schema (ab Phase 3 Auslagerung in ein `management`-Schema und pro Modul ein eigenes Schema).
| Tabelle | Zweck |
|---|---|
| `roles` | Globale Rollen: `ADMIN`, `USER` |
| `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) |
Geplant (Phase 3+): `modules`, `user_module_permissions`, `module_settings`, `system_settings`, `api_clients`.
## 6. Authentifizierung & Sessions
- **Argon2id** (19 MiB, t=2, p=1 – OWASP-Empfehlung) für Passwort-Hashing; niemals Klartextpasswörter.
- **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).
- **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.
## 7. Sicherheitsmaßnahmen (Phase 1)
- Security-Header via Helmet (Backend) und Nginx (Frontend): CSP, `X-Frame-Options: DENY`, `X-Content-Type-Options`, `Referrer-Policy`.
- Zod-Validierung **aller** Eingaben (serverseitig, clientseitige Validierung ist nur UX).
- SQL nur parametrisiert (`pg`), keine String-Konkatenation.
- Strukturierte Fehlermeldungen ohne Stack-Traces nach außen.
- Secrets ausschließlich über Umgebungsvariablen (`.env` ist gitignored).
- Interne Ports nicht veröffentlicht; PostgreSQL nur auf Loopback des Hosts gemappt.
- Unprivilegierter Container-Benutzer; kein Root in der Laufzeit.
Security Hardening (Penetrationstests, Dependency/Container-Scans, HSTS, Backup/Restore) ist gemäß Arbeitsplan Phase 10.
## 8. API-Übersicht (Phase 1)
| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| POST | `/api/v1/auth/login` | Public | Anmeldung, setzt Session-Cookies |
| POST | `/api/v1/auth/logout` | Session | Beendet die aktuelle Session |
| GET | `/api/v1/auth/me` | Session | Angemeldeter Benutzer (Id, Rolle, …) |
| GET | `/api/v1/health` | Public | Systemstatus (Backend, DB-Latenz, Version, Uptime) |
| GET | `/api/v1/users` | Admin | Alle Benutzer |
| POST | `/api/v1/users` | Admin | Benutzer anlegen (409 bei Duplikat) |
| GET | `/api/v1/users/:id` | Admin | Einzelner Benutzer |
| PATCH | `/api/v1/users/:id` | Admin | Bearbeiten / Aktivieren / Deaktivieren |
| PATCH | `/api/v1/users/:id/password` | Admin | Passwort zurücksetzen |
| DELETE | `/api/v1/users/:id` | Admin | Benutzer löschen |
| GET | `/api/v1/profile` | Session | Eigenes Profil |
| PATCH | `/api/v1/profile/password` | Session | Eigenes Passwort ändern |
| GET | `/api/v1/modules` | Admin | Alle Module |
| POST | `/api/v1/modules/install` | Admin | ZIP-Paket installieren (multipart, Feld `package`) |
| GET | `/api/v1/modules/:id` | Admin | Einzelnes Modul |
| POST | `/api/v1/modules/:id/start` | Admin | Modul starten |
| POST | `/api/v1/modules/:id/stop` | Admin | Modul stoppen |
| POST | `/api/v1/modules/:id/restart` | Admin | Modul neu starten |
| PATCH | `/api/v1/modules/:id/enabled` | Admin | Modul aktivieren/deaktivieren |
| GET | `/api/v1/modules/:id/health` | Admin | Healthcheck ausführen |
| DELETE | `/api/v1/modules/:id` | Admin | Modul entfernen |
OpenAPI/Swagger UI: `/api/docs` · Ab Phase 4: dynamisches Routing `/slug`.
### Schutzregeln der Benutzerverwaltung
- Keine Selbst-Deaktivierung und keine Selbst-Löschung (400)
- Der letzte aktive Administrator kann nicht herabgestuft, deaktiviert oder gelöscht werden (400)
- Deaktivierte Benutzer werden sofort von allen Sessions abgemeldet
- Passwort-Reset (Admin) meldet den Benutzer von allen Sessions ab
- Eigenes Passwort ändern meldet alle übrigen Sessions ab (aktuelle bleibt aktiv)
## 9. Modul-System (Phase 3)
### Manifest-Vertrag
Jedes Modul-Paket (ZIP, max. 10 MB) enthält ein `module.json`:
```json
{
"id": "calendar",
"name": "Kalender",
"version": "1.0.0",
"slug": "kalender-tool",
"description": "Kalenderverwaltung",
"author": "…",
"runtime": "node",
"entrypoint": "server.js",
"port": 41001,
"healthcheck": "/health",
"apiVersion": "v1"
}
```
Validierung per Zod: ID/Slug-Muster, SemVer, Port 41000–41999, kein Pfad-Traversal im Entrypoint, Runtime `node`, apiVersion `v1`.
### Lifecycle
```
INSTALLED → STARTING → RUNNING → STOPPING → STOPPED
↓ ↓
ERROR ERROR DISABLED (via Enable/Disable)
```
### Sicherheitsmaßnahmen
- **Zip-Slip-Schutz**: Alle ZIP-Einträge müssen im Zielverzeichnis bleiben
- **Minimale Prozess-ENV**: Module erhalten nur `PATH`, `NODE_ENV`, `PORT` – keine Plattform-Secrets
- **Spawn ohne Shell**: keine Command-Injection möglich
- **Eigene Log-Dateien** pro Modul (`/app/data/logs/module-<id>.log`)
- **Startup-Grace**: Healthcheck mit 10 Retries, bevor ein Modul als ERROR gilt
- **Isolation**: Ein abgestürztes Modul (ERROR) beeinträchtigt weder Plattform noch andere Module
- Persistente Volumes: Modul-Dateien und Logs überleben Container-Updates
Minimal-API je Modul (Phase 6): `GET /health`, `GET /api/manifest`, `GET /api/me`. Details zum Paketformat: [`modules/README.md`](../modules/README.md).
## 10. Architektur-Entscheidungen (ADR-Kurzform)
| # | Entscheidung | Begründung |
|---|---|---|
| 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 | 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 |
| 8 | Monorepo mit unabhängigen Apps (kein Workspace-Tooling) | Einfache, cache-freundliche Docker-Builds, keine Tool-Lock-in |
## 11. Frontend-Architektur
- 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.