getCockpit fail-closed (R146) - konsistent zu listAll/getContracts

Pentester R146 (Konsistenz + Defense-in-Depth): getCockpit war der
einzige der drei Geschwister-Endpunkte noch auf dem alten Fail-open-
Muster. Der Cockpit-SERVICE ist bereits fail-closed (customerIds:[] ->
IN () -> 0), aber der CONTROLLER uebergab bei falsy customerId
undefined statt [] -> {} -> alle Kunden. Genau die R4-HIGH-Stelle
'Cockpit leakt alle Vertraege'.

Jetzt: Portal-Token wird IMMER gescoped (ohne customerId -> []),
identischer Einzeiler wie listAll/getContracts. Nicht erreichbar (Token
traegt immer customerId), aber der urspruengliche HIGH-Endpunkt soll
nicht das letzte Fail-open-Muster bleiben.

Verifiziert: Cockpit Staff -> 17 Vertraege; Portal []->0 (contracts +
cancellationConfirmations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 11:44:52 +02:00
co-authored by Claude Opus 4.8
parent 8d93c1767b
commit e1ca56ebfb
@@ -633,9 +633,13 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
// Portal-User dürfen nur ihre eigenen + vertretene Kunden (mit Vollmacht) sehen.
// Analog zu getContracts. Sonst leakt das Cockpit ALLE Verträge ALLER Kunden
// (Pentest Runde 4, 2026-05-16: HOCH).
// Fail-closed (Pentest R146): Portal-Token wird IMMER gescoped; fehlt wider
// Erwarten die customerId, ergibt das eine leere Menge (nicht alle) der
// Cockpit-Service filtert `customerIds: []` bereits als „keine Treffer".
let customerIds: number[] | undefined;
if (req.user?.isCustomerPortal && req.user.customerId) {
customerIds = [req.user.customerId];
if (req.user?.isCustomerPortal) {
customerIds = req.user.customerId ? [req.user.customerId] : [];
if (req.user.customerId) {
const representedIds: number[] = req.user.representedCustomerIds || [];
for (const repCustId of representedIds) {
const hasAuth = await authorizationService.hasAuthorization(repCustId, req.user.customerId);
@@ -644,6 +648,7 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
}
}
}
}
const cockpitData = await contractCockpitService.getCockpitData({ customerIds });
res.json({ success: true, data: cockpitData } as ApiResponse);