ec9c77956d
Sieben Aufgaben in einem Plan: Tracer (PVE/Token end-to-end), Ticket-Zugang mit Fehler-Klartext und Beobachtungs-Riegel, PBS/PMG nachsichtig auswerten, Hintergrundabfrage je Mandant plus Verbindungstest, Einstellungsseite, Modulseite, Doku und Nachmessung. Ausgangswerte der Tore gemessen: api 77/1240, web 82/693, type-check 4/4, lint 5/5, Biome-web genau 53 Warnungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
911 lines
60 KiB
Markdown
911 lines
60 KiB
Markdown
---
|
|
phase: quick-260923-dhh
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
autonomous: true
|
|
requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11]
|
|
|
|
files_modified:
|
|
- apps/api/prisma/schema.prisma
|
|
- apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql
|
|
- apps/api/src/app.module.ts
|
|
- apps/api/src/proxmox/proxmox.types.ts
|
|
- apps/api/src/proxmox/proxmox-auth.ts
|
|
- apps/api/src/proxmox/proxmox-normalize.ts
|
|
- apps/api/src/proxmox/proxmox-client.service.ts
|
|
- apps/api/src/proxmox/proxmox.service.ts
|
|
- apps/api/src/proxmox/proxmox-scheduler.service.ts
|
|
- apps/api/src/proxmox/proxmox.controller.ts
|
|
- apps/api/src/proxmox/proxmox.module.ts
|
|
- apps/api/src/proxmox/proxmox.seed.ts
|
|
- apps/api/src/proxmox/dto/proxmox-server.dto.ts
|
|
- apps/api/src/proxmox/proxmox-client.service.spec.ts
|
|
- apps/api/src/proxmox/proxmox.service.spec.ts
|
|
- apps/api/src/proxmox/proxmox-normalize.spec.ts
|
|
- apps/api/src/proxmox/proxmox-scheduler.service.spec.ts
|
|
- apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
|
|
- apps/api/src/prisma/rls-access-inventory.spec.ts
|
|
- apps/web/src/lib/proxmox-api.ts
|
|
- apps/web/src/lib/module-loader.ts
|
|
- apps/web/src/app/(portal)/modules/proxmox/layout.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/page.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx
|
|
- apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx
|
|
- apps/web/src/messages/de.json
|
|
- apps/web/src/messages/en.json
|
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
|
- docs/anleitung-entwicklung.md
|
|
- docs/anwenderhandbuch.md
|
|
|
|
user_setup:
|
|
- service: proxmox
|
|
why: "Nur der Nutzer hat echte PVE-/PBS-/PMG-Server. Ohne sie bleibt die Feldnamen-Annahme A2/A3 der Recherche unbestaetigt."
|
|
dashboard_config:
|
|
- task: "Je Produkt einen NUR-LESE-Zugang anlegen: PVE-Rolle PVEAuditor, PBS-Rolle Audit bzw. DatastoreAudit, PMG-Rolle Auditor"
|
|
location: "Proxmox-Oberflaeche -> Datacenter/Configuration -> Permissions"
|
|
|
|
estimate:
|
|
tokens: 320000
|
|
raw_tokens: 210000
|
|
tasks: 7
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Kein Weg im gesamten Modul veraendert etwas bei Proxmox: die einzige Nicht-GET-Anfrage im ganzen Modul ist die Ticket-Anmeldung, und sie wird maschinell nachgezaehlt (D-01)."
|
|
- "Ein Administrator legt in den Einstellungen Server an (Name, Typ pve|pbs|pmg, Adresse, Zugang) und sieht die Zugangsdaten nie wieder im Klartext (D-02)."
|
|
- "PVE und PBS bieten API-Token ODER Benutzer/Passwort; PMG bietet nur Benutzer/Passwort — die Token-Felder erscheinen bei PMG gar nicht und ein Token-Zugang fuer PMG wird serverseitig abgelehnt (D-03)."
|
|
- "Zertifikatsfehler werden nur fuer die Server geduldet, bei denen der Administrator es einzeln eingeschaltet hat; Voreinstellung ist pruefen (D-04)."
|
|
- "Die Modulseite und jede Anzeige lesen ausschliesslich aus dem Zwischenlager, nie live bei Proxmox (D-05)."
|
|
- "Der Knopf Verbindung testen nennt die Ursache in Alltagssprache: nicht erreichbar, Zugang abgelehnt, Rechte reichen nicht, Zertifikat, unerwartete Antwort (D-06)."
|
|
- "Ein fehlendes, anders benanntes oder falsch typisiertes Feld einer Proxmox-Antwort fuehrt zu unbekannt in der Anzeige, nie zu einem Absturz, einer leeren Seite oder einem stillen Falschwert."
|
|
- "Ohne angelegten Server ist die Modulseite ruhig und erklaert, dass noch keiner eingetragen ist."
|
|
- "Beide neuen Tabellen tragen tenantId mit RLS-Policy; rls-coverage.spec.ts und rls-access-inventory.spec.ts bleiben gruen (D-08)."
|
|
artifacts:
|
|
- apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql
|
|
- apps/api/src/proxmox/proxmox-auth.ts
|
|
- apps/api/src/proxmox/proxmox-client.service.ts
|
|
- apps/api/src/proxmox/proxmox-normalize.ts
|
|
- apps/api/src/proxmox/proxmox-scheduler.service.ts
|
|
- apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
|
|
- "apps/web/src/app/(portal)/modules/proxmox/page.tsx"
|
|
- "apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx"
|
|
key_links:
|
|
- "proxmox-auth.ts ist die EINZIGE Stelle, die Kopfzeilen und Anmelde-Cookies je Produkt baut — Klient, Verbindungstest und Planer rufen sie, keiner baut sie nach (D-03)."
|
|
- "proxmox-client.service.ts baut den undici-Dispatcher je Aufruf aus dem Feld tlsRejectUnauthorized genau dieser Serverzeile (D-04)."
|
|
- "proxmox-scheduler.service.ts haengt an onApplicationBootstrap und faechert je Mandant auf; der Controller zieht nach jedem Speichern nach (D-05)."
|
|
- "proxmox.controller.ts traegt @UseModule('proxmox'); die Schreibwege zusaetzlich @Roles(ADMIN, SUPER_ADMIN) (D-09)."
|
|
- "Jeder Datenbankzugriff laeuft ueber forTenant(); nur der Startpfad des Planers ueber forSystem() und steht in FORSYSTEM_ALLOWED_CALL_SITES (D-08)."
|
|
---
|
|
|
|
<objective>
|
|
Das Proxmox-Modul anbinden: PVE, PBS und PMG **nur beobachten**. Der Administrator legt in
|
|
den Moduleinstellungen beliebig viele Server an (Name, Typ, Adresse, Zugang — verschluesselt
|
|
gespeichert). Ein Hintergrunddienst fragt sie periodisch ab und legt die Messwerte in einem
|
|
Zwischenlager ab. Die Modulseite zeigt die Serverliste mit Auslastung und liest dabei
|
|
ausschliesslich aus dem Zwischenlager.
|
|
|
|
Purpose: Der Nutzer sieht den Zustand seiner Proxmox-Landschaft in Tessera, ohne die
|
|
Proxmox-Oberflaechen einzeln zu oeffnen — und ohne dass Tessera je etwas an ihnen aendern kann.
|
|
|
|
Output: Ein vollstaendiges Modul `proxmox` (Datenbank, Dienst, API, Hintergrundabfrage,
|
|
Einstellungsseite, Modulseite, Dokumentation), sieben eigenstaendige Commits.
|
|
|
|
## Herkunft der Entscheidungen (D-Nummern)
|
|
|
|
Die D-Nummern in diesem Plan verweisen auf die elf bereits getroffenen Entscheidungen aus dem
|
|
Auftrag (`<decisions_already_made>`), in derselben Reihenfolge:
|
|
|
|
| ID | Entscheidung |
|
|
|---|---|
|
|
| D-01 | Nur beobachten — kein veraendernder Weg gegen Proxmox |
|
|
| D-02 | Administrator legt Server an; Zugangsdaten verschluesselt, nie im Klartext zurueck |
|
|
| D-03 | PVE/PBS: Token oder Benutzer/Passwort; PMG nur Benutzer/Passwort; Kopfzeilen aus EINER Stelle |
|
|
| D-04 | Zertifikatsfehler nur je Server umschaltbar dulden, nie global |
|
|
| D-05 | Zwischenlager statt Live-Abfrage; Planer nach TENDER-Muster (`onApplicationBootstrap`) |
|
|
| D-06 | Knopf „Verbindung testen" mit Klartext-Ursache |
|
|
| D-07 | Keine neue npm-Abhaengigkeit — `undici` ist bereits da |
|
|
| D-08 | Mandantentrennung Pflicht: `tenantId` + RLS + Klassifikationsdoku + gruene Waechter-Tests |
|
|
| D-09 | Zugriff ueber die normale Modulfreigabe |
|
|
| D-10 | Oberflaechentexte Deutsch in der Sie-Form ueber next-intl; Kommentare Deutsch |
|
|
| D-11 | Dashboard-Kachel ist NICHT in diesem Auftrag |
|
|
|
|
## Ausgangswerte der Tore (gemessen 2026-09-23, vor Beginn)
|
|
|
|
| Tor | Ausgangswert |
|
|
|---|---|
|
|
| `pnpm --filter @tessera/api test` | 77 Dateien, 1240 Tests, alle gruen |
|
|
| `pnpm --filter @tessera/web test` | 82 Dateien, 693 Tests, alle gruen |
|
|
| `pnpm type-check` | 4 von 4 erfolgreich |
|
|
| `pnpm lint` | 5 von 5 erfolgreich |
|
|
| Biome-Warnungen in `apps/web` | genau 53 (253 Dateien geprueft) |
|
|
|
|
Zielwert nach jeder Aufgabe: Testzahlen **groesser oder gleich** dem Ausgangswert und gruen,
|
|
type-check 4/4, lint 5/5, Biome-Warnungen in `apps/web` **exakt 53** — nicht mehr, nicht weniger.
|
|
|
|
## Ausdruecklich NICHT im Umfang
|
|
|
|
Dashboard-Kachel (D-11, kommt als eigener Auftrag ueber `WIDGET_TYPES` /
|
|
`WIDGET_MODULE_SLUGS` / `registerWidget`), Zeitreihen und Verlaufsgrafiken (`/rrddata`),
|
|
Eingriffe jeder Art (Start, Stopp, Sichern, Freigeben), Quarantaene-Verwaltung bei PMG.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-RESEARCH.md
|
|
@.planning/STATE.md
|
|
@CLAUDE.md
|
|
|
|
Bestandsmuster, die dieser Plan wortgetreu wiederverwendet (vor der jeweiligen Aufgabe lesen,
|
|
nicht raten):
|
|
|
|
@apps/api/src/favorites/icon-discovery.service.ts
|
|
@apps/api/src/ldap/ldap-config.service.ts
|
|
@apps/api/src/dkv/dkv-scheduler.service.ts
|
|
@apps/api/src/tenders/tender-scheduler.service.ts
|
|
@apps/api/src/domaincheck/domaincheck.module.ts
|
|
@apps/api/src/prisma/prisma-tenant.extension.ts
|
|
</context>
|
|
|
|
<interface_context>
|
|
Signaturen und Konstanten, auf die jede Aufgabe aufsetzt — so gemessen im Bestand, nicht erfunden:
|
|
|
|
- `CryptoService` (`apps/api/src/crypto/crypto.service.ts`): `encrypt(plaintext: string): string`
|
|
und `decrypt(stored: string): string`. Format `iv:authTag:ciphertext`, alles hex,
|
|
Doppelpunkt-getrennt. `CryptoModule` steht bereits in `app.module.ts`.
|
|
- Erkennungsform fuer „schon verschluesselt" (`ldap-config.service.ts:17`):
|
|
`/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]*$/i`.
|
|
- `forTenant(prisma, tenantId, userId?)` und `forSystem(prisma)` aus
|
|
`apps/api/src/prisma/prisma-tenant.extension.ts`. Konvention: lokale Konstante
|
|
`const tenantPrisma = forTenant(this.prisma, tenantId);` — keine andere Form, sonst schlaegt
|
|
`rls-access-inventory.spec.ts` fehl.
|
|
- `undici`: `import { Agent, fetch as undiciFetch } from 'undici'`. Nodes globales `fetch`
|
|
ignoriert einen `Agent` aus dem npm-Paket (gemessen, `icon-discovery.service.ts:33-37`).
|
|
- `@UseModule(slug)` aus `apps/api/src/module-registry/module.guard.ts`,
|
|
`@Roles(Role.ADMIN, Role.SUPER_ADMIN)` aus `apps/api/src/auth/decorators/roles.decorator.ts`.
|
|
- `ModuleRegistryService.seedModule({ slug, name, version, category, description: {de, en}, isSystem })`
|
|
— Vorlage `apps/api/src/domaincheck/domaincheck.seed.ts`.
|
|
- `CronJobClass` wird per `require('cron').CronJob` aufgeloest (pnpm-Isolation, Kommentar in
|
|
`dkv-scheduler.service.ts:6-16` woertlich uebernehmen).
|
|
- RLS-Policy-Form ohne Benutzerdimension (`20260909140000`, DkvModuleConfig):
|
|
`CREATE POLICY tenant_isolation_policy ON "X" USING ("tenantId" = current_tenant_id());`
|
|
- Systemlese-Form (`20260914120000`):
|
|
`CREATE POLICY system_read_policy ON "X" FOR SELECT USING (is_system_context());`
|
|
- Frontend-Datenzugriff: ein Helfer `apps/web/src/lib/<modul>-api.ts` (Vorbild `dkv-api.ts`),
|
|
`API_URL` aus `process.env.NEXT_PUBLIC_API_URL`, `credentials: 'include'`.
|
|
- Modulseiten liegen unter `apps/web/src/app/(portal)/modules/<slug>/`, `layout.tsx` umschliesst
|
|
mit `<ModuleAccessGate moduleSlug="<slug>">`, Eintrag in `MODULE_REGISTRY`
|
|
(`apps/web/src/lib/module-loader.ts`).
|
|
</interface_context>
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Aufgabe 1: Ein PVE-Server per Token — von der Tabelle bis zur Modulseite</name>
|
|
<files>
|
|
apps/api/prisma/schema.prisma,
|
|
apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql,
|
|
apps/api/src/proxmox/proxmox.types.ts,
|
|
apps/api/src/proxmox/proxmox-auth.ts,
|
|
apps/api/src/proxmox/proxmox-client.service.ts,
|
|
apps/api/src/proxmox/proxmox.service.ts,
|
|
apps/api/src/proxmox/proxmox.controller.ts,
|
|
apps/api/src/proxmox/proxmox.module.ts,
|
|
apps/api/src/proxmox/proxmox.seed.ts,
|
|
apps/api/src/proxmox/dto/proxmox-server.dto.ts,
|
|
apps/api/src/proxmox/proxmox.service.spec.ts,
|
|
apps/api/src/app.module.ts,
|
|
apps/web/src/lib/proxmox-api.ts,
|
|
apps/web/src/lib/module-loader.ts,
|
|
apps/web/src/app/(portal)/modules/proxmox/layout.tsx,
|
|
apps/web/src/app/(portal)/modules/proxmox/page.tsx,
|
|
apps/web/src/messages/de.json,
|
|
apps/web/src/messages/en.json,
|
|
docs/mandantentrennung-zugriffsklassifikation.md
|
|
</files>
|
|
<precondition>
|
|
`TESSERA_ENCRYPTION_KEY` ist gesetzt (64 Hex-Zeichen) und die Entwicklungsdatenbank ist vom
|
|
Host erreichbar — sonst scheitert `prisma migrate dev`. Erreichbarkeit siehe
|
|
`docs/anleitung-entwicklung.md`, Abschnitt „Datenbank vom Host erreichen"; die Datenbank hat
|
|
keinen Host-Port, der Zugriff laeuft ueber die Container-IP mit `tessera:tessera_dev`.
|
|
</precondition>
|
|
<action>
|
|
Duenner, aber durchgehender Schnitt durch ALLE Schichten, die dieses Modul anfasst — genau EIN
|
|
Weg: ein PVE-Server, Zugang per API-Token, vom Anlegen ueber die Abfrage und das Zwischenlager
|
|
bis zur Anzeige im Browser. Kein PBS, kein PMG, kein Benutzer/Passwort, kein Planer, keine
|
|
Einstellungsoberflaeche, kein Bearbeiten oder Loeschen — das bauen die Aufgaben 2 bis 6 auf
|
|
diesem bewiesenen Geruest auf. Was hier entsteht, ist Endstand, kein Wegwerfstueck: dieselbe
|
|
Fehlerbehandlung, dieselbe Mandantenbindung, dieselben Tore wie jede spaetere Aufgabe.
|
|
|
|
**Schema** (`schema.prisma`) — zwei Modelle nach dem Vorbild `CalendarSource` (mehrere
|
|
verschluesselte Fremdzugaenge je Mandant), NICHT nach `DkvModuleConfig` (Singleton je Mandant):
|
|
|
|
`ProxmoxServer`: `id` (uuid), `tenantId`, `name`, `productType` (Zeichenkette `pve|pbs|pmg`,
|
|
kommentiert wie `CalendarSource.type`), `baseUrl`, `authMethod` (`token|password`), `tokenId`
|
|
(nullable), `encryptedTokenSecret` (nullable, Kommentar „AES-256-GCM ciphertext
|
|
(iv:authTag:ciphertext hex)" wie `CalendarSource.encryptedPassword`), `username` (nullable),
|
|
`encryptedPassword` (nullable), `tlsRejectUnauthorized Boolean @default(true)` (Feldname
|
|
woertlich von `LdapConfig`, D-04), `isActive Boolean @default(true)`,
|
|
`pollIntervalMin Int @default(5)`, `position Int @default(0)`, `createdAt`, `updatedAt`,
|
|
Relation `status ProxmoxServerStatus?`, `@@index([tenantId])`.
|
|
|
|
`ProxmoxServerStatus`: `id`, `serverId String @unique` mit Relation auf `ProxmoxServer`
|
|
(`onDelete: Cascade`), `tenantId`, `lastPolledAt DateTime?`, `lastOkAt DateTime?`,
|
|
`reachable Boolean @default(false)`, `errorKind String?`, `errorDetail String?`,
|
|
`metrics Json?` (normalisierte Messwerte), `rawSample Json?` (gekuerzte Rohantwort zur
|
|
Fehlersuche beim Nutzer), `updatedAt`, `@@index([tenantId])`.
|
|
|
|
**Migration** (`20260923140000_proxmox_server/migration.sql`) — von Hand geschrieben nach dem
|
|
Vorbild `20260923120000_dashboard_tabs`: deutscher Kopfkommentar, der Zweck und die
|
|
Entscheidungen benennt. Beide Tabellen mit `ENABLE`/`FORCE ROW LEVEL SECURITY` und
|
|
`tenant_isolation_policy` in der Form OHNE Benutzerdimension
|
|
(`USING ("tenantId" = current_tenant_id())`, Vorbild `DkvModuleConfig`) — Proxmox-Server sind
|
|
Verwaltungsdaten des Mandanten, nicht persoenliche Daten eines Benutzers. Zusaetzlich auf
|
|
`ProxmoxServer` (und NUR dort) `system_read_policy … FOR SELECT USING (is_system_context())`
|
|
mit der Begruendung im Kommentar, dass der Planer aus Aufgabe 4 beim Start die aktiven Server
|
|
ALLER Mandanten sehen muss; `ProxmoxServerStatus` bekommt sie bewusst nicht, weil dort nur je
|
|
Mandant gebunden geschrieben wird. Indizes auf `tenantId` sowie `serverId` (unique). Rechte
|
|
fuer `tessera_app` kommen automatisch ueber `ALTER DEFAULT PRIVILEGES` aus
|
|
`20260909130000_rls_app_role` — im Kommentar erwaehnen, nichts tun.
|
|
|
|
**`proxmox.types.ts`** — die gemeinsamen Typen: `ProxmoxProductType = 'pve' | 'pbs' | 'pmg'`,
|
|
`ProxmoxAuthMethod = 'token' | 'password'`, `ProxmoxErrorKind =
|
|
'netz' | 'zugang' | 'rechte' | 'zertifikat' | 'antwortform' | 'server' | 'unbekannt'` (diese
|
|
sieben Werte landen so in der Datenbank und werden erst im Frontend uebersetzt — stabile
|
|
Schluessel, uebersetzbarer Text), und die Ergebnisform `ProxmoxPollResult` mit
|
|
`{ reachable, errorKind, errorDetail, metrics, rawSample }`.
|
|
|
|
**`proxmox-auth.ts`** — die EINZIGE Stelle im ganzen Modul, die Anmeldeinformationen in
|
|
Kopfzeilen uebersetzt (D-03, key_link). In dieser Aufgabe nur der Token-Zweig: eine reine
|
|
Funktion `buildTokenAuthHeader(productType, tokenId, tokenSecret)`, die fuer `pve` das Schema
|
|
`PVEAPIToken` mit Gleichheitszeichen vor dem Geheimnis und fuer `pbs` das Schema `PBSAPIToken`
|
|
mit Doppelpunkt vor dem Geheimnis liefert (Recherche, Block 1) und fuer `pmg` einen Fehler
|
|
wirft, weil PMG keine Token kennt. Die Funktion nimmt Klartext entgegen und gibt nur die
|
|
Kopfzeile zurueck — sie protokolliert nie, sie wirft das Geheimnis nie in eine Fehlermeldung.
|
|
|
|
**`proxmox-client.service.ts`** — der HTTP-Zugang, und ausschliesslich lesend (D-01).
|
|
Genau EINE oeffentliche Datenabruf-Funktion `proxmoxGet(server, path)`, die das
|
|
Anfrageverfahren fest auf Lesen setzt (kein Parameter dafuer, kein Durchreichen von aussen).
|
|
Zwingend `undiciFetch` aus dem `undici`-Paket, nicht das globale `fetch` — sonst wird der
|
|
Dispatcher stillschweigend ignoriert (gemessen, `icon-discovery.service.ts:33-37`); diesen
|
|
Grund als deutschen Kommentar in die Datei schreiben. Der Dispatcher wird JE AUFRUF aus der
|
|
gelesenen Serverzeile gebaut: ist `tlsRejectUnauthorized` wahr, wird kein Dispatcher
|
|
uebergeben (Normalweg, echte Pruefung); ist es falsch, ein frischer
|
|
`new Agent({ connect: { rejectUnauthorized: false } })` nur fuer diesen einen Aufruf (D-04).
|
|
Ausdruecklich KEINE Modulkonstante wie in `icon-discovery.service.ts` und ausdruecklich keine
|
|
Node-Umgebungsvariable — beides als Kommentar festhalten. Abbruch nach 8 Sekunden ueber
|
|
`AbortController`. Keine SSRF-Adresspruefung wie `isPublicHttpUrl`: Proxmox-Server stehen
|
|
per Definition im privaten Netz, eine solche Pruefung wuerde jede reale Adresse blockieren;
|
|
die Absicherung ist stattdessen, dass nur ein Administrator Adressen eintragen darf (siehe
|
|
Bedrohungsmodell T-DHH-02). Rueckgabe ist ein Ergebnisobjekt mit `ok`, `status`, `body` und
|
|
`errorKind` — geworfen wird nichts nach aussen; Netzfehler und Zertifikatsfehler werden
|
|
abgefangen und in `errorKind` uebersetzt.
|
|
|
|
**`proxmox.service.ts`** — die Fachlogik, jeder Datenbankzugriff ueber
|
|
`const tenantPrisma = forTenant(this.prisma, tenantId);` (Konvention woertlich, D-08):
|
|
`createServer(tenantId, dto)` verschluesselt das Token-Geheimnis mit `this.crypto.encrypt(...)`
|
|
und legt Server plus leere Zwischenlagerzeile an; `listWithStatus(tenantId)` liefert Server
|
|
samt Zwischenlager OHNE jedes Geheimnisfeld (`select` ohne `encryptedTokenSecret` und
|
|
`encryptedPassword`, nicht nachtraeglich maskiert — die Felder verlassen die Datenbank gar
|
|
nicht erst); `pollServer(tenantId, serverId)` entschluesselt in genau EINER privaten Methode
|
|
`decryptSecret(stored)` nach dem Vorbild `LdapConfigService.decryptBindPassword` (Form
|
|
erkennen, unveraenderte Altwerte durchreichen), ruft fuer `pve` den Pfad
|
|
`/api2/json/cluster/resources`, normalisiert das Ergebnis und schreibt es ins Zwischenlager.
|
|
In dieser Aufgabe nur PVE und nur eine Grundauswertung: Anzahl Knoten, Anzahl laufender und
|
|
gestoppter Gaeste, und je Knoten `cpu`/`maxcpu`/`mem`/`maxmem` — jeder Einzelwert nachsichtig
|
|
gelesen (fehlt er, steht `null` im Zwischenlager und spaeter „unbekannt" in der Anzeige, nie
|
|
ein Absturz und nie eine 0, die wie ein Messwert aussieht). Die Rohantwort wird auf hoechstens
|
|
20 000 Zeichen gekuerzt in `rawSample` abgelegt, damit der Nutzer beim Testen an seinen echten
|
|
Servern sieht, was tatsaechlich kam.
|
|
|
|
**`proxmox.controller.ts`** — `@Controller('modules/proxmox')` und `@UseModule('proxmox')` auf
|
|
Klassenebene (D-09, Vorbild `domaincheck.controller.ts`). Drei Wege: `GET servers` (Liste mit
|
|
Zwischenlager, fuer jeden Benutzer mit Modulzugriff), `POST servers` und
|
|
`POST servers/:id/poll` — beide Schreibwege zusaetzlich mit
|
|
`@Roles(Role.ADMIN, Role.SUPER_ADMIN)`. `tenantId` kommt ausschliesslich aus `req.tenantId`,
|
|
nie aus Body oder Query.
|
|
|
|
**`dto/proxmox-server.dto.ts`** — `class-validator`: `name` nicht leer, `productType` per
|
|
`@IsIn(['pve','pbs','pmg'])`, `baseUrl` per `@IsUrl({ protocols: ['http','https'], require_tld: false })`
|
|
(ohne `require_tld`, weil interne Namen wie `pve.intern` sonst abgelehnt wuerden),
|
|
`authMethod` per `@IsIn(['token','password'])`, `tlsRejectUnauthorized` optional boolesch,
|
|
`pollIntervalMin` als Ganzzahl zwischen 1 und 1440.
|
|
|
|
**`proxmox.module.ts` / `proxmox.seed.ts` / `app.module.ts`** — Vorbild Domaincheck:
|
|
`seedProxmoxModule` mit `slug: 'proxmox'`, `name: 'Proxmox'`, `version: '1.0.0'`,
|
|
`category: 'infrastructure'`, deutscher und englischer Beschreibung, `isSystem: true`;
|
|
`ProxmoxModule` importiert `ModuleRegistryModule` und ruft den Seed in `onModuleInit`;
|
|
Eintrag in `app.module.ts` unter `imports` hinter `BugReportsModule`.
|
|
|
|
**Frontend** — `apps/web/src/lib/proxmox-api.ts` nach dem Vorbild `dkv-api.ts`
|
|
(`listServers()`); `modules/proxmox/layout.tsx` mit
|
|
`<ModuleAccessGate moduleSlug="proxmox">`; `modules/proxmox/page.tsx` als Client-Komponente,
|
|
die die Serverliste laedt und je Server Name, Typ, Adresse und die vorhandenen Messwerte
|
|
anzeigt — fehlende Werte als „unbekannt", bei leerer Liste ein ruhiger Hinweis, dass noch kein
|
|
Server eingetragen ist (kein Fehlergewitter, keine weisse Flaeche); Eintrag `proxmox` in
|
|
`MODULE_REGISTRY` (`module-loader.ts`). Alle sichtbaren Texte ueber `useTranslations('proxmox')`
|
|
mit neuen Schluesseln in `de.json` UND `en.json` — deutsche Texte in der Sie-Form (D-10).
|
|
|
|
**Doku** — in `docs/mandantentrennung-zugriffsklassifikation.md` die neuen Fundstellen als
|
|
Tabellenzeilen im Format `| Datei | Modell | Klasse | Stand | Begruendung |` eintragen
|
|
(`apps/api/src/proxmox/proxmox.service.ts` / `proxmoxServer` und `proxmoxServerStatus`, Klasse
|
|
`muss-mandantengebunden`, Stand `gebunden`), sonst schlaegt `rls-access-inventory.spec.ts` fehl.
|
|
Die Bereichs- und Summenzeilen mit der Gate-Schleife NACHMESSEN, nicht abschreiben.
|
|
|
|
Deutsche Kommentare im Code wie in den Nachbardateien (D-10). Keine neue npm-Abhaengigkeit
|
|
(D-07) — `undici` steht bereits als direkte Abhaengigkeit in `apps/api/package.json`.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-coverage.spec.ts src/prisma/rls-access-inventory.spec.ts</automated>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm --filter @tessera/web test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
</verify>
|
|
<done>
|
|
`proxmox.service.spec.ts` fuehrt den ganzen Weg mit einer gefaelschten `undici`-Antwort durch
|
|
(Vorbild der Attrappe: `icon-discovery.service.spec.ts`, `vi.mock('undici', …)`): Server
|
|
anlegen, abfragen, Zwischenlager gelesen — und weist nach, dass (a) das Geheimnis
|
|
verschluesselt in der Datenbank steht und in der Antwort von `listWithStatus` ueberhaupt nicht
|
|
vorkommt, (b) bei `tlsRejectUnauthorized: true` KEIN Dispatcher uebergeben wird und bei
|
|
`false` genau einer mit abgeschalteter Pruefung, (c) ein fehlendes Feld der Antwort zu `null`
|
|
fuehrt und nicht zu einem Wurf. `pnpm --filter @tessera/api test` gruen mit mindestens 1240
|
|
Tests, `pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests, `pnpm type-check`
|
|
4 von 4. Im Browser ist `/modules/infrastructure/proxmox` erreichbar und zeigt bei leerer
|
|
Liste den ruhigen Hinweis.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 2: Zugang per Benutzer/Passwort, Fehler in Alltagssprache, Beobachtungs-Riegel</name>
|
|
<files>
|
|
apps/api/src/proxmox/proxmox-auth.ts,
|
|
apps/api/src/proxmox/proxmox-client.service.ts,
|
|
apps/api/src/proxmox/proxmox.service.ts,
|
|
apps/api/src/proxmox/dto/proxmox-server.dto.ts,
|
|
apps/api/src/proxmox/proxmox-client.service.spec.ts,
|
|
apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
|
|
</files>
|
|
<behavior>
|
|
- Ticket-Anmeldung: `POST /api2/json/access/ticket` mit `username`/`password` als Formularfeldern liefert `data.ticket`; Folgeanfragen tragen das Ticket als Cookie mit produktabhaengigem Namen (`PVEAuthCookie`, `PBSAuthCookie`, `PMGAuthCookie`).
|
|
- Kein `CSRFPreventionToken` wird jemals mitgesendet — dieses Modul liest nur, und fuer Leseanfragen verlangt Proxmox ihn laut offizieller Doku nicht.
|
|
- Ein Server vom Typ `pmg` mit `authMethod: 'token'` wird beim Anlegen und beim Bearbeiten mit einer deutschen Klartextmeldung abgelehnt (400), nicht erst beim Abfragen.
|
|
- Antwortstatus 401 wird zu `errorKind: 'zugang'`, 403 zu `'rechte'`, 404 zu `'antwortform'` mit dem Hinweis auf eine falsche Adresse, 5xx zu `'server'`.
|
|
- Ein geworfener Netzfehler ohne Antwort (Verbindung verweigert, Zeitablauf, Name nicht aufloesbar) wird zu `errorKind: 'netz'`.
|
|
- Ein Zertifikatsfehler (Meldungstext enthaelt eine der bekannten Zertifikatskennungen) wird zu `errorKind: 'zertifikat'` und NICHT zu `'netz'`.
|
|
- Eine Antwort, die kein JSON ist (HTML-Anmeldeseite, leerer Rumpf), wird zu `errorKind: 'antwortform'` — kein geworfener Parserfehler, kein Absturz.
|
|
- Laeuft ein Ticket ab (401 bei `authMethod: 'password'`), wird GENAU EINMAL neu angemeldet und die Abfrage wiederholt; erst ein zweites 401 wird zu `errorKind: 'zugang'`.
|
|
- Keine Fehlermeldung, kein Protokolleintrag und kein `rawSample` enthaelt jemals Passwort, Token-Geheimnis oder das Ticket.
|
|
- Im gesamten Verzeichnis `apps/api/src/proxmox` gibt es ausserhalb der Ticket-Anmeldung keine einzige Stelle, die ein anderes Anfrageverfahren als Lesen an Proxmox schickt.
|
|
</behavior>
|
|
<action>
|
|
Zuerst die Tests aus `<behavior>` in `proxmox-client.service.spec.ts` schreiben (rot), dann
|
|
implementieren. Die `undici`-Attrappe wie in `icon-discovery.service.spec.ts`.
|
|
|
|
`proxmox-auth.ts` waechst um den Ticket-Zweig und bleibt dabei die EINZIGE Stelle, die
|
|
Kopfzeilen und Cookies baut (D-03, key_link): `buildTokenAuthHeader` wie in Aufgabe 1, neu
|
|
`loginTicket(server, password)` und `buildTicketCookieHeader(productType, ticket)`. Die
|
|
Cookie-Namen je Produkt stehen als benannte Konstante in dieser einen Datei, mit deutschem
|
|
Kommentar, dass die Namen fuer PBS und PMG aus der Recherche nur abgeleitet sind (Annahme A2)
|
|
und der Nutzer sie an seinen echten Servern bestaetigt — steht dort ein anderer Name, ist es
|
|
genau diese eine Konstante, die angepasst wird.
|
|
|
|
`loginTicket` ist die EINZIGE Stelle im Modul, die eine nicht-lesende Anfrage an Proxmox
|
|
schickt, und sie aendert dort nichts — sie holt nur einen Nachweis ab (D-01). Diesen
|
|
Sonderstatus als deutschen Kommentar in der Datei festhalten.
|
|
|
|
`proxmox-client.service.ts` bekommt die Fehler-Uebersetzung: eine reine Funktion
|
|
`classifyFailure(status, thrownError)`, die genau die sieben Werte aus `ProxmoxErrorKind`
|
|
liefert, und eine Funktion `parseJsonLenient(text)`, die bei nicht-JSON kein Werfen zulaesst
|
|
sondern das Scheitern meldet. Die Zertifikatserkennung laeuft ueber die bekannten
|
|
Fehlerkennungen von Node/undici (selbstsigniert, abgelaufen, Name passt nicht, unbekannter
|
|
Aussteller) — im Zweifel `'zertifikat'` nur bei eindeutigem Treffer, sonst `'netz'`.
|
|
Zusaetzlich `errorDetail` als KURZE, deutsche Ergaenzung (Statuszahl, Fehlerkennung), aus der
|
|
niemals ein Geheimnis hervorgeht; die Weiterverarbeitung des `errors`-Feldes der Proxmox-Antwort
|
|
ist erlaubt, aber gekuerzt auf 500 Zeichen.
|
|
|
|
Die Ticket-Erneuerung sitzt in `proxmox.service.ts` (nicht im Klienten): ein Zaehler, der genau
|
|
einen zweiten Versuch erlaubt. Der Grund als Kommentar: bei Ticketdauer von zwei Stunden
|
|
erzeugt ein normaler Ablauf sonst alle zwei Stunden einen Fehlalarm.
|
|
|
|
`dto/proxmox-server.dto.ts` bekommt die produktabhaengige Pruefung (PMG plus Token ist
|
|
ungueltig) — Pflichtfelder je nach `authMethod` mit `@ValidateIf`, damit ein Token-Zugang
|
|
`tokenId` und Geheimnis verlangt und ein Passwort-Zugang `username` und Passwort.
|
|
|
|
`proxmox-nur-lesen.spec.ts` ist der maschinelle Riegel zu D-01, gebaut nach dem Vorbild von
|
|
`apps/api/src/prisma/rls-access-inventory.spec.ts` (Test liest den Quelltext, nicht das
|
|
Laufzeitverhalten): er liest alle `.ts`-Dateien unter `apps/api/src/proxmox`, entfernt vor
|
|
dem Zaehlen Kommentarzeilen und Zeichenkettenliterale aus Testdateien, und prueft zwei
|
|
Aussagen — erstens, dass die Summe der Stellen, die ein Anfrageverfahren an `undiciFetch`
|
|
uebergeben, genau EINS ist und in `proxmox-auth.ts` liegt; zweitens, dass jeder gegen einen
|
|
Proxmox-Pfad gebaute Aufruf ausser dieser einen ueber `proxmoxGet` laeuft. Die erwartete Zahl
|
|
steht als benannte Konstante mit ausgeschriebener Begruendung in der Testdatei, damit eine
|
|
spaetere Erhoehung eine bewusste Entscheidung erzwingt und nicht unbemerkt durchrutscht.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox</automated>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
</verify>
|
|
<done>
|
|
Alle Punkte aus `<behavior>` sind je durch mindestens einen Test belegt.
|
|
`proxmox-nur-lesen.spec.ts` ist gruen und wuerde rot, wenn irgendwo im Modul eine zweite
|
|
nicht-lesende Anfrage an Proxmox entstuende. `pnpm --filter @tessera/api test` gruen mit
|
|
mindestens 1240 Tests, `pnpm type-check` 4 von 4.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 3: PBS und PMG auswerten — nachsichtig gegen jede Antwortform</name>
|
|
<files>
|
|
apps/api/src/proxmox/proxmox-normalize.ts,
|
|
apps/api/src/proxmox/proxmox.service.ts,
|
|
apps/api/src/proxmox/proxmox.types.ts,
|
|
apps/api/src/proxmox/proxmox-normalize.spec.ts
|
|
</files>
|
|
<behavior>
|
|
- PVE: aus `/api2/json/cluster/resources` entstehen Knotenzahl, Zahl laufender und gestoppter Gaeste, je Knoten Prozessorlast und Speicherbelegung, je Speicherort Belegung.
|
|
- PBS: aus `/api2/json/status/datastore-usage` entsteht je Datenspeicher Gesamt, Belegt, Frei; aus `/api2/json/admin/datastore/{store}/snapshots` je Datenspeicher der Zeitpunkt der letzten Sicherung und das Ergebnis der letzten Pruefung.
|
|
- PMG: aus `/api2/json/statistics/mail` entstehen die Tageszahlen eingehend, ausgehend, Spam, Viren.
|
|
- Fehlt ein erwartetes Feld vollstaendig, ist der Einzelwert `null` — nie `0`, nie `NaN`, nie ein Wurf.
|
|
- Kommt eine Zahl als Zeichenkette (`"42"`, `"0.37"`), wird sie als Zahl gelesen; kommt sie als nicht umwandelbarer Text, ist der Wert `null`.
|
|
- Ist die gesamte Antwort eine Zeichenkette, ein Array statt eines Objekts, `null` oder leer, entsteht ein leeres Messwertobjekt mit `errorKind: 'antwortform'` — nie ein Wurf.
|
|
- Heisst ein Feld anders als erwartet, bleibt der zugehoerige Einzelwert `null` und die gekuerzte Rohantwort bleibt in `rawSample` erhalten, damit der Nutzer am echten Server erkennt, wie das Feld wirklich heisst.
|
|
- Ein Datenspeicher ohne Sicherungen ergibt „noch keine Sicherung" und keinen Fehler.
|
|
- Ein PBS-Server mit vielen Datenspeichern erzeugt hoechstens 10 Folgeabfragen je Durchlauf.
|
|
</behavior>
|
|
<action>
|
|
Zuerst `proxmox-normalize.spec.ts` schreiben (rot), mit ERFUNDENEN Antworten in der von der
|
|
Recherche dokumentierten Form — es gibt in dieser Umgebung keinen echten Proxmox-Server, und
|
|
es wird auch keiner angefragt. Je Punkt aus `<behavior>` mindestens ein Fall, und zusaetzlich
|
|
je Produkt ein Fall „Feld fehlt", „Zahl kommt als Zeichenkette" und „Antwort ist HTML statt
|
|
JSON".
|
|
|
|
`proxmox-normalize.ts` traegt die nachsichtigen Leser als reine Funktionen ohne
|
|
Datenbankbezug: `readNumber(value)` (Zahl, umwandelbare Zeichenkette, sonst `null`),
|
|
`readText(value)`, `readBool(value)` und `readList(value)` (liefert bei allem, was kein Array
|
|
ist, eine leere Liste). Darauf setzen `normalizePve(body)`, `normalizePbs(usage, snapshots)`
|
|
und `normalizePmg(body)` auf. Keine dieser Funktionen wirft jemals — der gesamte Umgang mit
|
|
einer unerwarteten Form ist ein Rueckgabewert, nicht eine Ausnahme; als deutscher Kommentar
|
|
festhalten, warum: der Nutzer prueft dieses Modul allein an seinen echten Servern, und ein Wurf
|
|
wuerde ihm eine leere Seite statt eines Hinweises zeigen.
|
|
|
|
Die Feldnamen von PBS und PMG sind aus der Recherche nur abgeleitet (Annahmen A2, A3, A5). In
|
|
`proxmox-normalize.ts` je Produkt eine benannte Konstante mit den erwarteten Feldnamen und
|
|
einem deutschen Kommentar, dass genau diese Liste anzupassen ist, falls der echte Server
|
|
andere Namen liefert — dadurch gibt es EINE Stelle zum Nachziehen statt verstreuter
|
|
Zeichenketten im Auswertungscode. Wo ein Feld unter mehreren plausiblen Namen auftreten kann,
|
|
darf die Konstante mehrere Namen in Reihenfolge nennen, und der Leser nimmt den ersten
|
|
vorhandenen.
|
|
|
|
`proxmox.service.ts` waechst um die produktabhaengige Abfragefolge: `pve` eine Abfrage, `pbs`
|
|
die Belegungsabfrage plus je Datenspeicher hoechstens zehn Folgeabfragen (Deckel als benannte
|
|
Konstante mit Begruendung), `pmg` eine Abfrage. Jede dieser Abfragen laeuft ueber `proxmoxGet`
|
|
— keine neue Aufrufform (Riegel aus Aufgabe 2 bleibt gruen). Das Zwischenlager bekommt je
|
|
Produkt seine Messwertform; `ProxmoxMetrics` in `proxmox.types.ts` als unterscheidbare Union
|
|
ueber `productType`, damit das Frontend typsicher verzweigen kann.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox</automated>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
</verify>
|
|
<done>
|
|
Alle Punkte aus `<behavior>` sind je durch mindestens einen Test belegt, einschliesslich der
|
|
vier ausdruecklich verlangten Fehlformen (Feld fehlt, Zahl als Zeichenkette, HTML statt JSON,
|
|
Statuscodes 401/403/404/500 — Letztere aus Aufgabe 2 weiterhin gruen).
|
|
`proxmox-nur-lesen.spec.ts` bleibt gruen. `pnpm --filter @tessera/api test` gruen mit
|
|
mindestens 1240 Tests, `pnpm type-check` 4 von 4.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 4: Hintergrundabfrage je Mandant und der Knopf „Verbindung testen"</name>
|
|
<files>
|
|
apps/api/src/proxmox/proxmox-scheduler.service.ts,
|
|
apps/api/src/proxmox/proxmox.service.ts,
|
|
apps/api/src/proxmox/proxmox.controller.ts,
|
|
apps/api/src/proxmox/proxmox.module.ts,
|
|
apps/api/src/proxmox/proxmox-scheduler.service.spec.ts,
|
|
apps/api/src/prisma/rls-access-inventory.spec.ts,
|
|
docs/mandantentrennung-zugriffsklassifikation.md
|
|
</files>
|
|
<behavior>
|
|
- Beim Start registriert der Planer je Mandant mit mindestens einem aktiven Server genau einen Auftrag unter dem Registry-Namen `proxmox-poll:<tenantId>`.
|
|
- Der Tick eines Mandanten geht ueber dessen Server und fragt jeden einzeln ab; ein fehlgeschlagener Server bricht die Schleife nicht ab.
|
|
- Ein zweiter Mandant verdraengt den Auftrag des ersten nicht — beide Auftraege bestehen nebeneinander.
|
|
- Keine aktiven Server bedeutet: kein Auftrag, ein Protokolleintrag, kein Fehler, nichts geloescht.
|
|
- Nach dem Speichern eines Servers zieht der Controller den Auftrag dieses Mandanten sofort nach — ohne Neustart.
|
|
- Der Planer haengt an `onApplicationBootstrap`, nicht an `onModuleInit`.
|
|
- Ein Fehler beim Start wird gefangen und protokolliert, nie weitergeworfen — die Anwendung startet trotzdem.
|
|
- `POST servers/:id/test` liefert bei Erfolg eine Erfolgsmeldung und bei Misserfolg genau einen der sieben Fehlerschluessel samt kurzer Ergaenzung, ohne den Zwischenlagerstand zu ueberschreiben.
|
|
- `POST servers/:id/poll` verweigert einen zweiten Durchlauf innerhalb von zehn Sekunden und liefert stattdessen den vorhandenen Zwischenlagerstand.
|
|
</behavior>
|
|
<action>
|
|
Zuerst `proxmox-scheduler.service.spec.ts` schreiben (rot) — Vorbild
|
|
`dkv-scheduler.service.spec.ts`, je Aussage aus `<behavior>` ein Test.
|
|
|
|
`proxmox-scheduler.service.ts` kombiniert die zwei Bestandsmuster (Recherche, Block 3): das
|
|
Mandanten-Auffaechern von `DkvSchedulerService` (ein Auftrag je Mandant, Registry-Name mit
|
|
Mandantenkennung als Suffix — die Vorgaengerform mit EINEM Auftragsfeld war genau der Fehler
|
|
WINDOWS #21) und die Lebenszyklus-Stufe von `TenderSchedulerService`
|
|
(`implements OnApplicationBootstrap`). Den Grund fuer `onApplicationBootstrap` als deutschen
|
|
Kommentar uebernehmen: die Reihenfolge der `onModuleInit`-Haken zwischen Modulen ist nicht
|
|
festgelegt, und die Erfahrung „frische Datenbank ingestiert nichts bis zum zweiten Neustart"
|
|
steht bereits im Projektgedaechtnis. Die Aufloesung von `CronJob` ueber `require('cron')`
|
|
samt Kommentar woertlich aus `dkv-scheduler.service.ts` uebernehmen (pnpm-Isolation).
|
|
|
|
Anders als bei DKV ist ein Mandant NICHT gleich ein Server: der Tick eines Mandanten geht ueber
|
|
dessen Serverzeilen. Das Abfrageintervall eines Mandanten ist das kleinste `pollIntervalMin`
|
|
seiner aktiven Server. Ein fehlgeschlagener Server schreibt seinen Fehler ins Zwischenlager
|
|
und die Schleife laeuft weiter — dieser Punkt ausdruecklich als Test.
|
|
|
|
Der Startpfad `loadActiveServersForScheduler()` in `proxmox.service.ts` ist der EINZIGE
|
|
Systemkontext-Aufruf des Moduls: `const systemPrisma = forSystem(this.prisma);`, nur lesend,
|
|
ohne `include` auf das Zwischenlager (die Zwischenlagertabelle hat bewusst keine
|
|
Systemlese-Regel — das Nachziehen laeuft je Zeile gebunden). Danach wird je Mandant und je
|
|
Server ueber `forTenant(this.prisma, tenantId)` geschrieben, Muster
|
|
`DkvSchedulerService`/`DashboardImagesService` (einmal lesen, viele bedienen). Diesen einen
|
|
Aufruf in `FORSYSTEM_ALLOWED_CALL_SITES` in `apps/api/src/prisma/rls-access-inventory.spec.ts`
|
|
eintragen (`apps/api/src/proxmox/proxmox.service.ts` mit Anzahl 1) und den Kopfkommentar
|
|
derselben Datei um den neuen Fall ergaenzen, wie es die bestehenden sieben Faelle vormachen —
|
|
sonst schlaegt der Waechter „ein Anfrageweg darf den Systemkontext nie rufen" fehl. In
|
|
`docs/mandantentrennung-zugriffsklassifikation.md` den Stand der Zeile
|
|
`proxmox.service.ts`/`proxmoxServer` von `gebunden` auf `system-gebunden` heben, mit derselben
|
|
Begruendungsform wie bei `dashboard-images.service.ts`; Bereichs- und Summenzeilen mit der
|
|
Gate-Schleife nachmessen.
|
|
|
|
`proxmox.controller.ts` bekommt `POST servers/:id/test` (ADMIN/SUPER_ADMIN) — es benutzt
|
|
denselben Klienten und dieselbe Fehleruebersetzung wie der Planer, schreibt aber NICHT ins
|
|
Zwischenlager, damit ein Testklick den zuletzt gemessenen Stand nicht ueberschreibt (Vorbild
|
|
`TenderEmailConfigService.testConnection` und der LDAP-Test). Zusaetzlich ruft der Controller
|
|
nach jedem erfolgreichen Anlegen und Speichern `scheduler.setInterval(tenantId)` — Vorbild
|
|
`DkvController`. `POST servers/:id/poll` bekommt die Zehn-Sekunden-Sperre als Schutz davor,
|
|
dass ein Klick in der Oberflaeche zu ungebremsten Anfragen gegen die Fremd-API wird
|
|
(Bedrohungsmodell T-DHH-06).
|
|
|
|
`proxmox.module.ts` nimmt den Planer in `providers` auf; `ScheduleModule` ist bereits global
|
|
in `app.module.ts` registriert — nichts zusaetzlich einzurichten.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-access-inventory.spec.ts src/prisma/rls-coverage.spec.ts</automated>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
</verify>
|
|
<done>
|
|
Alle Punkte aus `<behavior>` sind je durch mindestens einen Test belegt.
|
|
`rls-access-inventory.spec.ts` und `rls-coverage.spec.ts` sind gruen, einschliesslich des
|
|
neuen Erlaubnislisten-Eintrags und der nachgezogenen Dokumentationszeilen.
|
|
`proxmox-nur-lesen.spec.ts` bleibt gruen. `pnpm --filter @tessera/api test` gruen mit
|
|
mindestens 1240 Tests, `pnpm type-check` 4 von 4.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 5: Einstellungsseite — Server anlegen, bearbeiten, loeschen, testen</name>
|
|
<files>
|
|
apps/api/src/proxmox/proxmox.controller.ts,
|
|
apps/api/src/proxmox/proxmox.service.ts,
|
|
apps/api/src/proxmox/proxmox.service.spec.ts,
|
|
apps/web/src/lib/proxmox-api.ts,
|
|
apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx,
|
|
apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx,
|
|
apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx,
|
|
apps/web/src/messages/de.json,
|
|
apps/web/src/messages/en.json
|
|
</files>
|
|
<behavior>
|
|
- Bei Typ `pmg` erscheint die Auswahl „API-Token" im Formular gar nicht; nur Benutzer und Passwort sind zu sehen.
|
|
- Bei Typ `pve` oder `pbs` und Auswahl „API-Token" erscheinen Token-Kennung und Token-Geheimnis; bei Auswahl „Benutzer/Passwort" stattdessen Benutzer und Passwort.
|
|
- Ein gespeichertes Geheimnis wird beim Bearbeiten nie im Klartext angezeigt; das Feld ist leer und ein leer gelassenes Feld laesst das gespeicherte Geheimnis unveraendert.
|
|
- Der Schalter fuer die Zertifikatspruefung steht beim Anlegen auf „pruefen" und traegt einen erklaerenden Hinweis, dass die Ausnahme nur fuer diesen einen Server gilt.
|
|
- Der Knopf „Verbindung testen" zeigt bei Erfolg eine gruene Bestaetigung und bei Misserfolg den Klartext der Ursache in der Sie-Form.
|
|
- Ein Benutzer ohne Verwaltungsrolle sieht die Einstellungsseite nicht, sondern einen Hinweis.
|
|
- Loeschen verlangt eine Rueckfrage und entfernt Server samt Zwischenlagerzeile.
|
|
</behavior>
|
|
<action>
|
|
Zuerst `ServerForm.test.tsx` schreiben (rot), Vorbild
|
|
`modules/tender-radar/settings/components/EmailAlertConfigForm.test.tsx` und
|
|
`modules/dkv-fleet/settings/components/InboxConfigForm.tsx`.
|
|
|
|
Backend: `proxmox.controller.ts` und `proxmox.service.ts` um `PUT servers/:id` und
|
|
`DELETE servers/:id` ergaenzen, beide mit `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` und beide
|
|
ueber `forTenant()`. Beim Aendern gilt dieselbe Regel wie bei
|
|
`LdapConfigService.updateConfig`: ein NICHT gesendetes Geheimnisfeld laesst den gespeicherten
|
|
Wert unveraendert, eine LEERE Zeichenkette bedeutet „loeschen" und ein gefuellter Wert wird
|
|
neu verschluesselt. Die Ablehnung „PMG mit Token" gilt auch hier. Das Loeschen entfernt die
|
|
Zwischenlagerzeile ueber die Fremdschluesselregel mit Loeschweitergabe und zieht anschliessend
|
|
den Auftrag des Mandanten nach.
|
|
|
|
Frontend: `settings/page.tsx` nach dem Muster von
|
|
`modules/tender-radar/settings/page.tsx` — Rollenpruefung ausschliesslich zur Anzeige, mit
|
|
Ladezustand solange die Rolle unbekannt ist, damit die Verwaltungsteile fuer einen normalen
|
|
Benutzer nie kurz aufblitzen; der verbindliche Riegel bleibt serverseitig. Darin die
|
|
Serverliste und das Formular `ServerForm.tsx`: Name, Typ (drei Knoepfe oder Auswahl),
|
|
Adresse, Zugangsart, die typabhaengigen Zugangsfelder, Abfrageintervall, Schalter fuer die
|
|
Zertifikatspruefung, aktiv/inaktiv. Der Knopf „Verbindung testen" ruft
|
|
`POST servers/:id/test` und zeigt das Ergebnis direkt beim Formular. Die Uebersetzung der
|
|
sieben Fehlerschluessel liegt im Frontend unter `proxmox.errors.*` — deutsche Texte in der
|
|
Sie-Form (D-10), englische Entsprechungen in `en.json`; die Texte nennen die Ursache und den
|
|
naechsten Schritt, ohne Fachbegriffe (Beispielform fuer `zugang`: „Der Zugang wurde
|
|
abgelehnt. Bitte pruefen Sie Benutzername und Passwort beziehungsweise die Token-Angaben.").
|
|
`proxmox-api.ts` bekommt `createServer`, `updateServer`, `deleteServer`, `testServer`,
|
|
`pollServer`.
|
|
|
|
Biome-Warnungen in `apps/web` muessen danach exakt 53 bleiben — neue Formulareingaben brauchen
|
|
daher von Anfang an die im Bestand ueblichen Beschriftungsbezuege und Tastaturbedienbarkeit.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/web exec vitest run proxmox</automated>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm --filter @tessera/web test</automated>
|
|
<automated>pnpm lint</automated>
|
|
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
|
|
</verify>
|
|
<done>
|
|
Alle Punkte aus `<behavior>` sind je durch mindestens einen Test belegt.
|
|
`pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests,
|
|
`pnpm --filter @tessera/api test` gruen mit mindestens 1240 Tests, `pnpm lint` 5 von 5, und
|
|
`biome lint` in `apps/web` meldet unveraendert 53 Warnungen.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 6: Modulseite — Serverliste mit Auslastung, Klartext bei Stoerungen</name>
|
|
<files>
|
|
apps/web/src/app/(portal)/modules/proxmox/page.tsx,
|
|
apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx,
|
|
apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx,
|
|
apps/web/src/lib/proxmox-api.ts,
|
|
apps/web/src/messages/de.json,
|
|
apps/web/src/messages/en.json
|
|
</files>
|
|
<behavior>
|
|
- Ohne eingetragenen Server zeigt die Seite einen ruhigen Hinweis mit dem Weg zu den Einstellungen — keine Fehlermeldung, keine leere Flaeche.
|
|
- Ein PVE-Server zeigt Knotenzahl, laufende und gestoppte Gaeste sowie je Knoten Prozessorlast und Speicherbelegung.
|
|
- Ein PBS-Server zeigt je Datenspeicher Belegung, letzte Sicherung und Ergebnis der letzten Pruefung.
|
|
- Ein PMG-Server zeigt die Tageszahlen eingehend, ausgehend, Spam und Viren.
|
|
- Ein Messwert, der `null` ist, erscheint als „unbekannt" — nie als `0`, nie als leeres Feld, nie als `NaN`.
|
|
- Ein Server mit `reachable: false` zeigt den Klartext seiner Ursache und daneben den Zeitpunkt der letzten erfolgreichen Messung, falls es eine gab.
|
|
- Der Zeitpunkt der letzten Abfrage steht bei jedem Server.
|
|
- Zaehlerfelder sind ausdruecklich als „gesamt seit Start" beschriftet, nicht als aktueller Durchsatz.
|
|
- Der Knopf „Jetzt aktualisieren" loest eine Abfrage aus und laedt danach die Liste neu; waehrend des Laufs ist er gesperrt.
|
|
</behavior>
|
|
<action>
|
|
Zuerst `ServerCard.test.tsx` schreiben (rot) — je Punkt aus `<behavior>` ein Fall, mit
|
|
erfundenen Zwischenlagerstaenden je Produkttyp, einschliesslich eines Standes, in dem jeder
|
|
Einzelwert `null` ist.
|
|
|
|
`ServerCard.tsx` ist die Anzeige EINES Servers und verzweigt ueber `productType` auf der
|
|
unterscheidbaren Union aus Aufgabe 3. Eine gemeinsame kleine Hilfe stellt jeden Einzelwert
|
|
dar: ist er `null` oder `undefined`, erscheint der uebersetzte Text „unbekannt"; sonst der
|
|
Wert mit seiner Einheit (Prozentwerte gerundet, Byte-Werte in lesbarer Form). Diese Hilfe ist
|
|
die einzige Stelle, die einen Messwert in Text verwandelt — dadurch kann kein Zweig versehentlich
|
|
eine `0` anzeigen, wo nichts gemessen wurde. Den Grund als deutschen Kommentar festhalten: die
|
|
Feldnamen von PBS und PMG sind bis zur Pruefung am echten Server nur abgeleitet, und ein still
|
|
falscher Wert waere schlimmer als ein ehrliches „unbekannt".
|
|
|
|
Die Zaehlerfelder aus `cluster/resources` sind kumulative Werte seit dem Start eines Gastes,
|
|
keine Rate (Recherche, Fallstricke) — die Beschriftung sagt das ausdruecklich, damit der
|
|
Nutzer sie nicht als aktuellen Durchsatz liest.
|
|
|
|
`page.tsx` zeigt die Serverliste, oben den Knopf „Jetzt aktualisieren", und fuer Benutzer mit
|
|
Verwaltungsrolle einen Verweis auf die Einstellungsseite. Bei leerer Liste der ruhige Hinweis.
|
|
Schlaegt der Listenabruf selbst fehl, erscheint eine einzelne verstaendliche Meldung, nicht
|
|
mehrere. Alle Texte ueber `useTranslations('proxmox')` in `de.json` UND `en.json`, deutsch in
|
|
der Sie-Form (D-10).
|
|
|
|
Biome-Warnungen in `apps/web` bleiben exakt 53.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/web exec vitest run proxmox</automated>
|
|
<automated>pnpm --filter @tessera/web test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
|
|
</verify>
|
|
<human-check>
|
|
Im Browser `/modules/infrastructure/proxmox` oeffnen: ohne Server steht dort der ruhige
|
|
Hinweis; nach dem Anlegen eines Servers in den Einstellungen erscheint er in der Liste, und
|
|
ein absichtlich falsch eingetragener Zugang zeigt Klartext statt einer leeren Flaeche.
|
|
</human-check>
|
|
<done>
|
|
Alle Punkte aus `<behavior>` sind je durch mindestens einen Test belegt.
|
|
`pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests, `pnpm type-check` 4 von 4,
|
|
`biome lint` in `apps/web` unveraendert 53 Warnungen.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Aufgabe 7: Dokumentation und Nachmessung aller Tore</name>
|
|
<files>
|
|
docs/anleitung-entwicklung.md,
|
|
docs/anwenderhandbuch.md,
|
|
docs/mandantentrennung-zugriffsklassifikation.md
|
|
</files>
|
|
<action>
|
|
`docs/anwenderhandbuch.md` bekommt einen Abschnitt zum Proxmox-Modul in Alltagssprache und in
|
|
der Sie-Form (D-10): was das Modul zeigt, wie ein Server in den Einstellungen angelegt wird
|
|
(Name, Typ, Adresse, Zugang), welche NUR-LESE-Rolle im jeweiligen Produkt zu vergeben ist
|
|
(PVE `PVEAuditor`, PBS `Audit` beziehungsweise `DatastoreAudit`, PMG `Auditor`), dass bei PMG
|
|
nur Benutzer und Passwort moeglich sind, wozu der Schalter fuer die Zertifikatspruefung da ist
|
|
und dass er nur fuer genau diesen einen Server gilt, was der Knopf „Verbindung testen" sagt
|
|
und was „unbekannt" bei einem Messwert bedeutet. Ausdruecklich festhalten: Tessera veraendert
|
|
bei Proxmox nichts, es schaut nur zu (D-01).
|
|
|
|
`docs/anleitung-entwicklung.md` bekommt im Abschnitt „So entsteht ein neues Modul" einen
|
|
Hinweis auf `proxmox` als Vorlage fuer ein Modul mit Fremdsystem-Zugaengen und
|
|
Hintergrundabfrage, und an geeigneter Stelle den Merksatz zur `undici`-Falle (globales `fetch`
|
|
ignoriert einen Dispatcher aus dem npm-Paket), falls er dort noch nicht steht.
|
|
|
|
`docs/mandantentrennung-zugriffsklassifikation.md` abschliessend nachziehen: den neuen Bereich
|
|
`proxmox` als eigene Zeile in der Bereichsuebersicht und die Summenzeile — beides mit der
|
|
Gate-Schleife NACHGEMESSEN, nicht abgeschrieben, und mit dem Auftragskuerzel `260923-dhh`
|
|
versehen wie die bestehenden Eintraege.
|
|
|
|
Danach alle Tore einmal vollstaendig durchlaufen und die Endzahlen in der Zusammenfassung
|
|
gegen die Ausgangswerte aus dem `<objective>` stellen: api-Tests, web-Tests, type-check,
|
|
lint, Biome-Warnungen in `apps/web`. Eine Verschlechterung an irgendeinem Tor ist ein
|
|
Abbruchgrund, keine Randnotiz.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api test</automated>
|
|
<automated>pnpm --filter @tessera/web test</automated>
|
|
<automated>pnpm type-check</automated>
|
|
<automated>pnpm lint</automated>
|
|
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
|
|
</verify>
|
|
<done>
|
|
Anwenderhandbuch und Entwicklungsanleitung beschreiben das Modul; die Klassifikationstabelle
|
|
ist nachgemessen und `rls-access-inventory.spec.ts` gruen. Endzahlen dokumentiert:
|
|
api-Tests gruen und mindestens 1240, web-Tests gruen und mindestens 693, type-check 4 von 4,
|
|
lint 5 von 5, Biome-Warnungen in `apps/web` exakt 53.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Browser -> Tessera-API | Der Administrator sendet Serveradressen und Zugangsdaten; jeder Benutzer mit Modulfreigabe liest die Serverliste |
|
|
| Tessera-API -> Proxmox (PVE/PBS/PMG) | Ausgehende Verbindung in das interne Netz mit einem Geheimnis im Gepaeck; Gegenstelle ist nicht von Tessera kontrolliert |
|
|
| Tessera-API -> PostgreSQL | Verschluesselte Zugangsdaten und Messwerte; Mandantentrennung ueber RLS |
|
|
| Mandant A -> Mandant B | Zwei Mandanten duerfen die Proxmox-Zugaenge des jeweils anderen nie sehen |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-DHH-01 | Information Disclosure | Zugangsdaten in Antwort, Protokoll und Fehlermeldung | critical | mitigate | Aufgabe 1: `listWithStatus` waehlt `encryptedTokenSecret`/`encryptedPassword` per `select` gar nicht erst aus (nicht nachtraeglich maskiert). Aufgabe 2: `proxmox-auth.ts` protokolliert nie, `errorDetail` traegt nur Statuszahl und Fehlerkennung, `rawSample` ist auf 20 000 Zeichen gekuerzt und enthaelt nur Antwortdaten, nie die gesendete Kopfzeile. Test in Aufgabe 1/2: keine Geheimnisform in Antwort und Meldung |
|
|
| T-DHH-02 | Spoofing / SSRF | Vom Administrator eingetragene Adresse | high | mitigate | Nur ADMIN/SUPER_ADMIN duerfen Adressen eintragen (`@Roles` auf allen Schreibwegen, Aufgabe 1/5) — damit ist jede Adresse eine bewusste Freigabe (D-02). Adressform per `@IsUrl` auf `http`/`https` begrenzt. BEWUSST KEINE Privat-IP-Sperre wie `isPublicHttpUrl`: Proxmox steht per Definition im privaten Netz, eine solche Sperre wuerde das Modul unbrauchbar machen; die Begruendung steht als Kommentar in `proxmox-client.service.ts`. Abbruch nach 8 Sekunden begrenzt den Missbrauch als Portscanner |
|
|
| T-DHH-03 | Information Disclosure | Zertifikats-Ausnahme reicht weiter als gewollt | high | mitigate | Aufgabe 1: Dispatcher wird JE AUFRUF aus dem Feld `tlsRejectUnauthorized` genau dieser Serverzeile gebaut; Voreinstellung `true`. Keine Modulkonstante, keine Node-Umgebungsvariable. Test: bei `true` wird kein Dispatcher uebergeben, bei `false` genau einer mit abgeschalteter Pruefung — und die Ausnahme eines Servers wirkt nicht auf einen zweiten |
|
|
| T-DHH-04 | Elevation of Privilege | Fremder Mandant liest Proxmox-Zugaenge | critical | mitigate | Aufgabe 1: `tenantId` auf beiden Tabellen, `tenant_isolation_policy` in der Migration, jeder Zugriff ueber `forTenant()`. Aufgabe 4: der einzige `forSystem()`-Aufruf ist der Startpfad des Planers, in `FORSYSTEM_ALLOWED_CALL_SITES` eingetragen und rein lesend; geschrieben wird je Zeile gebunden. Gates: `rls-coverage.spec.ts`, `rls-access-inventory.spec.ts` |
|
|
| T-DHH-05 | Elevation of Privilege | Rechteausweitung ueber das Modul | high | mitigate | Aufgabe 1: `@UseModule('proxmox')` auf Klassenebene (Aktivierung UND Freigabe, D-09), zusaetzlich `@Roles(ADMIN, SUPER_ADMIN)` auf jedem Schreibweg. `tenantId` und Rolle kommen ausschliesslich aus dem geprueften Sitzungsnachweis, nie aus Body oder Query. Die Rollenpruefung im Frontend (Aufgabe 5) ist reine Anzeige und ersetzt nichts |
|
|
| T-DHH-06 | Denial of Service | Ungebremster Nutzer-Auslöser gegen die Fremd-API | medium | mitigate | Aufgabe 4: `POST servers/:id/poll` sperrt einen zweiten Durchlauf innerhalb von zehn Sekunden und liefert stattdessen den Zwischenlagerstand. Regulaer fragt ausschliesslich der Planer mit begrenzter Frequenz ab; jede Anzeige liest aus dem Zwischenlager (D-05). Aufgabe 3: Deckel von zehn Folgeabfragen je PBS-Durchlauf |
|
|
| T-DHH-07 | Tampering | Ein veraendernder Weg gegen Proxmox entsteht (heute oder spaeter) | high | mitigate | Aufgabe 1: nur eine Datenabruf-Funktion `proxmoxGet`, Verfahren fest verdrahtet. Aufgabe 2: `proxmox-nur-lesen.spec.ts` zaehlt maschinell nach, dass die einzige nicht-lesende Anfrage die Ticket-Anmeldung ist, mit benannter Erwartungszahl und ausgeschriebener Begruendung — eine spaetere Erhoehung erzwingt eine bewusste Entscheidung (D-01) |
|
|
| T-DHH-08 | Tampering | Zwischenlager zeigt still einen Falschwert | medium | mitigate | Aufgabe 3: jeder Einzelwert wird nachsichtig gelesen und ist bei fehlendem oder unbrauchbarem Feld `null`; Aufgabe 6: `null` erscheint als „unbekannt", nie als `0`. Die gekuerzte Rohantwort bleibt erhalten, damit der Nutzer am echten Server erkennt, wie ein Feld wirklich heisst |
|
|
| T-DHH-SC | Tampering | Paketinstallationen | low | accept | Dieser Auftrag installiert kein einziges Paket (D-07) — `undici` ist bereits direkte Abhaengigkeit von `apps/api`. Die Paket-Pruefliste der Recherche weist den Punkt ausdruecklich als nicht anwendbar aus. Entsteht wider Erwarten doch eine Installation, greift die Paket-Pruefung vor dem Einbau |
|
|
</threat_model>
|
|
|
|
<source_audit>
|
|
## Mehrfachquellen-Abdeckung
|
|
|
|
**GOAL** (Auftragsbeschreibung)
|
|
|
|
| Punkt | Status | Abgedeckt durch |
|
|
|---|---|---|
|
|
| PVE, PBS und PMG anbinden | COVERED | Aufgabe 1 (PVE), Aufgabe 3 (PBS, PMG) |
|
|
| Nur beobachten | COVERED | Aufgabe 1 (`proxmoxGet`), Aufgabe 2 (`proxmox-nur-lesen.spec.ts`) |
|
|
| Server in den Einstellungen anlegen (Adresse + Zugang) | COVERED | Aufgabe 1 (Anlegen), Aufgabe 5 (Oberflaeche, Bearbeiten, Loeschen) |
|
|
| Zugang Token oder Benutzer/Passwort, verschluesselt | COVERED | Aufgabe 1 (Token), Aufgabe 2 (Benutzer/Passwort), beide ueber `CryptoService` |
|
|
| Abfrage im Hintergrund mit Zwischenlager | COVERED | Aufgabe 1 (Zwischenlagertabelle), Aufgabe 4 (Planer) |
|
|
| Modulseite mit Serverliste und Auslastung | COVERED | Aufgabe 1 (duenne Liste), Aufgabe 6 (Auslastung je Produkt) |
|
|
|
|
**RESEARCH** (`260923-dhh-RESEARCH.md`)
|
|
|
|
| Punkt | Status | Abgedeckt durch |
|
|
|---|---|---|
|
|
| Token-Kopfzeilen je Produkt, PMG ohne Token (A1) | COVERED | Aufgabe 1 und 2 (`proxmox-auth.ts`), Aufgabe 5 (Formular bietet es bei PMG nicht an) |
|
|
| Ticket-Anmeldung, Cookie-Namen je Produkt (A2) | COVERED | Aufgabe 2, Cookie-Namen als EINE benannte Konstante mit Annahme-Kommentar |
|
|
| Kein CSRF noetig, weil nur gelesen wird | COVERED | Aufgabe 2 (`<behavior>`) |
|
|
| `cluster/resources` als eine Abfrage fuer PVE | COVERED | Aufgabe 1 und 3 |
|
|
| PBS-Belegung und Snapshot-Felder (A3) | COVERED | Aufgabe 3, Feldnamen als EINE benannte Konstante |
|
|
| PMG-Tageszahlen (A5, keine Quarantaene) | COVERED | Aufgabe 3; Quarantaene bleibt ausserhalb des Umfangs |
|
|
| Fehlerverhalten 401 breiter als ueblich (A4) | COVERED | Aufgabe 2 (`classifyFailure`) |
|
|
| undici-Dispatcher-Falle unter Node 24 | COVERED | Aufgabe 1 (Kommentar und Test), Aufgabe 7 (Anleitung) |
|
|
| Pro Zeile umschaltbarer Zertifikats-Bypass | COVERED | Aufgabe 1, T-DHH-03 |
|
|
| `onApplicationBootstrap` statt `onModuleInit` | COVERED | Aufgabe 4 |
|
|
| Mandanten-Auffaechern je Cron-Auftrag | COVERED | Aufgabe 4 |
|
|
| Zwischenlager statt Live-Abfrage | COVERED | Aufgabe 1, 4, 6 |
|
|
| RLS-Migration, Klassifikationsdoku, Erlaubnisliste | COVERED | Aufgabe 1 (Migration, Doku), Aufgabe 4 (Erlaubnisliste), Aufgabe 7 (Nachmessung) |
|
|
| Nur-Lese-Rollen je Produkt als Hinweis an den Admin | COVERED | Aufgabe 7 (Anwenderhandbuch), `user_setup` im Frontmatter |
|
|
| Zaehler sind kumulativ, keine Rate | COVERED | Aufgabe 6 (Beschriftung) |
|
|
| Ticket-Erneuerung bei 401 | COVERED | Aufgabe 2 |
|
|
| Keine neue npm-Abhaengigkeit | COVERED | Aufgabe 1 (D-07), Paket-Pruefliste nicht anwendbar |
|
|
|
|
**CONTEXT** (getroffene Entscheidungen D-01 bis D-11)
|
|
|
|
| ID | Status | Abgedeckt durch |
|
|
|---|---|---|
|
|
| D-01 | COVERED | Aufgabe 1 (`proxmoxGet`), Aufgabe 2 (`proxmox-nur-lesen.spec.ts`), `must_haves.truths`, T-DHH-07 |
|
|
| D-02 | COVERED | Aufgabe 1 (Verschluesselung, `select` ohne Geheimnisse), Aufgabe 5 (Formular), T-DHH-01 |
|
|
| D-03 | COVERED | Aufgabe 1 und 2 (`proxmox-auth.ts` als einzige Stelle), Aufgabe 5 (Formular ohne Token bei PMG) |
|
|
| D-04 | COVERED | Aufgabe 1 (Dispatcher je Aufruf), Aufgabe 5 (Schalter), T-DHH-03 |
|
|
| D-05 | COVERED | Aufgabe 1 (Zwischenlager), Aufgabe 4 (Planer nach TENDER-Muster), Aufgabe 6 (Seite liest nur den Cache) |
|
|
| D-06 | COVERED | Aufgabe 2 (Fehlerklassen), Aufgabe 4 (Testendpunkt), Aufgabe 5 (Knopf und Klartext) |
|
|
| D-07 | COVERED | Aufgabe 1 (nur `undici`), Paket-Pruefliste nicht anwendbar |
|
|
| D-08 | COVERED | Aufgabe 1 (Migration, Doku), Aufgabe 4 (Erlaubnisliste, Standwechsel), Aufgabe 7 (Nachmessung), T-DHH-04 |
|
|
| D-09 | COVERED | Aufgabe 1 (`@UseModule`, `ModuleAccessGate`), T-DHH-05 |
|
|
| D-10 | COVERED | Aufgaben 1, 5, 6 (next-intl, Sie-Form), 7 (Anwenderhandbuch) |
|
|
| D-11 | COVERED (als Ausschluss) | `<objective>`, Abschnitt „Ausdruecklich NICHT im Umfang" |
|
|
|
|
**Keine Luecke.** Nicht abgedeckt sind ausschliesslich die vom Auftrag ausgeschlossenen Punkte
|
|
(Dashboard-Kachel, `/rrddata`, Eingriffe, PMG-Quarantaene).
|
|
</source_audit>
|
|
|
|
<verification>
|
|
Nach jeder Aufgabe (je Commit):
|
|
|
|
- `pnpm --filter @tessera/api test` — gruen, mindestens 1240 Tests
|
|
- `pnpm --filter @tessera/web test` — gruen, mindestens 693 Tests
|
|
- `pnpm type-check` — 4 von 4 erfolgreich
|
|
- `pnpm lint` — 5 von 5 erfolgreich
|
|
- `pnpm --filter @tessera/web exec biome lint .` — exakt 53 Warnungen
|
|
|
|
Zusaetzlich nach den Aufgaben 1 und 4:
|
|
|
|
- `pnpm --filter @tessera/api exec vitest run src/prisma/rls-coverage.spec.ts src/prisma/rls-access-inventory.spec.ts` — gruen
|
|
|
|
Ab Aufgabe 2 dauerhaft:
|
|
|
|
- `pnpm --filter @tessera/api exec vitest run src/proxmox/proxmox-nur-lesen.spec.ts` — gruen
|
|
|
|
**Was diese Tore NICHT beweisen:** die Feldnamen von PBS und PMG (Annahmen A2, A3, A5 der
|
|
Recherche). Es gibt hier keinen echten PVE-/PBS-/PMG-Server; alle Tests laufen gegen erfundene
|
|
Antworten in der dokumentierten Form. Der Nutzer prueft das Modul selbst auf `alpha` gegen
|
|
seine echten Server. Genau dafuer sind die Feldnamen je Produkt als EINE benannte Konstante
|
|
gebaut und bleibt die gekuerzte Rohantwort im Zwischenlager erhalten: weicht die Wirklichkeit
|
|
ab, ist eine einzige Stelle nachzuziehen und der Nutzer sieht in der Oberflaeche „unbekannt"
|
|
statt eines Absturzes.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
1. Kein Weg im gesamten Modul veraendert etwas bei Proxmox; der maschinelle Riegel
|
|
`proxmox-nur-lesen.spec.ts` weist nach, dass die einzige nicht-lesende Anfrage die
|
|
Ticket-Anmeldung ist.
|
|
2. Ein Administrator legt in den Moduleinstellungen Server aller drei Typen an; bei PMG wird
|
|
die Token-Auswahl gar nicht erst angeboten und serverseitig abgelehnt.
|
|
3. Zugangsdaten stehen verschluesselt in der Datenbank und verlassen sie auf keinem Weg im
|
|
Klartext — auch nicht in Fehlermeldungen, Protokollen oder der Rohprobe.
|
|
4. Die Zertifikats-Ausnahme gilt nur fuer die Server, bei denen sie einzeln eingeschaltet
|
|
wurde; Voreinstellung ist pruefen.
|
|
5. Der Hintergrunddienst haengt an `onApplicationBootstrap`, faechert je Mandant auf und
|
|
ueberschreibt den Auftrag eines zweiten Mandanten nicht.
|
|
6. Die Modulseite liest ausschliesslich aus dem Zwischenlager, zeigt fehlende Werte als
|
|
„unbekannt" und ohne Server einen ruhigen Hinweis.
|
|
7. Der Knopf „Verbindung testen" nennt die Ursache in Alltagssprache.
|
|
8. Beide neuen Tabellen tragen `tenantId` mit RLS-Policy; `rls-coverage.spec.ts` und
|
|
`rls-access-inventory.spec.ts` sind gruen, die Klassifikationsdoku ist nachgemessen.
|
|
9. Alle Tore mindestens auf Ausgangswert: api-Tests ab 1240, web-Tests ab 693, type-check 4/4,
|
|
lint 5/5, Biome-Warnungen in `apps/web` exakt 53.
|
|
10. Anwenderhandbuch und Entwicklungsanleitung beschreiben das Modul, einschliesslich der
|
|
NUR-LESE-Rolle je Produkt.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Nach Abschluss `.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md`
|
|
schreiben — mit den gemessenen Endzahlen aller Tore neben den Ausgangswerten und einer
|
|
ausdruecklichen Liste der Stellen, die der Nutzer beim Test an seinen echten Servern
|
|
moeglicherweise nachziehen muss (Cookie-Namen je Produkt, Feldnamen je Produkt).
|
|
</output>
|