Ein normales GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader; wirkt nur auf Tabellen, die in diesem Moment existieren. Sobald die nächste Migration eine neue Tabelle anlegt, fängt der Reader-User dort wieder bei Null an. Genau dafür gibt es ALTER DEFAULT PRIVILEGES: einmal eingerichtet, gilt die Regel automatisch für alle künftigen Objekte.
Das Problem in einem Beispiel
SET ROLE app_owner;
CREATE TABLE orders (id serial primary key);
GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader;
-- Spaeter, in einer neuen Migration:
CREATE TABLE customers (id serial primary key);
SET ROLE reader;
SELECT * FROM customers;
-- ERROR: permission denied for table customersreader hatte SELECT auf alle Tabellen, die zum Zeitpunkt des GRANT existierten — orders. Die später angelegte customers ist davon nicht betroffen.
Die Lösung: ALTER DEFAULT PRIVILEGES
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT ON TABLES TO reader;Übersetzt: „Ab jetzt — sobald jemand in Schema app eine neue Tabelle anlegt, soll reader automatisch SELECT bekommen."
Damit greift die Regel auf alle zukünftig erzeugten Tabellen. Die existierenden bleiben unverändert. Für die brauchst du weiterhin den klassischen GRANT SELECT ON ALL TABLES ….
Faustregel: bei einem neuen Read-Only-User immer beides ausführen:
-- 1. Bestehendes:
GRANT USAGE ON SCHEMA app TO reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO reader;
-- 2. Zukuenftiges:
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT ON TABLES TO reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT ON SEQUENCES TO reader;Worauf wirken Default Privileges?
Du kannst Defaults für die folgenden Objekttypen setzen:
| Objekttyp | Mögliche Privilegien |
|---|---|
TABLES (inkl. Views, Foreign Tables) | SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER |
SEQUENCES | USAGE, SELECT, UPDATE |
FUNCTIONS (inkl. Procedures) | EXECUTE |
TYPES | USAGE |
SCHEMAS (ab PG 15) | USAGE, CREATE |
Der wichtigste Stolperstein: GRANTOR
Defaults sind immer an eine bestimmte erzeugende Rolle gebunden — den GRANTOR. In Klartext: die Regel greift nur, wenn die neue Tabelle von derselben Rolle erstellt wird, die das ALTER DEFAULT PRIVILEGES ausgeführt hat.
-- Als app_owner:
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT ON TABLES TO reader;
-- Sind app_owner Migrationen, greift die Regel.
-- Wenn aber eine andere Rolle (z. B. ein Migrations-User
-- "migrator") die Tabellen erstellt:
SET ROLE migrator;
CREATE TABLE app.invoices (...);
-- reader bekommt KEIN SELECT auf invoices.Lösung: explizit für die richtige Rolle setzen.
ALTER DEFAULT PRIVILEGES FOR ROLE migrator IN SCHEMA app
GRANT SELECT ON TABLES TO reader;Wer mehrere Rollen Tabellen erzeugen lässt, braucht das ALTER DEFAULT PRIVILEGES FOR ROLE … für jede dieser Rollen. Praktischer ist, das im Migrations-Setup zu vereinheitlichen: eine Owner-Rolle für alle Migrationen, einmal Defaults setzen, fertig.
Pro Schema vs. global
Ohne IN SCHEMA gilt die Regel datenbankweit — für jede zukünftige Tabelle in jedem Schema:
ALTER DEFAULT PRIVILEGES
GRANT SELECT ON TABLES TO reader;Das ist häufig zu großzügig. In den meisten Setups gibt es ein App-Schema, ein paar Werkzeug-Schemas (migrations, tmp) und das public-Schema. Pro-Schema-Defaults sind expliziter und sicherer.
Defaults inspizieren
Im psql:
myapp=# \ddp
Default access privileges
Owner | Schema | Type | Access privileges
------------+--------+-------+---------------------------
app_owner | app | table | reader=r/app_owner
app_owner | app | seq | reader=r/app_ownerOder direkt im Katalog:
SELECT pg_get_userbyid(defaclrole) AS grantor,
nspname AS schema,
defaclobjtype AS object_type,
defaclacl AS privileges
FROM pg_default_acl
LEFT JOIN pg_namespace ON pg_namespace.oid = defaclnamespace;defaclobjtype-Werte: r = relation (Tabelle), S = sequence, f = function, T = type, n = schema.
Zurücknehmen
Symmetrisch:
ALTER DEFAULT PRIVILEGES IN SCHEMA app
REVOKE SELECT ON TABLES FROM reader;Wichtig: das wirkt nur auf die Defaults selbst (für künftige Objekte). Schon vergebene Privilegien auf existierende Tabellen bleiben — die musst du separat mit klassischem REVOKE zurückziehen.
Besonderheiten
Defaults sind keine retroaktive Wirkung.
Sie greifen ab dem Moment, ab dem du sie setzt — und nur auf Objekte, die danach entstehen. Wer nur ALTER DEFAULT PRIVILEGES setzt und das normale GRANT … ON ALL … weglässt, hat existierende Tabellen unangetastet gelassen. Beide gehören zusammen.
Pro GRANTOR — die häufigste „warum greift das nicht?“-Falle.
Wenn deine Migrationen unter migrator laufen, deine Default-Privileges aber als app_owner gesetzt wurden, passiert nichts. \ddp zeigt dir, welcher GRANTOR welche Defaults pflegt — wenn deine erstellende Rolle nicht in der Tabelle steht, ist das die Erklärung.
DROP OWNED BY entfernt auch Defaults der Rolle.
Wenn app_owner gedroppt wird (nach REASSIGN OWNED und DROP OWNED), verschwinden auch alle Default-Privilege-Definitionen, die unter seinem Namen liefen. Beim Aufräumen einer Rolle also vorher dokumentieren, welche Defaults sie pflegte — sonst muss das Setup beim nächsten Owner manuell wiederhergestellt werden.
In CI/Test-DBs Defaults gleich nach dem Anlegen setzen.
Wenn deine Test-DB nach jedem Lauf neu erstellt wird (etwa via docker-compose down -v), sind auch die Defaults weg. Setze sie als Teil des Bootstrap-Skripts (/docker-entrypoint-initdb.d/), sonst sind die ersten Migrationen ohne Reader-Rechte und Tests scheitern erst beim Read-Schritt.
Schema-Defaults gibt es erst seit PG 15.
ALTER DEFAULT PRIVILEGES … GRANT … ON SCHEMAS … ist neu. In früheren Versionen mussten Schemas einzeln per GRANT rechtefiziert werden. Wer noch auf älteren Versionen unterwegs ist, sollte beim Upgrade die Default-Privilege-Definitionen ergänzen.
Weiterführende Ressourcen
Externe Quellen
- ALTER DEFAULT PRIVILEGES – PostgreSQL Documentation
- pg_default_acl – PostgreSQL Documentation
- psql \ddp – Default access privileges output
- Release Notes 15: ALTER DEFAULT PRIVILEGES on schemas