272 lines
7.8 KiB
TypeScript
272 lines
7.8 KiB
TypeScript
/**
|
|
* Access-Control-Helper für Portal-Kunden-Isolation.
|
|
*
|
|
* Portal-Kunden haben die Permission `contracts:read` / `customers:read`, damit
|
|
* sie ihre eigenen Daten sehen können. Damit sie aber NICHT fremde Daten über
|
|
* geratene IDs abrufen (IDOR), muss bei jedem Endpoint der eine sensible
|
|
* Ressource (Vertrag, Rechnung, Passwort, ...) zurückliefert, der Kunde auf
|
|
* Besitz/Vollmacht geprüft werden.
|
|
*/
|
|
import { Response } from 'express';
|
|
import prisma from '../lib/prisma.js';
|
|
import * as authorizationService from '../services/authorization.service.js';
|
|
import { AuthRequest } from '../types/index.js';
|
|
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
|
|
|
|
/**
|
|
* Wird intern aufgerufen, wenn ein canAccess*-Check 403 zurückgibt.
|
|
* Schreibt ein SecurityEvent für Monitoring + spätere Threshold-Detection.
|
|
*/
|
|
function emitAccessDenied(req: AuthRequest, label: string, targetId: number | string): void {
|
|
const ctx = contextFromRequest(req);
|
|
emitSecurityEvent({
|
|
type: 'ACCESS_DENIED',
|
|
severity: 'MEDIUM',
|
|
message: `Zugriff verweigert: ${label} #${targetId}`,
|
|
ipAddress: ctx.ipAddress,
|
|
userId: ctx.userId,
|
|
customerId: ctx.customerId,
|
|
userEmail: ctx.userEmail,
|
|
endpoint: ctx.endpoint,
|
|
details: { resource: label, targetId },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prüft ob der authentifizierte User auf einen bestimmten Vertrag zugreifen darf.
|
|
* - Mitarbeiter/Admin mit customers:read / contracts:read: ja, immer
|
|
* - Portal-Kunde: nur wenn contract.customerId = eigener customerId ODER
|
|
* wenn er einen Vertreter für diesen Kunden ist MIT gültiger Vollmacht
|
|
*
|
|
* @returns true = erlaubt, false = Zugriff verweigert (Response wurde bereits gesendet)
|
|
*/
|
|
export async function canAccessContract(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
contractId: number,
|
|
): Promise<boolean> {
|
|
// Nicht-Portal-User (Mitarbeiter/Admin) kommen hier immer durch, wenn sie die Permission haben
|
|
if (!req.user?.isCustomerPortal) {
|
|
return true;
|
|
}
|
|
|
|
if (!req.user.customerId) {
|
|
res.status(403).json({ success: false, error: 'Kein Zugriff' });
|
|
return false;
|
|
}
|
|
|
|
// Vertrag laden, Besitzer-ID prüfen
|
|
const contract = await prisma.contract.findUnique({
|
|
where: { id: contractId },
|
|
select: { customerId: true },
|
|
});
|
|
|
|
if (!contract) {
|
|
res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' });
|
|
return false;
|
|
}
|
|
|
|
// Eigene Verträge = immer erlaubt
|
|
if (contract.customerId === req.user.customerId) {
|
|
return true;
|
|
}
|
|
|
|
// Fremde Verträge nur mit aktiver Vollmacht
|
|
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
|
if (!representedIds.includes(contract.customerId)) {
|
|
emitAccessDenied(req, 'Contract', contractId);
|
|
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Vertrag' });
|
|
return false;
|
|
}
|
|
|
|
const hasAuth = await authorizationService.hasAuthorization(
|
|
contract.customerId,
|
|
req.user.customerId,
|
|
);
|
|
if (!hasAuth) {
|
|
emitAccessDenied(req, 'Contract (Vollmacht fehlt)', contractId);
|
|
res.status(403).json({ success: false, error: 'Vollmacht erforderlich' });
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Prüft Zugriff auf einen Kunden (analog zu canAccessContract).
|
|
*/
|
|
export async function canAccessCustomer(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
customerId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) {
|
|
return true;
|
|
}
|
|
|
|
if (!req.user.customerId) {
|
|
res.status(403).json({ success: false, error: 'Kein Zugriff' });
|
|
return false;
|
|
}
|
|
|
|
if (customerId === req.user.customerId) {
|
|
return true;
|
|
}
|
|
|
|
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
|
if (!representedIds.includes(customerId)) {
|
|
emitAccessDenied(req, 'Customer', customerId);
|
|
res.status(403).json({ success: false, error: 'Kein Zugriff auf diese Kundendaten' });
|
|
return false;
|
|
}
|
|
|
|
const hasAuth = await authorizationService.hasAuthorization(customerId, req.user.customerId);
|
|
if (!hasAuth) {
|
|
emitAccessDenied(req, 'Customer (Vollmacht fehlt)', customerId);
|
|
res.status(403).json({ success: false, error: 'Vollmacht erforderlich' });
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generische Zugriffsprüfung: Ressource → customerId → canAccessCustomer.
|
|
*/
|
|
async function canAccessResourceByCustomerId(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
customerId: number | null | undefined,
|
|
resourceLabel: string,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
|
|
if (!customerId) {
|
|
res.status(404).json({ success: false, error: `${resourceLabel} nicht gefunden` });
|
|
return false;
|
|
}
|
|
return canAccessCustomer(req, res, customerId);
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf eine Adresse prüfen (lädt sie aus der DB, prüft customerId).
|
|
*/
|
|
export async function canAccessAddress(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
addressId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const addr = await prisma.address.findUnique({
|
|
where: { id: addressId },
|
|
select: { customerId: true },
|
|
});
|
|
return canAccessResourceByCustomerId(req, res, addr?.customerId, 'Adresse');
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf eine BankCard prüfen.
|
|
*/
|
|
export async function canAccessBankCard(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
bankCardId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const card = await prisma.bankCard.findUnique({
|
|
where: { id: bankCardId },
|
|
select: { customerId: true },
|
|
});
|
|
return canAccessResourceByCustomerId(req, res, card?.customerId, 'Bankkarte');
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf ein IdentityDocument prüfen.
|
|
*/
|
|
export async function canAccessIdentityDocument(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
documentId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const doc = await prisma.identityDocument.findUnique({
|
|
where: { id: documentId },
|
|
select: { customerId: true },
|
|
});
|
|
return canAccessResourceByCustomerId(req, res, doc?.customerId, 'Ausweis');
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf einen Meter prüfen.
|
|
*/
|
|
export async function canAccessMeter(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
meterId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const meter = await prisma.meter.findUnique({
|
|
where: { id: meterId },
|
|
select: { customerId: true },
|
|
});
|
|
return canAccessResourceByCustomerId(req, res, meter?.customerId, 'Zähler');
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf eine StressfreiEmail prüfen.
|
|
*/
|
|
export async function canAccessStressfreiEmail(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
stressfreiEmailId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const sfe = await prisma.stressfreiEmail.findUnique({
|
|
where: { id: stressfreiEmailId },
|
|
select: { customerId: true },
|
|
});
|
|
return canAccessResourceByCustomerId(req, res, sfe?.customerId, 'E-Mail-Konto');
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf eine CachedEmail prüfen (StressfreiEmail → customerId).
|
|
*/
|
|
export async function canAccessCachedEmail(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
emailId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const email = await prisma.cachedEmail.findUnique({
|
|
where: { id: emailId },
|
|
select: { stressfreiEmail: { select: { customerId: true } } },
|
|
});
|
|
return canAccessResourceByCustomerId(
|
|
req,
|
|
res,
|
|
email?.stressfreiEmail?.customerId,
|
|
'E-Mail',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Zugriff auf ein EnergyContractDetails prüfen (ECD → Contract → customerId).
|
|
*/
|
|
export async function canAccessEnergyContractDetails(
|
|
req: AuthRequest,
|
|
res: Response,
|
|
ecdId: number,
|
|
): Promise<boolean> {
|
|
if (!req.user?.isCustomerPortal) return true;
|
|
const ecd = await prisma.energyContractDetails.findUnique({
|
|
where: { id: ecdId },
|
|
select: { contract: { select: { customerId: true } } },
|
|
});
|
|
return canAccessResourceByCustomerId(
|
|
req,
|
|
res,
|
|
ecd?.contract?.customerId,
|
|
'Energievertrag',
|
|
);
|
|
}
|