opencrm/backend/src/utils/accessControl.ts

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',
);
}