Files
opencrm/backend/src/services/authorization.service.ts
T
duffyduck 25681075b4 Pentest 24.6 INFO + 26.7 LOW: PENDING-Status sperren + documentPath-Validator
24.6 (Portal kann Consent auf PENDING zurücksetzen):
- gdpr.controller updateCustomerConsent prüft jetzt explizit, dass
  der Portal-User nur GRANTED oder WITHDRAWN setzen kann. PENDING
  ist nur der initiale System-Status; ein Reset darauf hätte die
  DSGVO-Auswertung verfälscht.

26.7 (documentPath ohne Validierung):
- Neuer Helper isValidDocumentPath + assertValidDocumentPath in
  utils/sanitize: nur /?uploads/<safe>, keine "..", keine
  javascript:/data:/vbscript:, kein HTML.
- consent.service.updateConsent ruft den Assert auf – Defense-in-
  Depth gegen zukünftige Caller, die documentPath aus User-Input
  durchreichen könnten.
- authorization.service.grantAuthorization analog.
- Cleanup-Skript (prisma/cleanup-xss-and-mass-assignment) entfernt
  seine lokale Kopie der Path-Validierung und nutzt den shared
  Helper – Single Source of Truth.

27.1 (Altdaten in Staging-DB):
- Cleanup-Skript läuft sowieso bei jedem Container-Start. Nina-
  Records mit "../../../etc/passwd" werden beim nächsten Restart
  genullt (oder verschwinden mit dem VM-Snapshot-Wechsel).

Live-Test isValidDocumentPath: 13/13 OK – legitime Pfade durch,
Traversal/JS-URI/HTML blockiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 14:20:13 +02:00

233 lines
6.7 KiB
TypeScript

import prisma from '../lib/prisma.js';
import fs from 'fs';
import path from 'path';
import { assertValidDocumentPath } from '../utils/sanitize.js';
/**
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
*/
export async function getAuthorizationsForCustomer(customerId: number) {
return prisma.representativeAuthorization.findMany({
where: { customerId },
include: {
representative: {
select: { id: true, customerNumber: true, firstName: true, lastName: true },
},
},
orderBy: { createdAt: 'desc' },
});
}
/**
* Vollmachten die ein Vertreter erhalten hat (welche Kunden darf er einsehen?)
*/
export async function getAuthorizationsForRepresentative(representativeId: number) {
return prisma.representativeAuthorization.findMany({
where: { representativeId },
include: {
customer: {
select: { id: true, customerNumber: true, firstName: true, lastName: true },
},
},
orderBy: { createdAt: 'desc' },
});
}
/**
* Prüft ob ein Vertreter eine Vollmacht für einen Kunden hat
*/
export async function hasAuthorization(customerId: number, representativeId: number): Promise<boolean> {
const auth = await prisma.representativeAuthorization.findUnique({
where: {
customerId_representativeId: { customerId, representativeId },
},
});
return auth?.isGranted === true;
}
/**
* Vollmacht erteilen oder aktualisieren
*/
export async function grantAuthorization(
customerId: number,
representativeId: number,
data: { source?: string; documentPath?: string; notes?: string }
) {
// Pentest 26.7 (Defense-in-Depth): documentPath nur als /uploads/<safe>.
assertValidDocumentPath(data.documentPath, 'documentPath');
return prisma.representativeAuthorization.upsert({
where: {
customerId_representativeId: { customerId, representativeId },
},
update: {
isGranted: true,
grantedAt: new Date(),
withdrawnAt: null,
source: data.source,
documentPath: data.documentPath ?? undefined,
notes: data.notes ?? undefined,
},
create: {
customerId,
representativeId,
isGranted: true,
grantedAt: new Date(),
source: data.source || 'crm-backend',
documentPath: data.documentPath,
notes: data.notes,
},
});
}
/**
* Vollmacht widerrufen + PDF löschen falls vorhanden
*/
export async function withdrawAuthorization(customerId: number, representativeId: number) {
// Erst prüfen ob eine PDF vorhanden ist
const existing = await prisma.representativeAuthorization.findUnique({
where: { customerId_representativeId: { customerId, representativeId } },
select: { documentPath: true },
});
// PDF vom Filesystem löschen
if (existing?.documentPath) {
try {
const filePath = path.join(process.cwd(), existing.documentPath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (err) {
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
}
}
return prisma.representativeAuthorization.update({
where: {
customerId_representativeId: { customerId, representativeId },
},
data: {
isGranted: false,
withdrawnAt: new Date(),
documentPath: null,
},
});
}
/**
* Vollmacht-Dokument (PDF) hochladen
*/
export async function updateAuthorizationDocument(
customerId: number,
representativeId: number,
documentPath: string
) {
// Wenn Dokument hochgeladen wird, gilt das als Vollmacht erteilen
return prisma.representativeAuthorization.upsert({
where: {
customerId_representativeId: { customerId, representativeId },
},
update: {
documentPath,
isGranted: true,
grantedAt: new Date(),
withdrawnAt: null,
source: 'papier',
},
create: {
customerId,
representativeId,
documentPath,
isGranted: true,
grantedAt: new Date(),
source: 'papier',
},
});
}
/**
* Vollmacht-Dokument löschen
*/
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
// Prüfen ob die Vollmacht per Papier erteilt wurde
const auth = await prisma.representativeAuthorization.findUnique({
where: { customerId_representativeId: { customerId, representativeId } },
select: { source: true, documentPath: true },
});
if (!auth) throw new Error('Vollmacht nicht gefunden');
// Datei löschen
if (auth.documentPath) {
try {
const filePath = path.join(process.cwd(), auth.documentPath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (err) {
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
}
}
// Wenn per Papier erteilt → Vollmacht widerrufen
// Wenn per Portal/Online erteilt → nur PDF entfernen, Vollmacht bleibt
const withdrawData = auth.source === 'papier'
? { documentPath: null, isGranted: false, withdrawnAt: new Date() }
: { documentPath: null };
return prisma.representativeAuthorization.update({
where: { customerId_representativeId: { customerId, representativeId } },
data: withdrawData,
});
}
/**
* Alle genehmigten Vertreter-IDs für einen Kunden
* (Welche Vertreter dürfen die Verträge dieses Kunden sehen?)
*/
export async function getAuthorizedRepresentativeIds(customerId: number): Promise<number[]> {
const auths = await prisma.representativeAuthorization.findMany({
where: { customerId, isGranted: true },
select: { representativeId: true },
});
return auths.map((a) => a.representativeId);
}
/**
* Alle Kunden-IDs für die ein Vertreter eine Vollmacht hat
*/
export async function getAuthorizedCustomerIds(representativeId: number): Promise<number[]> {
const auths = await prisma.representativeAuthorization.findMany({
where: { representativeId, isGranted: true },
select: { customerId: true },
});
return auths.map((a) => a.customerId);
}
/**
* Erstellt fehlende Vollmacht-Einträge für bestehende Vertreterbeziehungen
* (wird aufgerufen wenn man den Tab aufruft)
*/
export async function ensureAuthorizationEntries(customerId: number) {
// Alle aktiven Vertreter für diesen Kunden
const representatives = await prisma.customerRepresentative.findMany({
where: { customerId, isActive: true },
select: { representativeId: true },
});
for (const rep of representatives) {
// Erstelle Eintrag falls nicht vorhanden
await prisma.representativeAuthorization.upsert({
where: {
customerId_representativeId: { customerId, representativeId: rep.representativeId },
},
update: {}, // Nichts ändern wenn schon vorhanden
create: {
customerId,
representativeId: rep.representativeId,
isGranted: false,
},
});
}
}