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.
| 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.
| 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.
- 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.
- 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.
- Alle Operationen: Lesen, Anlegen, Ändern, Löschen und Mandantenwechsel prüfen. Service-Role- oder administrative Schlüssel dienen nicht als erfolgreiche Nutzerkontrolle.
- 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.
- 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.
- 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.