feat(260923-dhh): Proxmox-Modul Aufgabe 1 - PVE per Token, Ende-zu-Ende

- ProxmoxServer/ProxmoxServerStatus mit RLS (tenant_isolation_policy +
  system_read_policy auf ProxmoxServer fuer den kommenden Planer)
- proxmox-auth.ts (Token-Kopfzeilen PVE/PBS), proxmox-client.service.ts
  (proxmoxGet, ausschliesslich lesend, Dispatcher je Aufruf aus
  tlsRejectUnauthorized, nie global)
- proxmox.service.ts: Server anlegen (Geheimnis verschluesselt,
  select ohne Geheimnisfelder), Serverliste, PVE-Abfrage mit
  nachsichtiger Grundauswertung (Knoten/Gaeste)
- Controller/Modul/Seed nach Domaincheck-Vorbild, Kategorie
  "infrastructure", @UseModule('proxmox') + @Roles auf Schreibwegen
- Modulseite (duenne Liste) + proxmox-api.ts + Registrierung in
  MODULE_REGISTRY
- Zugriffsklassifikation nachgezogen (rls-access-inventory.spec.ts gruen)

Tore: api 1247/1247 (>=1240), web 693/693, type-check 4/4, lint 5/5,
Biome apps/web 53 Warnungen (unveraendert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 10:17:33 +02:00
parent ec9c77956d
commit 3a1bfd943e
19 changed files with 1528 additions and 4 deletions
@@ -0,0 +1,94 @@
-- 260923-dhh — Proxmox-Modul (PVE/PBS/PMG), nur beobachten (D-01).
--
-- Zweck: zwei neue Tabellen fuer das Proxmox-Modul. `ProxmoxServer` traegt
-- die vom Administrator eingetragenen Server (Name, Typ, Adresse, Zugang,
-- verschluesselt) — mehrere Zeilen je Mandant, Vorbild `CalendarSource`,
-- NICHT `DkvModuleConfig` (Singleton je Mandant). `ProxmoxServerStatus` ist
-- das Zwischenlager (D-05): der Hintergrunddienst (Aufgabe 4) beschreibt
-- diese Zeile, die Modulseite liest ausschliesslich daraus.
--
-- Von Hand geschrieben (Vorbild 20260923120000_dashboard_tabs), von Hand
-- gepflegter Kopfkommentar Pflicht bei jeder RLS-Migration in diesem Projekt.
--
-- Zeilenschutz (D-08, Pflicht — sonst schlaegt rls-coverage.spec.ts fehl):
-- beide Tabellen tragen `tenantId` und `tenant_isolation_policy` OHNE
-- Benutzerdimension (`USING ("tenantId" = current_tenant_id())`, Form aus
-- `DkvModuleConfig`, Migration 20260909140000) — Proxmox-Server sind
-- Verwaltungsdaten des Mandanten, nicht persoenliche Daten eines einzelnen
-- Benutzers.
--
-- Zusaetzlich NUR auf "ProxmoxServer" eine `system_read_policy` (Form aus
-- 20260914120000_rls_system_context_read): der Hintergrunddienst aus
-- Aufgabe 4 muss beim Start ueber `forSystem()` die aktiven Server ALLER
-- Mandanten sehen, um je Mandant einen eigenen Cron-Auftrag zu registrieren
-- (Muster DKV-/Tender-Planer). "ProxmoxServerStatus" bekommt diese Regel
-- BEWUSST NICHT — geschrieben wird dort ausschliesslich je Zeile
-- mandantengebunden (`forTenant(prisma, tenantId)`), ein Systemlesezugriff
-- auf das Zwischenlager hat keinen Aufrufer.
--
-- Rechte fuer die Anwendungsrolle tessera_app kommen automatisch ueber
-- ALTER DEFAULT PRIVILEGES aus 20260909130000_rls_app_role — hier nichts zu
-- tun.
--
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirken diese Regeln erst,
-- wenn die Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter
-- heute AUS, siehe docs/mandantentrennung-datenbankrolle.md).
-- 1) ProxmoxServer
CREATE TABLE "ProxmoxServer" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"productType" TEXT NOT NULL,
"baseUrl" TEXT NOT NULL,
"authMethod" TEXT NOT NULL,
"tokenId" TEXT,
"encryptedTokenSecret" TEXT,
"username" TEXT,
"encryptedPassword" TEXT,
"tlsRejectUnauthorized" BOOLEAN NOT NULL DEFAULT true,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"pollIntervalMin" INTEGER NOT NULL DEFAULT 5,
"position" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProxmoxServer_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProxmoxServer_tenantId_idx" ON "ProxmoxServer"("tenantId");
ALTER TABLE "ProxmoxServer" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "ProxmoxServer" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "ProxmoxServer"
USING ("tenantId" = current_tenant_id());
CREATE POLICY system_read_policy ON "ProxmoxServer"
FOR SELECT USING (is_system_context());
-- 2) ProxmoxServerStatus — Zwischenlager, 1:1 je Server, Loeschweitergabe.
CREATE TABLE "ProxmoxServerStatus" (
"id" TEXT NOT NULL,
"serverId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"lastPolledAt" TIMESTAMP(3),
"lastOkAt" TIMESTAMP(3),
"reachable" BOOLEAN NOT NULL DEFAULT false,
"errorKind" TEXT,
"errorDetail" TEXT,
"metrics" JSONB,
"rawSample" JSONB,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProxmoxServerStatus_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ProxmoxServerStatus_serverId_key" ON "ProxmoxServerStatus"("serverId");
CREATE INDEX "ProxmoxServerStatus_tenantId_idx" ON "ProxmoxServerStatus"("tenantId");
ALTER TABLE "ProxmoxServerStatus" ADD CONSTRAINT "ProxmoxServerStatus_serverId_fkey"
FOREIGN KEY ("serverId") REFERENCES "ProxmoxServer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProxmoxServerStatus" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "ProxmoxServerStatus" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "ProxmoxServerStatus"
USING ("tenantId" = current_tenant_id());
+51
View File
@@ -651,3 +651,54 @@ model TenderRssFeedSource {
@@unique([userId, url])
@@index([userId])
}
// Quick-Auftrag 260923-dhh — Proxmox-Modul (PVE/PBS/PMG), nur beobachten (D-01).
//
// Vorbild ist `CalendarSource` (mehrere verschluesselte Fremdsystem-Zugaenge
// je Mandant), NICHT `DkvModuleConfig` (Singleton je Mandant): ein Mandant
// traegt hier beliebig viele Server ein. `authMethod` waehlt zwischen einem
// API-Token (`tokenId`/`encryptedTokenSecret`) und Benutzer/Passwort
// (`username`/`encryptedPassword`); PMG kennt laut Recherche nur Letzteres
// (DTO lehnt Token bei PMG serverseitig ab, D-03). `tlsRejectUnauthorized`
// ist woertlich der Feldname aus `LdapConfig` — Voreinstellung "pruefen",
// pro Zeile umschaltbar, nie global (D-04).
model ProxmoxServer {
id String @id @default(uuid())
tenantId String
name String
productType String // 'pve' | 'pbs' | 'pmg'
baseUrl String
authMethod String // 'token' | 'password'
tokenId String?
encryptedTokenSecret String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex), wie CalendarSource.encryptedPassword
username String?
encryptedPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex)
tlsRejectUnauthorized Boolean @default(true)
isActive Boolean @default(true)
pollIntervalMin Int @default(5)
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status ProxmoxServerStatus?
@@index([tenantId])
}
// Zwischenlager (D-05): der Hintergrunddienst (Aufgabe 4) beschreibt diese
// Zeile, die Modulseite liest ausschliesslich daraus — nie live bei Proxmox.
model ProxmoxServerStatus {
id String @id @default(uuid())
serverId String @unique
server ProxmoxServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
tenantId String
lastPolledAt DateTime?
lastOkAt DateTime?
reachable Boolean @default(false)
errorKind String?
errorDetail String?
metrics Json?
rawSample Json?
updatedAt DateTime @updatedAt
@@index([tenantId])
}