Alle Beiträge

Blog · Agenturpraxis ·

Supabase RLS testen: Mandantentrennung im Agentur-CRM

Prüfen Sie Row-Level Security mit berechtigten und unberechtigten Nutzern direkt an der Daten-API. Eine erfolgreiche Kontrollanfrage, eine abgewiesene Gegenprobe und der unveränderte Datenbestand gehören zusammen. Unser lokales CRM-Beispiel macht das mit zwei Organisationen, Mitarbeitern und Partnern nachvollziehbar: 43 HTTP-Prüfungen gegen PostgreSQL und PostgREST.

Was wurde tatsächlich getestet?

Getestet wurden echte HTTP-Anfragen an eine lokale PostgreSQL-/PostgREST-Umgebung. Es handelt sich weder um reine SQL-Simulationen noch um einen Test des gehosteten Supabase-Dienstes. PostgreSQL 18.6 und PostgREST 14.14 liefen in eigenen Docker-Containern; die Datenbank hatte keinen veröffentlichten Host-Port. Nur die Test-API war auf einem zufälligen lokalen Port erreichbar.

Die veröffentlichten Ergebnisse enthalten 43 bestandene Prüfungen, die verwendeten Versionen, Antworten und Prüfsummen der Quelldateien. Der aufgezeichnete Lauf vom 28. September 2026 wurde mit frischen Containern ausgeführt. Namen und Kontakte sind künstlich; Passwörter und Tokens wurden für diesen Lauf erzeugt und nicht veröffentlicht.

PostgREST prüft die signierten Test-Tokens und setzt die Datenbankrolle. Eine eigene Hilfsfunktion liest daraus die Benutzer-ID. Supabase Auth und dessen echte Funktion auth.uid() wurden dabei nicht ausgeführt. Das Beispiel belegt deshalb die gezeigten Grants, Policies und HTTP-Reaktionen, aber keine Supabase-Anmeldung, MFA, Sitzungserneuerung oder gehostete API-Konfiguration.

Welche Rechte hat das Beispiel-CRM?

Eine Agentur baut ein CRM für zwei getrennte Kundenorganisationen: Alpha und Beta. Jede hat einen Mitarbeiter und einen externen Partner. Alpha besitzt die Kontakte a1 und a2, Beta den Kontakt b1. Der Alpha-Partner darf nur a1 lesen. Das ist ein bewusst kleines Modell, dessen erlaubte Zugriffe vollständig benannt sind.

Fachliche Rollen und erwartete Zugriffe
Identität Lesen Schreiben
Mitarbeiter Alpha a1, a2 Kontakte in Alpha anlegen, umbenennen und löschen
Partner Alpha Nur a1 Keine Änderungen
Mitarbeiter Beta Nur b1 Kontakte in Beta anlegen, umbenennen und löschen
Partner Beta Nur b1 Keine Änderungen
Angemeldet ohne Mitgliedschaft Keine Kontakte Keine Änderungen
Ohne Anmeldung Zugriff auf die Kontakttabelle verweigert Keine Änderungen

Die Datenbankrolle aller angemeldeten Testnutzer heißt authenticated. Ihre fachlichen Rollen employee und partner stehen in einer separaten Mitgliedschaftstabelle. Kein Nutzer darf diese Tabelle über die API ändern. Ein Organisationsheader oder ein selbst gesetzter Wert in user_metadata verschafft ihm keine zusätzlichen Rechte.

Das ist eine wichtige Trennung für Supabase: Ein authentifiziertes Konto ist nicht automatisch Mitarbeiter eines Kunden. Supabase unterscheidet die Rollen anon und authenticated; die fachliche Berechtigung muss das Projekt zusätzlich definieren. Nutzerveränderbare Metadaten eignen sich dafür nicht als Autorität. Auch ein anonymer Supabase-Auth-Nutzer kann die Rolle authenticated haben; das ist etwas anderes als ein Aufruf ohne Anmeldung.

Wie greifen Grants und RLS ineinander?

