test(260923-dhh): Proxmox-Modul Aufgabe 2 - Benutzer/Passwort, Fehlerklassen, Nur-Lesen-Riegel
- proxmox-auth.ts: loginTicket (die einzige nicht-lesende Anfrage im Modul, POST /access/ticket) und buildTicketCookieHeader je Produkt (Cookie-Namen als benannte Konstante, Annahme A2 kommentiert) - proxmox-client.service.ts: classifyFailure (401->zugang, 403->rechte, 404->antwortform, 5xx->server, Netzfehler->netz, Zertifikatsfehler-> zertifikat) und parseJsonLenient (kein Wurf bei Nicht-JSON); kein explizites method-Feld mehr an proxmoxGet (GET ist Grundwert) - proxmox.service.ts: Passwort-Zweig via Ticket-Anmeldung, genau ein zweiter Versuch nach 401 (Ticket-Ablauf alle zwei Stunden kein Fehlalarm) - proxmox-nur-lesen.spec.ts: maschinischer Riegel zu D-01 — genau eine Stelle (proxmox-auth.ts) uebergibt ein Anfrageverfahren an undiciFetch, jeder Proxmox-Pfad ausserhalb laeuft ueber proxmoxGet Tore: api 1270/1270 (>=1240), type-check 4/4. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import type { ProxmoxServer } from '@prisma/client';
|
||||
import { CryptoService } from '../crypto/crypto.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { buildTokenAuthHeader } from './proxmox-auth';
|
||||
import { buildTicketCookieHeader, buildTokenAuthHeader, loginTicket } from './proxmox-auth';
|
||||
import { proxmoxGet } from './proxmox-client.service';
|
||||
import type { CreateProxmoxServerDto } from './dto/proxmox-server.dto';
|
||||
import type {
|
||||
@@ -183,8 +183,13 @@ export class ProxmoxService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Baut die Anmeldekopfzeile fuer GENAU diesen Server ueber `proxmox-auth.ts` (D-03). */
|
||||
private buildAuthHeaders(server: ProxmoxServer): AuthHeaderResult {
|
||||
/**
|
||||
* Baut die Anmeldekopfzeile fuer GENAU diesen Server ueber
|
||||
* `proxmox-auth.ts` (D-03). Beim Passwort-Zweig loest das eine
|
||||
* Ticket-Anmeldung aus (die einzige nicht-lesende Anfrage des Moduls,
|
||||
* D-01) — deshalb `async`.
|
||||
*/
|
||||
private async buildAuthHeaders(server: ProxmoxServer): Promise<AuthHeaderResult> {
|
||||
if (server.authMethod === 'token') {
|
||||
const tokenSecret = this.decryptSecret(server.encryptedTokenSecret);
|
||||
if (!server.tokenId || !tokenSecret) {
|
||||
@@ -203,31 +208,42 @@ export class ProxmoxService {
|
||||
),
|
||||
};
|
||||
}
|
||||
// Benutzer/Passwort-Zweig (Ticket-Anmeldung) folgt in Aufgabe 2.
|
||||
|
||||
const password = this.decryptSecret(server.encryptedPassword);
|
||||
if (!server.username || !password) {
|
||||
return {
|
||||
ok: false,
|
||||
errorKind: 'zugang',
|
||||
errorDetail: 'Kein Benutzer/Passwort hinterlegt.',
|
||||
};
|
||||
}
|
||||
|
||||
const login = await loginTicket(
|
||||
{ baseUrl: server.baseUrl, tlsRejectUnauthorized: server.tlsRejectUnauthorized },
|
||||
server.productType as 'pve' | 'pbs' | 'pmg',
|
||||
server.username,
|
||||
password,
|
||||
);
|
||||
if (!login.ok) {
|
||||
return { ok: false, errorKind: login.errorKind, errorDetail: login.errorDetail };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
errorKind: 'unbekannt',
|
||||
errorDetail: 'Benutzer/Passwort-Zugang wird in dieser Aufgabe noch nicht unterstuetzt.',
|
||||
ok: true,
|
||||
headers: buildTicketCookieHeader(server.productType as 'pve' | 'pbs' | 'pmg', login.ticket),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* EIN Abfragedurchlauf gegen genau diesen Server. In dieser Aufgabe nur
|
||||
* `pve` mit Token — Aufgabe 2 ergaenzt Benutzer/Passwort und die
|
||||
* Fehlerklassen, Aufgabe 3 ergaenzt `pbs`/`pmg`.
|
||||
* EIN Abfragedurchlauf gegen genau diesen Server. In dieser Aufgabe `pve`
|
||||
* mit Token ODER Benutzer/Passwort (Aufgabe 3 ergaenzt `pbs`/`pmg`).
|
||||
*
|
||||
* Ticket-Erneuerung (Aufgabe 2, `<behavior>`): laeuft der Zugang ueber
|
||||
* `password` und antwortet Proxmox mit 401 (`errorKind: 'zugang'`), wird
|
||||
* GENAU EINMAL neu angemeldet und die Abfrage wiederholt — bei einer
|
||||
* Ticketdauer von zwei Stunden erzeugt ein normaler Ablauf sonst alle
|
||||
* zwei Stunden einen Fehlalarm. Ein zweites 401 bleibt `'zugang'`.
|
||||
*/
|
||||
private async pollOne(server: ProxmoxServer): Promise<ProxmoxPollResult> {
|
||||
const authHeaders = this.buildAuthHeaders(server);
|
||||
if (!authHeaders.ok) {
|
||||
return {
|
||||
reachable: false,
|
||||
errorKind: authHeaders.errorKind,
|
||||
errorDetail: authHeaders.errorDetail,
|
||||
metrics: null,
|
||||
rawSample: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (server.productType !== 'pve') {
|
||||
// PBS/PMG folgen in Aufgabe 3.
|
||||
return {
|
||||
@@ -239,7 +255,18 @@ export class ProxmoxService {
|
||||
};
|
||||
}
|
||||
|
||||
const result = await proxmoxGet(
|
||||
const authHeaders = await this.buildAuthHeaders(server);
|
||||
if (!authHeaders.ok) {
|
||||
return {
|
||||
reachable: false,
|
||||
errorKind: authHeaders.errorKind,
|
||||
errorDetail: authHeaders.errorDetail,
|
||||
metrics: null,
|
||||
rawSample: null,
|
||||
};
|
||||
}
|
||||
|
||||
let result = await proxmoxGet(
|
||||
{
|
||||
baseUrl: server.baseUrl,
|
||||
tlsRejectUnauthorized: server.tlsRejectUnauthorized,
|
||||
@@ -248,6 +275,20 @@ export class ProxmoxService {
|
||||
'/api2/json/cluster/resources',
|
||||
);
|
||||
|
||||
if (!result.ok && result.errorKind === 'zugang' && server.authMethod === 'password') {
|
||||
const retryHeaders = await this.buildAuthHeaders(server);
|
||||
if (retryHeaders.ok) {
|
||||
result = await proxmoxGet(
|
||||
{
|
||||
baseUrl: server.baseUrl,
|
||||
tlsRejectUnauthorized: server.tlsRejectUnauthorized,
|
||||
headers: retryHeaders.headers,
|
||||
},
|
||||
'/api2/json/cluster/resources',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
reachable: false,
|
||||
|
||||
Reference in New Issue
Block a user