Skip to content

Fixed documentation about PostgreSQL read-only user's default privileges - #521

Closed
LouisKottmann wants to merge 1 commit into
ankane:masterfrom
LouisKottmann:fix-pg-readonly-user
Closed

Fixed documentation about PostgreSQL read-only user's default privileges#521
LouisKottmann wants to merge 1 commit into
ankane:masterfrom
LouisKottmann:fix-pg-readonly-user

Conversation

@LouisKottmann

Copy link
Copy Markdown
Contributor

After many years of scratching my head about why new tables remained un-selectable from a PostgreSQL read-only user that had been given the likes of :

ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO blazer;

I finally figured it out (with help from our LLM friends).

It was in the docs all this time : turns out, if you call ALTER DEFAULT PRIVILEGES ... without specifying FOR ROLE, the automatic privilege (the GRANT/REVOKE part in the right side of the command) is only applied when triggered (a table being created in our case) by the user who altered the default privileges.

In other words, if you run ALTER DEFAULT PRIVILEGES ... with the postgres user (common for bare-metal installs when managing users creations / rights), only tables created by the postgres user will trigger the GRANT SELECT ON TABLES TO blazer.

And since people (at least me I guess) tend to run migrations as the app user (or even a dedicated user with elevated DDL rights when using a bouncer for example), the tables created over time are not granted as selectable to the blazer user.

The fix is in the PR, it's only a small documentation fix.

Here is a minimal reproduction in SQL :

> psql
psql (18.4)
Type "help" for help.
postgres=# DROP DATABASE IF EXISTS repro_db;
NOTICE:  database "repro_db" does not exist, skipping
DROP DATABASE
postgres=# DROP ROLE IF EXISTS app_owner;
NOTICE:  role "app_owner" does not exist, skipping
DROP ROLE
postgres=# DROP ROLE IF EXISTS blazer;
NOTICE:  role "blazer" does not exist, skipping
DROP ROLE
postgres=# CREATE ROLE app_owner WITH LOGIN PASSWORD 'owner_pass';
CREATE ROLE
postgres=# CREATE ROLE blazer WITH LOGIN PASSWORD 'blazer_pass';
CREATE ROLE
postgres=# CREATE DATABASE repro_db OWNER app_owner;
CREATE DATABASE
postgres=# \c repro_db postgres
You are now connected to database "repro_db" as user "postgres".
repro_db=# GRANT USAGE ON SCHEMA public TO blazer;
GRANT
repro_db=# --
repro_db=# -- current blazer docs recommendation --
repro_db=# --
repro_db=# ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO blazer;
ALTER DEFAULT PRIVILEGES
repro_db=# \c repro_db app_owner
You are now connected to database "repro_db" as user "app_owner".
repro_db=> CREATE TABLE table_wrong (id int);
CREATE TABLE
repro_db=> \c repro_db blazer
You are now connected to database "repro_db" as user "blazer".
repro_db=> SELECT 'TEST 1 (Wrong implementation) - Has SELECT? ' AS check,
       has_table_privilege('blazer', 'table_wrong', 'SELECT') AS result;
                    check                     | result
----------------------------------------------+--------
 TEST 1 (Wrong implementation) - Has SELECT?  | f
(1 row)
repro_db=# --
repro_db=# -- did not grant SELECT to table created by the app user
repro_db=# --
repro_db=> \c repro_db postgres
You are now connected to database "repro_db" as user "postgres".
repro_db=# --
repro_db=# -- The fix : specify we alter privileges for the app user changes in schema public
repro_db=# --
repro_db=# ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public GRANT SELECT ON TABLES TO blazer;
ALTER DEFAULT PRIVILEGES
repro_db=# \c repro_db app_owner
You are now connected to database "repro_db" as user "app_owner".
repro_db=> CREATE TABLE table_fixed (id int);
CREATE TABLE
repro_db=> \c repro_db blazer
You are now connected to database "repro_db" as user "blazer".
repro_db=> SELECT 'TEST 2 (Fixed implementation) - Has SELECT? ' AS check,
       has_table_privilege('blazer', 'table_fixed', 'SELECT') AS result;
                    check                     | result
----------------------------------------------+--------
 TEST 2 (Fixed implementation) - Has SELECT?  | t
(1 row)
repro_db=# --
repro_db=# -- select rights automatically granted
repro_db=# --
repro_db=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=# DROP DATABASE repro_db;
DROP ROLE app_owner;
DROP ROLE blazer;

…ges, that only applied to tables created by the postgres user that altered the default privileges (often the superadmin)
@ankane ankane closed this in 61a82e4 Jun 23, 2026
@ankane

ankane commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Hi @LouisKottmann, thanks for the suggestion. Improved the example in the commit above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants