Mandantenfähigkeit und Berechtigungen in Laravel richtig trennen

Warum Tenant-Grenzen, Objektberechtigungen und fachliche Zustände getrennte Sicherheitsregeln und negative Tests benötigen.

Mandantenfähigkeit ist nicht nur eine zusätzliche tenant_id-Spalte. Eine Anwendung muss verhindern, dass Daten, Aktionen, Dateien oder Hintergrundjobs aus einem Mandanten in einen anderen Kontext gelangen. Gleichzeitig braucht jedes Objekt eigene fachliche Berechtigungen. Wer beide Regeln vermischt, baut schwer prüfbare Sicherheitslogik.

Drei getrennte Fragen

Bei jedem Zugriff sind mindestens drei Entscheidungen nötig:

  1. Zu welchem Mandanten gehört die aktuelle Anfrage?
  2. Darf die angemeldete Person dieses konkrete Objekt verwenden?
  3. Ist die gewünschte Aktion im aktuellen fachlichen Zustand erlaubt?

Ein Benutzer kann zum richtigen Mandanten gehören und trotzdem nicht jede Rechnung freigeben. Eine Person mit Freigaberecht darf eine bereits stornierte Rechnung nicht erneut verbuchen. Diese Regeln benötigen getrennte Namen, Tests und Fehlerfälle.

Mandantenkontext nicht aus Nutzereingaben vertrauen

Eine Mandanten-ID aus Route, Header oder Formular ist zunächst untrusted input. Der Server muss sie gegen Sitzung, Token, Domain oder eine andere verlässliche Zuordnung prüfen. Danach wird ein expliziter Kontext für die Anfrage hergestellt.

Globale Eloquent Scopes können versehentliche Abfragen reduzieren. Sie sind jedoch kein vollständiges Sicherheitsmodell. Queries über DB::table() und bewusst mit withoutGlobalScope() ausgeführte Eloquent-Abfragen umgehen diese Hilfe. Administrative Prozesse und Queue-Jobs behalten einen Modell-Scope zwar grundsätzlich, besitzen aber häufig keinen automatisch gesetzten Request-Tenant; der Kontext muss dort explizit hergestellt werden. Die Grenze muss auch an Repository-, Policy- und Datenbankzugriffen erkennbar bleiben.

Objektberechtigungen in Policies ausdrücken

Laravel Policies geben Aktionen wie view, update, delete oder approve einen zentralen Ort. Eine Policy sollte den bereits bestimmten Mandantenkontext und die Beziehung des Nutzers zum Objekt prüfen. Controller und Oberfläche dürfen diese Entscheidung aufrufen, aber nicht in abweichenden Varianten nachbauen.

Ein unsicheres Muster ist die alleinige Prüfung einer globalen Rolle:

return $user->role === 'manager';

Die relevante Frage lautet eher: Ist diese Person Manager in genau diesem Mandanten, gehört das Objekt dorthin und erlaubt sein Zustand die Aktion? Je näher der Code diese Fachfrage abbildet, desto besser lässt er sich prüfen.

Versteckte Systemgrenzen beachten

Viele Datenlecks entstehen außerhalb normaler Controller:

  • Queue-Jobs verlieren den Mandantenkontext oder laden ein Objekt nur per ID.
  • signierte Download-Links prüfen die Signatur, aber nicht den aktuellen Zugriff.
  • Exporte verwenden einen zu breiten Query ohne Tenant-Filter.
  • Suchindizes enthalten Datensätze mehrerer Kunden und filtern erst in der UI.
  • Cache-Keys unterscheiden Rolle oder Mandant nicht.
  • Logs und Fehlerberichte enthalten fremde Fachdaten.

Für jeden asynchronen Prozess muss der Kontext explizit serialisiert und bei Ausführung erneut validiert werden. Ein Job sollte nicht darauf vertrauen, dass ein globaler Zustand aus dem ursprünglichen Request noch existiert.

Negative Tests als Sicherheitsnachweis

Der wichtigste Test lautet nicht nur „Mandant A kann seine Rechnung öffnen“. Ebenso nötig sind:

Test Erwartung
Nutzer A ruft ID aus Mandant B ab 404 oder kontrollierte Ablehnung
Manager ohne Objektbezug gibt frei verboten
Job erhält Objekt aus falschem Tenant Abbruch und Alarmierung
signierter Link wird nach Rechteentzug genutzt kein Zugriff
Suchergebnis enthält fremdes Objekt Ergebnis wird nie ausgeliefert

Factory-Zustände und Datensätze für mindestens zwei Mandanten machen solche Tests lesbar. Zufällig generierte IDs reichen nicht, wenn Beziehungen absichtlich gekreuzt werden müssen.

Datenbank als zusätzliche Grenze

Eindeutige Schlüssel sollten den Mandanten berücksichtigen, wenn Werte nur innerhalb eines Kunden eindeutig sein müssen. Fremdschlüssel und Constraints verhindern ungültige Beziehungen auch dann, wenn ein Codepfad einen Fehler hat. Bei sehr hohem Schutzbedarf kann Datenbank-Row-Level-Security eine weitere Schicht bilden, erhöht aber Betriebs- und Testaufwand.

Die Architekturentscheidung hängt von Schutzbedarf, Team und Plattform ab. Separate Datenbanken bieten starke Isolation, erschweren jedoch Migrationen, Reporting und Betrieb. Eine gemeinsame Datenbank ist gut beherrschbar, wenn Kontext und Tests konsequent sind.

Eine kleine Cross-Tenant-Testmatrix in Pest

Die Matrix sollte dieselbe fremde Objekt-ID über alle erreichbaren Wege prüfen, nicht nur über den Controller:

it('rejects cross-tenant access through every delivery path', function (string $path) {
    $user = User::factory()->for($tenantA)->create();
    $invoice = Invoice::factory()->for($tenantB)->create();

    actingAs($user);

    match ($path) {
        'http' => getJson(route('invoices.show', $invoice))->assertForbidden(),
        'download' => get(route('invoices.download', $invoice))->assertForbidden(),
        'export' => postJson(route('exports.store'), ['invoice_id' => $invoice->id])
            ->assertForbidden(),
    };
})->with(['http', 'download', 'export']);

Ein separater Queue-Test dispatcht den Job mit IDs aus zwei Mandanten und erwartet einen kontrollierten Abbruch vor dem Datenzugriff. Ob die Anwendung 403 oder 404 liefert, ist eine bewusste Produktentscheidung. Wichtig ist, dass alle Pfade dieselbe Invariante erzwingen und keine fremden Daten in Antwort, Log oder Export gelangen.

Die umfassendere Architektur dahinter beschreibt Laravel-Sicherheit beginnt vor dem Controller. Konkrete unsichere Implementierungsmuster stehen in 10 häufige Sicherheitsfehler in Laravel-Projekten.

Quellen und Vertiefung

Fazit

Sichere Mandantenfähigkeit entsteht durch mehrere sichtbare Grenzen. Mandantenkontext, Objektberechtigung und fachlicher Zustand werden getrennt entschieden und gemeinsam getestet. Besonders Queue-Jobs, Downloads, Suche und Caches verdienen dieselbe Sorgfalt wie der Controller.

Eine ähnliche Entscheidung steht in Ihrem Projekt an?

Schildern Sie den Kontext. Ich ordne die technischen Optionen, Risiken und einen sinnvollen nächsten Schritt ein.

Projektfrage besprechen ↗