Grants erlauben eine Operation auf einer Tabelle oder Spalte. RLS begrenzt die betroffenen Zeilen. Beides steht in der vollständigen SQL-Fixture. Die Kontakttabelle aktiviert und erzwingt RLS. Für Lesen, Anlegen, Ändern und Löschen gibt es getrennte Policies.

Die Leseregel fordert eine Mitgliedschaft in derselben Organisation. Ein Mitarbeiter sieht deren Kontakte; ein Partner zusätzlich nur die ihm zugeordnete Zeile. Der Ausschnitt stammt aus der tatsächlich ausgeführten Fixture:

create policy contact_read on api.contacts
for select to authenticated using (
  exists (
    select 1 from private.memberships m
    where m.user_id = (select private.subject())
      and m.org_id = contacts.org_id
      and (
        m.crm_role = 'employee'
        or contacts.partner_id = m.user_id
      )
  )
);

private.subject() ist hier ausdrücklich die lokale Hilfsfunktion. Sie liest den von PostgREST geprüften sub-Claim. Für ein Supabase-Projekt ist der echte Auth-Pfad mit auth.uid() gesondert zu prüfen; die Fixture soll dessen vorhandene Funktionen und Rollen nicht überschreiben.

USING begrenzt den Zugriff auf vorhandene Zeilen. WITH CHECK prüft neue beziehungsweise geänderte Zeilen. Im Beispiel verhindert diese Prüfung das Anlegen eines Kontakts in der fremden Organisation. Die PostgreSQL-Dokumentation zu CREATE POLICY beschreibt die unterschiedliche Wirkung je Operation.

Eine weitere Grenze kommt vom Spaltenrecht: Der REST-Endpunkt darf nur name ändern. org_id und partner_id bleiben darüber unveränderlich. Der versuchte Mandantenwechsel scheitert folglich am Grant, während das fremde INSERT an RLS scheitert. Ein Prüfbericht sollte diesen Unterschied benennen.

Der Diagnoseaufruf belegt für die vier Mitgliedschaftsidentitäten und für anon: aktive und erzwungene RLS, kein Superuser, kein BYPASSRLS und keine Ausführung als Tabellenbesitzer. Das ist erforderlich, weil diese privilegierten Rollen Regeln umgehen können; FORCE ROW LEVEL SECURITY hebt insbesondere BYPASSRLS nicht auf. Siehe PostgreSQL: Row Security Policies.

Das Beispiel selbst ausführen

Speichern Sie fixture.sql und run.py in einem neuen Verzeichnis und lesen Sie die Dateien vor dem Start. Die vollständige Anleitung enthält die festgeschriebenen Image-Digests und die Grenzen des Versuchs. Code und Testmaterial stehen unter MIT-Lizenz.

Voraussetzungen sind Python 3.10 oder neuer und eine laufende Docker-Linux-Engine. Der erste Image-Download benötigt Netzwerkzugriff. Nach dem Laden der zwei in der Anleitung genannten Images starten Sie im Verzeichnis:

python3 run.py > results.json

Der Runner erzeugt seine Container, sein Netzwerk und die Testdaten selbst. Er akzeptiert keine Adresse eines bestehenden Systems und entfernt seine Ressourcen anschließend. Ein erfolgreicher Lauf endet mit Exit-Code 0 und "passed": 43. Kein Supabase-Konto, kein API-Schlüssel eines Kunden und keine zusätzliche Python-Bibliothek sind nötig.

Die Test-Tokens werden ausschließlich vom lokalen Runner signiert. PostgREST validiert sie bei echten HTTP-Aufrufen und übernimmt die erlaubte Datenbankrolle. Diese Rolle wird zusätzlich im Test abgefragt. Die PostgREST-14-Dokumentation zur Authentifizierung erklärt diesen Ablauf. Das Erzeugen von Test-Tokens ist kein Anmeldeverfahren für Ihre Anwendung.

Die beobachteten Ergebnisse

Die folgenden Ergebnisse sind eine Auswahl aus dem veröffentlichten Lauf. Jede erlaubte und verbotene Anfrage wurde einzeln geprüft. Nach Schreibgegenproben wurde der gespeicherte Zustand über einen berechtigten Nutzer kontrolliert.

