Sicherheitsregeln (RLS)
Übersicht
Abschnitt betitelt „Übersicht“Mit Sicherheitsregeln können Sie Row Level Security (RLS)-Richtlinien für Ihre PostgreSQL-Tabellen direkt in Ihren Collection-Definitionen festlegen. Wenn das Drizzle-Schema generiert wird, erstellt Rebase die entsprechenden CREATE POLICY-Anweisungen.
import { defineCollection } from "@rebasepro/cms-types";const postsCollection = defineCollection({ slug: "posts", name: "Posts", table: "posts", properties: { /* ... */ }, securityRules: [ { operation: "select", access: "public" }, { operations: ["insert", "update", "delete"], ownerField: "authorId" } ]});Wie es funktioniert
Abschnitt betitelt „Wie es funktioniert“- Sie definieren
securityRulesin einer Collection rebase schema generateerstellt das Drizzle-Schema mit aktiviertem RLSrebase db pushoderrebase db migratewendet die Richtlinien auf PostgreSQL an- Jede Abfrage wird automatisch nach dem Kontext des aktuellen Benutzers gefiltert
Die Identität des authentifizierten Benutzers ist in SQL über Folgendes verfügbar:
| Funktion | Rückgabewert |
|---|---|
rebase.uid() |
Die ID des aktuellen Benutzers |
rebase.roles() |
Kommagetrennte App-Rollen-IDs |
rebase.jwt() |
Vollständige JWT-Claims als JSONB |
Diese werden vom Rebase-Backend automatisch pro Transaktion gesetzt.
Policies, die eine Collection erhält, ohne sie zu schreiben
Abschnitt betitelt „Policies, die eine Collection erhält, ohne sie zu schreiben“Die Policies einer Tabelle sind mehr als die securityRules der Collection selbst:
defaultSecurityRules, exportiert ausconfig/collections/index.ts. Eine Collection, derensecurityRulesfehlt oder leer ist, übernimmt sie alle an deren Stelle, als hätte sie sie selbst geschrieben. Eine Collection, die auch nur eine eigene Regel deklariert, übernimmt keine: Die beiden Listen werden nie zusammengeführt. Sie werden überall angewendet, wo das Verzeichnis gelesen wird:rebase dev,rebase db push, das Admin-Panel und ein Backend, demcollectionsDirübergeben wird. Der Index des Scaffolds wendet sie außerdem auf dascollections-Array an, das er exportiert, sodass auch ein Backend, dem dieses Array übergeben wird, sie erhält. Ein auf andere Weise zusammengestelltes Array trägt nur die Regeln, die seine Collections deklarieren.- Die Admin-Basis. Jede Collection erhält außerdem
<table>_default_admin_readund<table>_default_admin_write_insert,_updateund_delete: permissive Policies, die den Server-Kontext und die Rolleadminjede Zeile lesen und schreiben lassen. Sie stehen neben den Regeln, die die Collection am Ende hat, ihren eigenen oder den geerbten Defaults, und permissive Policies werden per OR verknüpft, sodass sie beide erweitern. Die Users-Collection erhält zudem ein Self-Read und ein restriktives Schreib-Gate, dasrolesAdmins vorbehält. - Mandantenfähigkeit. Eine mandantengebundene Collection erhält eine restriktive Regel, die jede Operation auf den Mandanten des Aufrufers beschränkt; der Server-Kontext besteht sie.
disableDefaultPolicies: true an einer Collection entfernt die permissive Basis
und behält die restriktiven Gates. Es gilt pro Collection: defaultSecurityRules
trägt kein solches Flag, sodass eine Collection, die die Defaults erbt, die
Basis behält, sofern sie das Flag nicht selbst setzt. Das generierte DDL markiert jede injizierte
Policy, und rebase db push stellt eine von Hand gelöschte wieder her.
Praktische Shortcuts
Abschnitt betitelt „Praktische Shortcuts“Besitzerbasierter Zugriff
Abschnitt betitelt „Besitzerbasierter Zugriff“Das einfachste Muster — Benutzer können nur auf Zeilen zugreifen, die sie selbst besitzen:
securityRules: [ { operation: "all", ownerField: "user_id" }]Dies generiert: USING ((user_id)::text = (SELECT rebase.uid())) — zum SELECT siehe
Einmal pro Statement ausgewertet.
Öffentlicher Zugriff
Abschnitt betitelt „Öffentlicher Zugriff“Erlaubt jedem (einschließlich nicht authentifizierten Benutzern) das Lesen:
securityRules: [ { operation: "select", access: "public" }]Dies generiert: USING (true)
Authentifizierter Zugriff
Abschnitt betitelt „Authentifizierter Zugriff“Erlaubt den Zugriff für jeden angemeldeten Benutzer. Dies ist eine condition und kein access-Shortcut — access hat genau einen Wert, nämlich "public" —, da „angemeldet“ eine Prüfung des Aufrufers darstellt und der Builder der Ort ist, an dem Prüfungen gegen den Aufrufer stattfinden:
import { policy } from "@rebasepro/types";
securityRules: [ { operation: "select", condition: policy.authenticated() }]policy.authenticated() ist auch für anonyme Anmeldungen (sign-in) wahr, bei denen eine echte Benutzerzeile und eine echte Sitzung erzeugt werden. Verwenden Sie policy.registered(), wenn ein Gast sich nicht qualifizieren soll — etwa beim Verfassen einer Bewertung, beim Beitreten zu einer Organisation oder beim Ausgeben von Geld.
Rollenbasierter Zugriff
Abschnitt betitelt „Rollenbasierter Zugriff“Beschränken Sie Operationen auf bestimmte Rollen:
securityRules: [ { operation: "all", roles: ["admin"] }, { operation: "select", roles: ["editor", "viewer"] }]Mitgliedschaftsbasierter / Relationaler Zugriff
Abschnitt betitelt „Mitgliedschaftsbasierter / Relationaler Zugriff“Um den Zugriff basierend auf der Mitgliedschaft in einer verwandten Collection einzugrenzen — z. B. „nur Zeilen, zu deren Team der Aufrufer gehört“ —, verwenden Sie die strukturierte condition mit policy.existsIn. Dies wird zu einer einzelnen korrelierten EXISTS-Unterabfrage kompiliert (keine Abfragen pro Zeile) und ist die sichere First-Class-Alternative zum manuellen Schreiben des unten gezeigten reinen SQLs.
import { policy } from "@rebasepro/types";
// documents visible only to members of the document's team:securityRules: [ { operation: "select", condition: policy.existsIn({ collection: "team_members", // the join / membership collection where: policy.and( // correlate to the row being checked: policy.compare(policy.field("team_id"), "eq", policy.outerField("team_id")), // …and to the caller: policy.compare(policy.field("user_id"), "eq", policy.authUid()), ), }), },]Innerhalb von where bezieht sich policy.field(...) auf eine Spalte der verknüpften Collection (team_members), während sich policy.outerField(...) auf eine Spalte der zu prüfenden Zeile (documents) bezieht. Kombinieren Sie dies mit policy.authUid(), um die Abfrage auf den aktuellen Benutzer einzugrenzen. Da dies von der Datenbank erzwungen wird, betrachtet die Admin-UI dies als serverseitig maßgeblich.
Der policy-Builder im Detail
Abschnitt betitelt „Der policy-Builder im Detail“Wird aus @rebasepro/types importiert. Ausdrücke lassen sich zusammensetzen; Operanden bilden die Blätter.
| Ausdruck | Kompiliert zu |
|---|---|
policy.true() / policy.false() |
true / false |
policy.and(…) / policy.or(…) |
Konjunktion / Disjunktion |
policy.not(e) |
Negation |
policy.compare(left, op, right) |
Ein Vergleich zwischen zwei Operanden |
policy.rolesOverlap(roles) |
Der Aufrufer hat mindestens eine dieser App-Rollen |
policy.rolesContain(roles) |
Der Aufrufer hat alle dieser App-Rollen |
policy.authenticated() |
Angemeldet — rebase.uid() ist gesetzt und ist kein anonymer Sentinel-Wert. IS NOT NULL allein wäre eine Tautologie, da ein anonymer Request einen Sentinel-Wert setzt, anstatt ihn ungesetzt zu lassen |
policy.registered() |
Mit einem Konto angemeldet — authenticated() und kein Gast. Siehe unten |
policy.serverContext() |
rebase.uid() IS NULL — siehe den Warnhinweis unten |
policy.existsIn({ collection, where }) |
Eine korrelierte EXISTS-Unterabfrage |
policy.raw(sql) |
Ein Escape-Hatch, wird unverändert eingefügt |
| Operand | Bedeutung |
|---|---|
policy.field(name) |
Eine Spalte der zu prüfenden Collection — oder, innerhalb von existsIn, der gejointen Collection |
policy.outerField(name) |
Innerhalb von existsIn, eine Spalte der äußeren Zeile |
policy.literal(value) |
Ein String, eine Zahl, ein Boolean oder null |
policy.authUid() |
rebase.uid() |
policy.authRoles() |
rebase.roles() |
Einmal pro Statement ausgewertet
Abschnitt betitelt „Einmal pro Statement ausgewertet“Jeder Teil einer Regel, der die Zeile nicht liest — die ID des Aufrufers, eine
Rollenprüfung, ein Claim, serverContext() — wird innerhalb einer skalaren
Unterabfrage kompiliert. Eine Besitzerregel wird zu
(user_id)::text = (SELECT rebase.uid()), und der standardmäßige Admin-Read wird zu
(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin'])).
Postgres berechnet eine solche Unterabfrage einmal pro Statement (ein initPlan) statt einmal pro Zeile und plant so, als ließe die Prüfung die Hälfte der Tabelle durch statt 1,5%. Diese Schätzung hält eine tiefe Seite einer großen Tabelle auf ihrem Primärschlüssel: Ohne sie waren es bei Seite 401 von einer Million Zeilen ein sequenzieller Scan und eine Sortierung mit 640 MB. Wer was lesen oder schreiben darf, bleibt unverändert.
Rohes using / withCheck-SQL erhält dieselbe Behandlung, wenn Rebase seine Struktur
lesen kann (owner_id = rebase.uid() gehört dazu). Alles andere wird unverändert
eingefügt, umschließen Sie die Aufrufe also selbst: (SELECT rebase.uid()).
authenticated() und registered()
Abschnitt betitelt „authenticated() und registered()“Zwei unterschiedliche Dinge werden als anonym bezeichnet, und es lohnt sich, genau zu unterscheiden, was eine Regel meint.
Ein nicht authentifizierter Request besitzt keinerlei Session. Ihm wird eine Sentinel-ID zugewiesen, sodass rebase.uid() auf dem Benutzerpfad niemals NULL ist, und policy.authenticated() schließt ihn aus — genau das sorgt dafür, dass es „angemeldet“ bedeutet und nicht „jeder“.
Ein Gast ist das andere: eine Session ohne eine reale Person dahinter. POST /auth/anonymous generiert eine echte Benutzerzeile mit einer echten UID, sodass ein Gast jeden Test besteht, der die ID prüft. Das ist der Sinn des Features — ein Warenkorb vor dem Checkout, ein Entwurf vor der Registrierung — und es bedeutet, dass authenticated() für jeden wahr ist, der auf Als Gast fortfahren geklickt hat, was weder E-Mail noch Passwort noch die Zustimmung zu irgendetwas erfordert.
policy.registered() ist authenticated() plus „kein Gast“. Verwenden Sie es überall dort, wo es in einer Regel um eine Person geht, die für etwas zur Verantwortung gezogen werden könnte: das Verfassen einer Bewertung, der Beitritt zu einer Organisation, das Ausgeben von Geld. Greifen Sie zu authenticated(), wenn Gäste ausdrücklich willkommen sind.
// Anyone with a session, guests included — a draft cart.{ operation: "insert", check: policy.authenticated() }
// Someone with an account.{ operation: "insert", check: policy.registered() }Unter der Haube reist das Gast-Flag mit der Session mit — es befindet sich im Access Token und gelangt als rebase.is_anonymous() in die Datenbank —, sodass eine Richtlinie diese Frage ohne Lookup stellen kann. Eine Datenbank, die von einem Server bedient wird, der zu alt ist, um dies zu setzen, interpretiert jede Session als Konto; genau dieses Verhalten hatte das betreffende Deployment bereits zuvor.
Reine SQL-Ausdrücke
Abschnitt betitelt „Reine SQL-Ausdrücke“Für komplexe Logik verwenden Sie using und withCheck:
securityRules: [ { operation: "select", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = rebase.uid())" }]using— Filtert, welche bestehenden Zeilen sichtbar sind (gilt für SELECT, UPDATE, DELETE)withCheck— Validiert neue Zeilenwerte (gilt für INSERT, UPDATE)
Spaltenreferenzen verwenden die Syntax {column_name}, die zur vollständig tabellenqualifizierten Spalte aufgelöst wird.
Shortcuts und SQL kombinieren
Abschnitt betitelt „Shortcuts und SQL kombinieren“Kombinieren Sie praktische Shortcuts mit reinem SQL:
securityRules: [ // Admins can do anything { operation: "all", roles: ["admin"], using: "true" }, // Regular users can only see their own rows { operation: "select", ownerField: "user_id" }, // Users can insert, but only for themselves { operation: "insert", withCheck: "{user_id} = rebase.uid()" }, // Locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false" }]Permissiv vs. Restriktiv
Abschnitt betitelt „Permissiv vs. Restriktiv“PostgreSQL bietet zwei Richtlinien-Modi:
- Permissive (Standard) — Mehrere permissive Richtlinien werden mit ODER (OR) verknüpft. Wenn eine davon zutrifft, wird der Zugriff gewährt.
- Restrictive — Restriktive Richtlinien werden mit UND (AND) verknüpft. Alle müssen zutreffen.
securityRules: [ // Permissive: owners can access their rows { operation: "all", ownerField: "user_id" }, // Restrictive: but locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false", withCheck: "{is_locked} = false" }]Operationen
Abschnitt betitelt „Operationen“| Operation | SQL-Äquivalent | Beschreibung |
|---|---|---|
"select" |
SELECT |
Zeilen lesen |
"insert" |
INSERT |
Neue Zeilen erstellen |
"update" |
UPDATE |
Bestehende Zeilen ändern |
"delete" |
DELETE |
Zeilen entfernen |
"all" |
Alle oben genannten | Kurzform für alle Operationen |
Sie können auch operations (Plural) verwenden, um eine Regel auf mehrere Operationen anzuwenden:
{ operations: ["insert", "update", "delete"], ownerField: "authorId" }Vollständiges SecurityRule-Interface
Abschnitt betitelt „Vollständiges SecurityRule-Interface“SecurityRule ist eine Union, kein einzelnes offenes Objekt: Eine Regel wählt genau eine Möglichkeit, ihr Prädikat auszudrücken; die anderen sind mit never typisiert, sodass ihre Vermischung ein Compilerfehler ist, anstatt einer Richtlinie, die die Hälfte dessen, was Sie geschrieben haben, stillschweigend ignoriert.
// Shared by every variantinterface SecurityRuleBase { name?: string; // Policy name. Omit it and one is derived operation?: SecurityOperation; // "select" | "insert" | "update" | "delete" | "all" operations?: SecurityOperation[]; // …or several at once mode?: "permissive" | "restrictive"; // Default: "permissive" roles?: string[]; // App roles, via rebase.roles() pgRoles?: string[]; // Native Postgres roles — the CREATE POLICY `TO` clause. // NOT the same as `roles`. Default: ["public"]}
// …plus exactly one of:{ ownerField: string } // <column> = rebase.uid(){ access: "public" } // the one shortcut — "no row filter"{ condition: PolicyExpression; // the structured builder — `policy.*` check?: PolicyExpression } // defaults to `condition`, as Postgres does{ using?: string; withCheck?: string } // raw SQLroles und pgRoles werden häufig verwechselt. roles ist eine Anwendungsrolle, die innerhalb der USING- / WITH CHECK-Klausel über rebase.roles() erzwungen wird. pgRoles ist eine Datenbankrolle und steuert, an welche Verbindungen die Richtlinie überhaupt angehängt wird. Fast jedes Projekt benötigt roles.
Beispiele
Abschnitt betitelt „Beispiele“Blog-Plattform
Abschnitt betitelt „Blog-Plattform“securityRules: [ // Anyone can read published posts { operation: "select", using: "{status} = 'published'" }, // Authors can see their own drafts { operation: "select", ownerField: "authorId" }, // Authors can create and edit their own posts { operations: ["insert", "update"], ownerField: "authorId" }, // Only admins can delete { operation: "delete", roles: ["admin"] }]Mandantenfähiges SaaS
Abschnitt betitelt „Mandantenfähiges SaaS“securityRules: [ { operation: "all", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = rebase.uid())" }]Anonymer Zugriff (Öffentliche Inserts)
Abschnitt betitelt „Anonymer Zugriff (Öffentliche Inserts)“Eine häufige Anforderung besteht darin, nicht authentifizierten Benutzern das Übermitteln von Daten zu erlauben — Kontaktformulare, Newsletter-Anmeldungen, öffentliche Bewerbungen. Rebase bietet hierfür ein sauberes Muster.
Empfohlen: Eine reine withCheck-Regel
Abschnitt betitelt „Empfohlen: Eine reine withCheck-Regel“import { defineCollection } from "@rebasepro/cms-types";
const contactMessagesCollection = defineCollection({ slug: "contact_messages", name: "Contact Messages", table: "contact_messages", securityRules: [ // Anyone can submit a contact message { operation: "insert", // A raw rule carries `using` (which rows are visible) and `withCheck` // (what a write must satisfy); an insert only exercises the latter. using: "true", withCheck: "true" }, // Only admins can read, update, or delete messages { operations: ["select", "update", "delete"], roles: ["admin"] } ], properties: { email: { name: "Email", type: "string" } }});Der access: "public"-Shortcut generiert eine Richtlinie, die die Operation ohne erforderliche Authentifizierung erlaubt.
Für Lead-Erfassung / Registrierungen
Abschnitt betitelt „Für Lead-Erfassung / Registrierungen“import { defineCollection } from "@rebasepro/cms-types";
const leadSignupsCollection = defineCollection({ slug: "lead_magnet_signups", name: "Lead Magnet Signups", table: "lead_magnet_signups", securityRules: [ // Allow anonymous inserts { operation: "insert", using: "true", withCheck: "true" }, // Admins can view all signups { operation: "select", roles: ["admin"] } ], properties: { email: { name: "Email", type: "string" } }});Wie anonyme Anfragen funktionieren
Abschnitt betitelt „Wie anonyme Anfragen funktionieren“Wenn eine Anfrage ohne JWT-Token eintrifft, setzt das Rebase-Backend die PostgreSQL-Sitzungsvariablen auf:
| Variable | Wert |
|---|---|
app.user_id |
'anonymous' |
app.user_roles |
'' (leer) |
Das bedeutet:
rebase.uid()gibt'anonymous'zurückrebase.roles()gibt einen leeren String zurückaccess: "public"-Richtlinien sind erfolgreich, da sieUSING (true)/WITH CHECK (true)erzeugenpolicy.authenticated()-Bedingungen schlagen fehl, da sie nach einer echten Benutzer-ID prüfenownerField-Richtlinien schlagen fehl, da keine Zeileuser_id = 'anonymous'haben wird (sofern nicht explizit festgelegt)
Erweitert: Reines SQL für anonyme Zugriffe
Abschnitt betitelt „Erweitert: Reines SQL für anonyme Zugriffe“Wenn Sie eine granularere Kontrolle benötigen, verwenden Sie reines SQL:
securityRules: [ { operation: "insert", withCheck: "rebase.uid() = 'anonymous' OR rebase.uid() IS NOT NULL" }]Zeilen hier, Felder nebenan
Abschnitt betitelt „Zeilen hier, Felder nebenan“Sicherheitsregeln beantworten eine Frage: Welche Zeilen erreicht dieser Aufrufer. Sie werden von Postgres selbst bei jeder Anweisung durchgesetzt, unabhängig von der Route — weshalb sie das Autorisierungsmodell darstellen und alles darüber reine Bequemlichkeit ist.
Sie sagen nichts über die Spalten einer Zeile aus, die ein Aufrufer tatsächlich erreicht. Eine Richtlinie, die es einem Mitarbeiter erlaubt, die Zeilen seines Teams zu lesen, erlaubt es ihm, jedes Feld dieser Zeilen zu lesen, einschließlich des Gehalts. Genau dafür gibt es das Eigenschafts-basierte access:
salary: { type: "number", // Everyone the rules above let read the row; only HR gets this column. access: { read: ["hr"], write: [] }}Beide ergänzen sich und widersprechen sich nie: Eine Feldregel kann den Zeilenzugriff nicht erweitern, und eine Zeile, die Sie nicht lesen können, besitzt keine Felder, über die man sprechen könnte. Rollen sind dieselben Rollen — rebase.roles() innerhalb einer Richtlinie, user.roles im Request —, sodass rolesOverlap(['hr']) in einer Regel und access: { read: ["hr"] } auf einer Eigenschaft dasselbe hr bedeuten. Feldregeln werden vom Server und nicht von Postgres angewendet, sodass sie die API-Oberfläche abdecken; eine Abfrage, die über rebase.sql() ausgeführt wird, sieht jede Spalte, genau so, wie sie RLS umgeht.
Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- Feldzugriff — Lese-/Schreibrollen pro Feld
- Relationen — Fremdschlüssel und Joins
- Entity-Callbacks — Lifecycle-Hooks
- Custom Functions — Benutzerdefinierte API-Endpunkte