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 haeufigste „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