Lokaler Prüflauf vom 28. September 2026
Prüfung Beobachtet Kontrolle
Alpha-Mitarbeiter liest Kontakte 200; a1 und a2 Beta-Mitarbeiter sieht nur b1
Alpha-Partner liest a2 oder fremden Mandanten 200; keine freigegebenen fremden Zeilen Der erlaubte Kontakt a1 bleibt lesbar
Ohne Anmeldung Kontakte lesen oder anlegen 401; fehlendes Tabellenrecht Angemeldeter Mitarbeiter kann lesen und anlegen
Partner oder fremder Mitarbeiter ändert/löscht 200; keine betroffene Zeile Der berechtigte Nutzer liest den unveränderten Kontakt
Alpha-Mitarbeiter legt in Beta an 403; PostgreSQL-Code 42501 Kein neuer Datensatz; Anlegen in Alpha erfolgreich
org_id oder partner_id ändern 403; Spaltenrecht fehlt Organisation und Zuordnung unverändert
Ungültige Signatur oder abgelaufenes JWT 401 Gültiges Mitarbeiter-Token funktioniert
Partner-Mitgliedschaft entziehen Dasselbe JWT sieht anschließend keine Kontakte Der Mitarbeiter sieht weiterhin a1 und a2

Zusätzlich scheiterten die Annahme der privilegierten Datenbankrolle postgres und der Zugriff auf das nicht freigegebene Schema. Ein mitgesendeter Organisationsheader und zusätzliche Nutzermetadaten erweiterten die Rechte nicht. Der JSON-Bericht mit allen 43 Prüfungen zeigt die Antworten und die Zuordnung zur geprüften Quellversion.

Warum kann HTTP 200 eine korrekte Ablehnung sein?

RLS kann nicht erlaubte Zeilen aus der sichtbaren Ergebnismenge entfernen. Ein erfolgreicher Abruf mit [] bedeutet dann: Für diese Identität ist keine passende Zeile sichtbar. Auch ein PATCH oder DELETE kann keine Zeile treffen, ohne einen Berechtigungsfehler zurückzugeben. Das Beispiel fordert die Antwortdarstellung an und prüft deren Inhalt.

Wer ausschließlich auf 403 wartet, kann eine wirksame Regel für defekt halten. Wer ausschließlich auf 200 schaut, kann eine wirkungslose Änderung für erfolgreich halten. Prüfen Sie deshalb Status, Antwort und Datenbestand zusammen. Für einen verbotenen Schreibversuch muss ein berechtigter Kontrollnutzer anschließend den ursprünglichen Wert sehen. Für einen erlaubten Versuch muss der neue Wert gespeichert sein.

Eine absichtlich nicht gewährte Tabellen- oder Spaltenberechtigung führt dagegen zu einer anderen Reaktion. Die beobachteten 401 und 403 sind Ergebnisse dieser konkreten PostgREST-Konfiguration; zusätzliche Gateway- oder Anwendungsschichten können eigene Fehlerantworten erzeugen.

Was muss im echten Supabase-Projekt folgen?

Übertragen Sie die Prüffälle in eine getrennte Testumgebung mit der tatsächlichen Supabase-Konfiguration. Supabase beschreibt Datenbanktests mit pgTAP und der CLI. Ergänzen Sie dazu HTTP-Tests über die echte Data API. SQL-Tests prüfen Regeln präzise; API-Tests prüfen zusätzlich den verwendeten Zugangsweg.

  1. Echte Testidentitäten: Mitarbeiter und Partner beider Organisationen regulär anmelden. Die Tokens aus diesem Auth-Pfad verwenden; einen gültigen Nutzer ohne Mitgliedschaft und einen Aufruf ohne Anmeldung ergänzen.
  2. Tatsächliche Grants: Freigegebene Schemas, Tabellenrechte, Spaltenrechte und Policies gemeinsam kontrollieren. Neue Tabellen dürfen nicht unbeabsichtigt lesbar werden. Die Supabase-Anleitung zur API-Absicherung beschreibt diese Grenze.
  3. Alle Operationen: Lesen, Anlegen, Ändern, Löschen und Mandantenwechsel prüfen. Service-Role- oder administrative Schlüssel dienen nicht als erfolgreiche Nutzerkontrolle.
  4. Weitere Datenwege: Views, RPCs, Dateianhänge, Storage-Regeln, Exporte und Hintergrundjobs mit den fachlichen Rechten abgleichen. Ein grüner Kontakt-Endpunkt belegt diese Wege noch nicht.
  5. Entzug: Mitgliedschaft entfernen und mit dem vorhandenen Token erneut aufrufen. Die lokale Beobachtung gilt nur für die dynamische Mitgliedschaftsprüfung dieser Fixture; sie belegt keine allgemeine Sitzungssperre.
  6. Nachweis: Schema-/Migrationsstand, Umgebung, Rollen, erwartete und beobachtete Ergebnisse sowie bereinigte Protokolle festhalten. Nach einer Änderung der Policies die passenden Gegenproben wiederholen.

