Skip to content

Matches page titles and page text. Forty pages, indexed at build time.

Privileges

How to grant an application role only what it needs, including the default privileges step that people forget and then debug for an afternoon.

Grant an application role only what it uses, and let a separate migration role own the schema. Everything on this page is ordinary SQL your managed elevated role can run.

There is one step people miss, and it fails a week later rather than at the time. It is the third section down.

The shape that works

Own the schema with a migration role, and grant the application role only what it uses.

sql
-- Ownership sits with the role that runs migrations.
CREATE ROLE app_migrator LOGIN PASSWORD 'REDACTED-EXAMPLE-ONLY';
CREATE SCHEMA app AUTHORIZATION app_migrator;

-- The application role connects and uses, and owns nothing.
CREATE ROLE app_user LOGIN PASSWORD 'REDACTED-EXAMPLE-ONLY';
GRANT CONNECT ON DATABASE appdb TO app_user;
GRANT USAGE ON SCHEMA app TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA app TO app_user;

The step everyone forgets

GRANT ... ON ALL TABLES grants on the tables that exist right now. The next migration creates a table your application cannot read, and the error appears in production rather than in the migration.

Set default privileges so future objects are covered:

sql
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA app
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA app
  GRANT USAGE ON SEQUENCES TO app_user;

FOR ROLE app_migrator matters. Default privileges attach to the role that creates the object, so setting them for the wrong role produces exactly the same failure with an extra hour of confusion.

Revoke the public schema

New databases give everyone rights on public. If you are using your own schema, take them away:

sql
REVOKE ALL ON SCHEMA public FROM PUBLIC;

Read-only, properly

A reporting role that is genuinely read-only needs the default privileges treatment too, or it starts failing the week after the next migration:

sql
CREATE ROLE app_readonly LOGIN PASSWORD 'REDACTED-EXAMPLE-ONLY';
GRANT CONNECT ON DATABASE appdb TO app_readonly;
GRANT USAGE ON SCHEMA app TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readonly;

ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA app
  GRANT SELECT ON TABLES TO app_readonly;

What you will see

Check what a role actually holds:

sql
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.table_privileges
WHERE grantee = 'app_user'
ORDER BY table_schema, table_name;

One row per table per privilege. An application role set up as above shows SELECT, INSERT, UPDATE and DELETE on each table in its schema, and nothing in public.

Default privileges are in a different catalogue, because they are grants on objects that do not exist yet:

sql
SELECT pg_get_userbyid(defaclrole) AS granted_by,
       defaclnamespace::regnamespace AS schema,
       defaclobjtype AS object_type,
       defaclacl AS grants
FROM pg_default_acl;

An empty result here is the usual cause of "the application could read the table yesterday".

Troubleshooting

permission denied for schema app. The role has no USAGE on the schema. CONNECT on the database is not enough.

permission denied for table, on a table created by the last migration. Default privileges are missing, or they were set FOR ROLE the wrong role. They attach to the role that creates the object, so set them for your migration role.

permission denied for sequence. An INSERT into a table with a bigserial column needs USAGE on its sequence. Grant it, and add sequences to the default privileges too.

A read-only role can still write. Check it did not inherit through another role, and check public. A new database grants everyone rights on public until you revoke them.