Eine bestehende Datenbank hat schon Tabellen — und du willst einer neuen Rolle nachträglich Rechte darauf vergeben. PostgreSQL kennt dafür Bulk-Befehle. Dieser Artikel zeigt, was sie tun, was sie nicht tun, und warum man fast immer noch ein ALTER DEFAULT PRIVILEGES dazustellt.

Die drei Bulk-Varianten

SQL GRANT auf alle Objekte eines Typs in einem Schema
GRANT SELECT ON ALL TABLES    IN SCHEMA app TO reader;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO reader;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA app TO reader;

ALL TABLES deckt: ordentliche Tabellen, partitionierte Tabellen, Views, Materialized Views, Foreign Tables — alles, was unter pg_class.relkind in ('r','p','v','m','f') fällt. Indexe und Sequenzen sind separat.

Vorausgesetzt: USAGE auf das Schema

Ohne USAGE auf das umgebende Schema sieht der Reader gar nichts — auch wenn er auf alle Tabellen darin SELECT-Rechte hat:

SQL
GRANT USAGE ON SCHEMA app TO reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader;

USAGE ist quasi „Tür auf zum Raum”; SELECT ist „darfst die Bücher im Raum lesen”. Beides ist nötig.

Der häufigste Fehler: zukünftige Objekte fehlen

GRANT … ON ALL TABLES IN SCHEMA … wirkt nur auf existierende Tabellen — eine reine Stichtags-Operation:

SQL So sieht der Bug aus
-- 2026-05-06: GRANT setzen
GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader;

-- 2026-05-15: Migration legt eine neue Tabelle an
CREATE TABLE app.invoices (...);

-- reader versucht zu lesen:
SELECT * FROM app.invoices;
-- ERROR: permission denied for table invoices

Die invoices-Tabelle existierte am 6. Mai noch nicht — der Bulk-GRANT konnte sie nicht erfassen. Damit zukünftige Tabellen automatisch SELECT bekommen, brauchst du zusätzlich:

SQL
ALTER DEFAULT PRIVILEGES IN SCHEMA app
    GRANT SELECT ON TABLES TO reader;

Faustregel: wann immer du einen Bulk-GRANT machst, kommt ein passender ALTER DEFAULT PRIVILEGES direkt dahinter. Beide gehören zusammen.

Komplettes Read-Only-Setup

Wenn du eine Read-Only-Rolle einrichten willst, sind das die fünf Befehle, die zusammen funktionieren:

SQL Read-Only — vollstaendig
-- 1. Schema-Zugang
GRANT USAGE ON SCHEMA app TO reader;

-- 2. Bestehende Tabellen
GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader;

-- 3. Bestehende Sequenzen
GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO reader;

-- 4. Zukuenftige Tabellen
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT SELECT ON TABLES TO reader;

-- 5. Zukuenftige Sequenzen
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT SELECT ON SEQUENCES TO reader;

Wichtig: das FOR ROLE app_owner muss zur Rolle passen, die deine Migrations-Tools nutzen. Sonst greifen die Defaults nicht (siehe Default Privileges).

Read-Write-Setup mit korrekten Sequenz-Rechten

Bei Schreibrechten ist die Sequenz-Falle besonders gemein. Eine Tabelle mit serial/identity-Spalte braucht beim INSERT Zugriff auf die zugehörige Sequenz:

SQL Read-Write — bestehende Objekte
GRANT USAGE ON SCHEMA app TO writer;
GRANT SELECT, INSERT, UPDATE, DELETE
    ON ALL TABLES IN SCHEMA app TO writer;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO writer;

Ohne GRANT USAGE ON ALL SEQUENCES:

SQL
myapp=> INSERT INTO orders (total) VALUES (99.95);
ERROR:  permission denied for sequence orders_id_seq

USAGE lässt nextval() aufrufen, SELECT lässt aktuelle Position lesen — bei serial-Spalten beides gebraucht.

Privilegien zurückziehen

Symmetrisch:

SQL
REVOKE SELECT ON ALL TABLES IN SCHEMA app FROM reader;
REVOKE USAGE  ON SCHEMA app FROM reader;

Aber Vorsicht: das wirkt nur auf existierende Objekte. Default-Privileges bleiben. Wenn der Reader auch in Zukunft keine SELECT-Rechte mehr bekommen soll:

SQL
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    REVOKE SELECT ON TABLES FROM reader;

Privilegien-Inspektion nach Bulk-GRANT

SQL Welche Tabellen hat reader was?
SELECT n.nspname  AS schema,
       c.relname  AS table,
       has_table_privilege('reader', c.oid, 'SELECT') AS can_select,
       has_table_privilege('reader', c.oid, 'INSERT') AS can_insert
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'app' AND c.relkind = 'r'
ORDER BY c.relname;

has_table_privilege ist die saubere Variante, weil sie auch transitiv geerbte Rechte berücksichtigt — anders als ein direkter Lookup in pg_class.relacl.

Häufige Stolperfallen

GRANT auf ALL TABLES wirkt nicht auf zukuenftige Tabellen.

Die häufigste Frustquelle: GRANTs werden gesetzt, am nächsten Tag legt eine Migration eine neue Tabelle an, und die Reader-Rolle bekommt „permission denied”. Lösung: ALTER DEFAULT PRIVILEGES mit dazustellen — Bulk-GRANT für Vergangenheit, DEFAULT für Zukunft.

USAGE auf Schema vergessen.

Ohne USAGE sieht der User die Tabellen gar nicht — auch nicht, dass sie existieren. Die Fehlermeldung lautet „permission denied for schema app”, was im Stress oft als „Tabelle existiert nicht” missverstanden wird. Erst Schema-Recht setzen, dann Tabellen-Rechte.

Sequenz-Rechte vergessen — Insert schlaegt fehl.

GRANT INSERT ON ALL TABLES … reicht nicht für eine Tabelle mit serial-Spalte. Postgres verteilt die Default-Werte aus der Sequenz, und dafür braucht der User USAGE (für nextval()). Wer das vergisst, sieht „permission denied for sequence …” und sucht stundenlang.

ALL FUNCTIONS umfasst keine Procedures (CREATE PROCEDURE).

Bis PG 13 reichte ALL FUNCTIONS aus, weil es keine echten Prozeduren gab. Seit PG 11 gibt es CREATE PROCEDURE — aber GRANT EXECUTE ON ALL FUNCTIONS deckt sie NICHT ab. Es braucht zusätzlich GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA …. Häufig übersehen.

GRANT FROM einem User, der spaeter gedroppt wird, hinterlaesst Schaden.

Wer einen GRANT als app_owner setzt und dann app_owner droppt (auch nach REASSIGN OWNED), hinterlässt mitunter wirkungslose Privilegien-Einträge. Verwende für GRANTs immer eine Rolle, die langfristig existiert, oder räume vor dem Drop konsequent auf.

Owner taucht nicht in pg_class.relacl auf.

Wenn \dp orders keine Zeile mit dem Owner zeigt, ist das normal — Owner sind kein „Privileg” und stehen nicht in der ACL. Sie haben implizit alle Rechte. Das verwirrt beim ersten Audit, ist aber konsistent mit dem Postgres-Modell.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Server-Administration

Zur Übersicht