Wo liegen die Grenzen des Beispiels?

Das Modell enthält eine Kontakttabelle und eine Mitgliedschaftstabelle. Es prüft keine vollständige CRM-Geschäftslogik, keine Partnerverwaltung, keine mehrstufigen Datenbeziehungen und keine konkurrierenden Rechteänderungen. Beim Anlegen eines Kontakts kann ein Mitarbeiter partner_id mitgeben; die Zugehörigkeit dieses Partners zur Organisation wird dabei nicht geprüft. Die Leseregel verlangt weiterhin eine passende Mitgliedschaft. Ein nachträglicher Wechsel der Zuordnung ist über den REST-Endpunkt gesperrt. Ein reales System braucht für Anlage und Änderung von Partnerzuordnungen einen vollständig autorisierten und geprüften Ablauf.

Views oder Funktionen mit erhöhten Rechten können andere Zugriffswege eröffnen. Ebenso benötigen Dateiablagen und externe APIs eigene Kontrollen. Das lokale Beispiel verwendet weder Supabase Storage noch Realtime, Edge Functions oder dessen API-Key-Gateway. Die 43 bestandenen Prüfungen sind ein begrenzter, reproduzierbarer Nachweis. Sie sind keine Sicherheitsfreigabe für ein anderes Projekt.

Wo obhut beim Agenturprojekt ansetzt

RLS gehört zur Kundenanwendung. Ein vorgeschalteter Proxy oder ein geschütztes Frontend konfiguriert keine Datenbankregeln und schützt eine separat erreichbare Supabase-API nicht automatisch. Der Leitfaden zum sicheren Betrieb von Vibe-Coding-Anwendungen ordnet diese Verantwortung zusammen mit Anmeldung, Secrets, Backups und Übergabe ein.

obhut entwickelt eine Plattform für Agenturen mit Websites, SaaS, Portalen und internen Tools. Statische Deployments, DNS, Zertifikate sowie Proxy- und Tunnelgrundlagen sind implementiert; der aktuelle Stand ist ein interner Pilot. Access für Kundenanwendungen, WAF und Runtime-Hosting sind geplant. Die Hosting-Beta ist noch nicht extern freigegeben. obhut bietet mit diesem Beispiel kein Managed Supabase und keinen automatisch ausgeführten Sicherheitscheck Ihrer Anwendung an.

Für ein Gespräch über Ihr Kundenprojekt nennen Sie Anwendung, Laufzeit, Datenhaltung, Benutzergruppen und den offenen Betriebsschritt. Ein konkreter Datenweg und eine Rechtematrix helfen dabei, den Bedarf einzuordnen.

Quellen und Prüfstand

Primärquellen geprüft am 28. September 2026. Der lokale Ergebnisbericht benennt die tatsächlich verwendeten Versionen; daraus folgt keine Gleichheit mit den Versionen eines Supabase-Projekts.

Ihr Kundenprojekt. Der nächste Betriebsschritt.

Ein CRM, Kundenportal oder internes Tool steht vor der Übergabe? Besprechen wir Anwendung, Zugriffe und Verantwortung anhand Ihres konkreten Projekts.