PostgreSQL User & Role Management

PostgreSQL User & Role Management

Table of Contents



1. Users vs. Roles: There’s No Difference

The primary difference between CREATE ROLE and CREATE USER is that CREATE USER automatically includes the LOGIN privilege, allowing the user to connect to the database, whereas CREATE ROLE does not grant the LOGIN privilege unless it is explicitly specified.

PostgreSQL internally treats users and groups as roles. Therefore, using CREATE ROLE is often recommended. However, CREATE USER and CREATE GROUP are still supported and commonly used by DBAs. You can use whichever command you find easier to understand and manage.

Both users and roles are stored in the same PostgreSQL system catalog (pg_authid / pg_roles). However, the pg_user view displays only users  & roles with the LOGIN privilege, not all roles.

postgres=# CREATE USER rama;
CREATE ROLE
postgres=# SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'rama';
 rolname | rolcanlogin
---------+-------------
 rama    | t    <--------- CREATE USER auto-sets rolcanlogin = true 
(1 row)

postgres=#
postgres=# CREATE ROLE ravi;
CREATE ROLE
postgres=# SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'ravi';
 rolname | rolcanlogin
---------+-------------
 ravi    | f   <---------- CREATE ROLE leaves rolcanlogin = false
(1 row)

postgres=#

postgres-# \du
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 rama      |
 ravi      | Cannot login

postgres-#

2. Creating Users and Roles

-- Login accounts
postgres=# CREATE USER raj WITH PASSWORD 'raj_pwd123';
CREATE ROLE
postgres=# CREATE USER sugi WITH PASSWORD 'sugi_pwd123';
CREATE ROLE
postgres=# CREATE USER teja WITH PASSWORD 'teja_pwd123';
CREATE ROLE
postgres=# CREATE USER gebadm WITH PASSWORD 'gebadm_pwd123';   <-- we will set as schema owner
CREATE ROLE
postgres=# CREATE USER gebusr WITH PASSWORD 'gebusr_pwd123';   <-- app read-only user
CREATE ROLE
postgres=#

-- NOLOGIN group roles (permission bundles)

postgres=# CREATE ROLE geb_ro NOLOGIN;  <-- read-only privilege bundle
CREATE ROLE 
postgres=# CREATE ROLE geb_rw NOLOGIN;  <-- read-write privilege bundle
CREATE ROLE
postgres=#
postgres=# \du
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 geb_ro    | Cannot login
 geb_rw    | Cannot login
 gebadm    |
 gebusr    |
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 raj       |
 rama      |
 ravi      | Cannot login
 sugi      |
 teja      |

postgres=#

Both users and roles are stored in the same PostgreSQL system catalog (pg_authid / pg_roles). However, the pg_user view displays only users  & roles with the LOGIN privilege, not all roles.
postgres=# select rolname from pg_roles;
           rolname
-----------------------------
 pg_database_owner
 pg_read_all_data
 pg_write_all_data
 pg_monitor
 pg_read_all_settings
 pg_read_all_stats
 pg_stat_scan_tables
 pg_read_server_files
 pg_write_server_files
 pg_execute_server_program
 pg_signal_backend
 pg_checkpoint
 pg_maintain
 pg_use_reserved_connections
 pg_create_subscription
 postgres
 rama
 ravi
 raj
 sugi
 teja
 gebadm
 gebusr
 geb_ro
 geb_rw
(25 rows)

postgres=#
postgres=# select usename from pg_user;
 usename
----------
 postgres
 rama
 raj
 sugi
 teja
 gebadm
 gebusr
(7 rows)

postgres=#

Roles like geb_ro and geb_rw never log in directly. Instead, real users get added to them with GRANT role TO user, which is far easier to manage than granting privileges to each person individually.

3. Databases, Schemas, and Ownership

Once the roles exist, you can create a database and schema owned by the appropriate role, then open the “door” to that schema for everyone who needs it.

postgres=# CREATE DATABASE gebua;
CREATE DATABASE
postgres=#
postgres=# \c gebua postgres
You are now connected to database "gebua" as user "postgres".
gebua=#
gebua=# CREATE SCHEMA geb AUTHORIZATION gebadm;
CREATE SCHEMA
gebua=# GRANT USAGE ON SCHEMA geb TO geb_ro, geb_rw, raj, sugi, teja;
GRANT
gebua=#

gebua=# \l gebua
                                                 List of databases
 Name  |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | Locale | ICU Rules | Access privileges
-------+----------+----------+-----------------+-------------+-------------+--------+-----------+-------------------
 gebua | postgres | UTF8     | libc            | en_SG.UTF-8 | en_SG.UTF-8 |        |           |
(1 row)

gebua=#
gebua=# \dn
      List of schemas
  Name  |       Owner
--------+-------------------
 geb    | gebadm
 public | pg_database_owner
(2 rows)

gebua=#
gebua=# \dn+
                                       List of schemas
  Name  |       Owner       |           Access privileges            |      Description
--------+-------------------+----------------------------------------+------------------------
 geb    | gebadm            | gebadm=UC/gebadm                      +|
        |                   | geb_ro=U/gebadm                       +|
        |                   | geb_rw=U/gebadm                       +|
        |                   | raj=U/gebadm                          +|
        |                   | sugi=U/gebadm                         +|
        |                   | teja=U/gebadm                          |
 public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
        |                   | =U/pg_database_owner                   |
(2 rows)

gebua=#

U = USAGE privilege   <---- USAGE means the user can see that tables and other objects exist in the schema, but cannot view or modify their data unless additional permissions are granted.
C = CREATE privilege
postgres=# \c gebua gebadm
You are now connected to database "gebua" as user "gebadm".
gebua=>

gebua=> CREATE TABLE geb.dept ( dept_id SERIAL PRIMARY KEY, dept_name VARCHAR(100) NOT NULL);
CREATE TABLE
gebua=> CREATE TABLE geb.emp (emp_id SERIAL PRIMARY KEY, emp_name VARCHAR(100) NOT NULL,dept_id INT REFERENCES geb.dept(dept_id),salary NUMERIC(10,2));
CREATE TABLE
gebua=>
gebua=> \dt geb.*
       List of relations
 Schema | Name | Type  | Owner
--------+------+-------+--------
 geb    | dept | table | gebadm
 geb    | emp  | table | gebadm
(2 rows)

gebua=>
gebua=> INSERT INTO geb.dept (dept_name) VALUES ('Engineering'), ('Sales'), ('HR');
INSERT 0 3
gebua=> INSERT INTO geb.emp (emp_name, dept_id, salary) VALUES ('Arjun',  1, 75000),('Meena',  2, 65000),('Kabir',  3, 60000);
INSERT 0 3
gebua=>
gebua=> select * from emp;
ERROR:  relation "emp" does not exist
LINE 1: select * from emp;
                      ^
gebua=> select * from geb.emp;
 emp_id | emp_name | dept_id |  salary
--------+----------+---------+----------
      1 | Arjun    |       1 | 75000.00
      2 | Meena    |       2 | 65000.00
      3 | Kabir    |       3 | 60000.00
(3 rows)

gebua=> select * from geb.dept;
 dept_id |  dept_name
---------+-------------
       1 | Engineering
       2 | Sales
       3 | HR
(3 rows)

gebua=>

4. Granting Privileges on Existing Objects

GRANT SELECT ON ALL TABLES IN SCHEMA geb TO geb_ro; <------Will automatically give SELECT access to future tables. It does not.
This command grants permissions only to the tables that exist at the time you run it. Any tables created later will not receive these permissions automatically. To ensure that new tables also get the required permissions, you must use ALTER DEFAULT PRIVILEGES. 

-- Grant privilleges to RO (Read-only) role.

[postgres@pgdb02 ~]$ psql
psql (17.10)
Type "help" for help.

postgres=# GRANT SELECT ON ALL TABLES IN SCHEMA geb TO geb_ro;
ERROR:  schema "geb" does not exist
postgres=#
postgres=# \c gebua
You are now connected to database "gebua" as user "postgres".
gebua=#
gebua=# GRANT SELECT ON ALL TABLES IN SCHEMA geb TO geb_ro;
GRANT
gebua=# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA geb TO geb_ro;
GRANT
gebua=#

-- Grant privilleges to RW (Read-Write) role.
gebua=# GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA geb TO geb_rw;
GRANT
gebua=# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA geb TO geb_rw;
GRANT
gebua=#
-- Assign RO role to user
gebua=# GRANT geb_ro TO gebusr;
GRANT ROLE
gebua=#

gebua=# \dp geb.*
                                      Access privileges
 Schema |       Name       |   Type   |   Access privileges    | Column privileges | Policies
--------+------------------+----------+------------------------+-------------------+----------
 geb    | dept             | table    | gebadm=arwdDxtm/gebadm+|                   |
        |                  |          | geb_ro=r/gebadm       +|                   |
        |                  |          | geb_rw=arwd/gebadm     |                   |
 geb    | dept_dept_id_seq | sequence | gebadm=rwU/gebadm     +|                   |
        |                  |          | geb_ro=rU/gebadm      +|                   |
        |                  |          | geb_rw=rU/gebadm       |                   |
 geb    | emp              | table    | gebadm=arwdDxtm/gebadm+|                   |
        |                  |          | geb_ro=r/gebadm       +|                   |
        |                  |          | geb_rw=arwd/gebadm     |                   |
 geb    | emp_emp_id_seq   | sequence | gebadm=rwU/gebadm     +|                   |
        |                  |          | geb_ro=rU/gebadm      +|                   |
        |                  |          | geb_rw=rU/gebadm       |                   |
(4 rows)

gebua=#

| Code | Meaning    |
| ---- | ---------- |
| r    | SELECT     |
| a    | INSERT     |
| w    | UPDATE     |
| d    | DELETE     |
| D    | TRUNCATE   |
| x    | REFERENCES |
| t    | TRIGGER    |
| m    | MAINTAIN   |  <------

What is m (MAINTAIN)?

The MAINTAIN privilege allows operations such as:
VACUUM
ANALYZE
REINDEX
CLUSTER
REFRESH MATERIALIZED VIEW
LOCK TABLE

-- Verify 
[postgres@pgdb02 ~]$ psql -d gebua -U gebusr
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "gebusr".
gebua=>
gebua=> \dt geb.*
       List of relations
 Schema | Name | Type  | Owner
--------+------+-------+--------
 geb    | dept | table | gebadm
 geb    | emp  | table | gebadm
(2 rows)

gebua=> select * from geb.emp;
 emp_id | emp_name | dept_id |  salary
--------+----------+---------+----------
      1 | Arjun    |       1 | 75000.00
      2 | Meena    |       2 | 65000.00
      3 | Kabir    |       3 | 60000.00
(3 rows)

gebua=> select * from geb.dept;
 dept_id |  dept_name
---------+-------------
       1 | Engineering
       2 | Sales
       3 | HR
(3 rows)

gebua=>

5. Default Privileges for Future Objects

ALTER DEFAULT PRIVILEGES allows you to define permissions that will be automatically granted to future objects created by a specific role.

In this example, whenever gebadm creates a new table in the geb schema:

  • geb_ro automatically receives SELECT permission (read-only access).
  • geb_rw automatically receives SELECT, INSERT, UPDATE, and DELETE permissions (read-write access).
-- For RO Role

gebua=# \c
You are now connected to database "gebua" as user "postgres".

gebua=# ALTER DEFAULT PRIVILEGES FOR ROLE gebadm IN SCHEMA geb GRANT SELECT ON TABLES TO geb_ro;
ALTER DEFAULT PRIVILEGES
gebua=# ALTER DEFAULT PRIVILEGES FOR ROLE gebadm IN SCHEMA geb GRANT USAGE, SELECT ON SEQUENCES TO geb_ro;
ALTER DEFAULT PRIVILEGES
gebua=#

-- For RW Role

gebua=# ALTER DEFAULT PRIVILEGES FOR ROLE gebadm IN SCHEMA geb GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO geb_rw;
ALTER DEFAULT PRIVILEGES
gebua=# ALTER DEFAULT PRIVILEGES FOR ROLE gebadm IN SCHEMA geb GRANT USAGE, SELECT ON SEQUENCES TO geb_rw;
ALTER DEFAULT PRIVILEGES
gebua=#

-- Live proof: a brand-new table already has the right grants

[postgres@pgdb02 ~]$ psql -d gebua -U gebadm
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "gebadm".
gebua=>
gebua=> CREATE TABLE geb.payroll (payroll_id SERIAL PRIMARY KEY, amount NUMERIC);
CREATE TABLE

gebua=> INSERT INTO geb.payroll (amount) VALUES (50000.00);
INSERT 0 1
gebua=>
gebua=> \dp geb.payroll
                                Access privileges
 Schema |  Name   | Type  |   Access privileges    | Column privileges | Policies
--------+---------+-------+------------------------+-------------------+----------
 geb    | payroll | table | gebadm=arwdDxtm/gebadm+|                   |
        |         |       | geb_ro=r/gebadm       +|                   |
        |         |       | geb_rw=arwd/gebadm     |                   |
(1 row)

gebua=>

ALTER DEFAULT PRIVILEGES allows you to define permissions that will be automatically granted to future objects.

[postgres@pgdb02 ~]$ psql -d gebua -U gebusr
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "gebusr".
gebua=>
gebua=> select * from geb.payroll;
 payroll_id |  amount
------------+----------
          1 | 50000.00    <------
(1 row)

gebua=>

6. Per-User, Per-Table Grants and Revokes

Sometimes you want a single user to see exactly one table, with no role membership involved:

postgres=# \c gebua
You are now connected to database "gebua" as user "postgres".

gebua=# GRANT SELECT ON geb.emp TO raj;
GRANT
gebua=# GRANT SELECT ON geb.dept TO sugi;
GRANT
gebua=# GRANT SELECT ON geb.payroll TO teja;
GRANT
gebua=#

-- Schema level 
gebua=# \dn+ geb
                 List of schemas
 Name | Owner  | Access privileges | Description
------+--------+-------------------+-------------
 geb  | gebadm | gebadm=UC/gebadm +|
      |        | geb_ro=U/gebadm  +|
      |        | geb_rw=U/gebadm  +|
      |        | raj=U/gebadm     +|
      |        | sugi=U/gebadm    +|
      |        | teja=U/gebadm     |
(1 row)

gebua=#

-- Table Level 
gebua-# \dp geb.*
                                         Access privileges
 Schema |          Name          |   Type   |   Access privileges    | Column privileges | Policies
--------+------------------------+----------+------------------------+-------------------+----------
 geb    | dept                   | table    | gebadm=arwdDxtm/gebadm+|                   |
        |                        |          | geb_ro=r/gebadm       +|                   |
        |                        |          | geb_rw=arwd/gebadm    +|                   |
        |                        |          | sugi=r/gebadm          |                   |
 geb    | dept_dept_id_seq       | sequence | gebadm=rwU/gebadm     +|                   |
        |                        |          | geb_ro=rU/gebadm      +|                   |
        |                        |          | geb_rw=rU/gebadm       |                   |
 geb    | emp                    | table    | gebadm=arwdDxtm/gebadm+|                   |
        |                        |          | geb_ro=r/gebadm       +|                   |
        |                        |          | geb_rw=arwd/gebadm    +|                   |
        |                        |          | raj=r/gebadm           |                   |
 geb    | emp_emp_id_seq         | sequence | gebadm=rwU/gebadm     +|                   |
        |                        |          | geb_ro=rU/gebadm      +|                   |
        |                        |          | geb_rw=rU/gebadm       |                   |
 geb    | payroll                | table    | gebadm=arwdDxtm/gebadm+|                   |
        |                        |          | geb_ro=r/gebadm       +|                   |
        |                        |          | geb_rw=arwd/gebadm    +|                   |
        |                        |          | teja=r/gebadm          |                   |
 geb    | payroll_payroll_id_seq | sequence | gebadm=rwU/gebadm     +|                   |
        |                        |          | geb_ro=rU/gebadm      +|                   |
        |                        |          | geb_rw=rU/gebadm       |                   |
(6 rows)

gebua-#

=== LIVE TEST: raj can read emp, but NOT dept/hr ===

gebua-# \c gebua raj
You are now connected to database "gebua" as user "raj".
gebua->
gebua=>
gebua=> SELECT * FROM geb.emp;
 emp_id | emp_name | dept_id |  salary
--------+----------+---------+----------
      1 | Arjun    |       1 | 75000.00
      2 | Meena    |       2 | 65000.00
      3 | Kabir    |       3 | 60000.00
(3 rows)

gebua=>
gebua=> SELECT * FROM geb.dept;
ERROR: permission denied for table dept
gebua=>
gebua=> SELECT * FROM geb.hr;
ERROR: relation "geb.hr" does not exist
LINE 1: SELECT * FROM geb.hr;
^
gebua=>

=== LIVE TEST: sugi can read dept, but NOT emp/hr ===

gebua=> \c gebua sugi
You are now connected to database "gebua" as user "sugi".
gebua=>
gebua=> SELECT * FROM geb.dept;
 dept_id |  dept_name
---------+-------------
       1 | Engineering
       2 | Sales
       3 | HR
(3 rows)

gebua=> SELECT * FROM geb.emp;
ERROR:  permission denied for table emp
gebua=>
gebua=> SELECT * FROM geb.hr;
ERROR:  relation "geb.hr" does not exist
LINE 1: SELECT * FROM geb.hr;
                      ^
gebua=>


=== LIVE TEST: teja can read hr, but NOT emp/dept ===

gebua=> \c gebua teja
You are now connected to database "gebua" as user "teja".
gebua=>
gebua=> SELECT * FROM geb.payroll;
 payroll_id |  amount
------------+----------
          1 | 50000.00
(1 row)

gebua=> SELECT * FROM geb.emp;
ERROR:  permission denied for table emp
gebua=>
gebua=> SELECT * FROM geb.dept;
ERROR:  permission denied for table dept
gebua=>

Each of these users has no membership in geb_ro or geb_rw — they get exactly the one table explicitly granted, nothing more. Taking access away later uses REVOKE.

--- Revoke
[postgres@pgdb02 ~]$ psql
psql (17.10)
Type "help" for help.

postgres=# \c gebua
You are now connected to database "gebua" as user "postgres".
gebua=# REVOKE SELECT ON geb.emp  FROM raj;
REVOKE
gebua=# REVOKE SELECT ON geb.dept FROM sugi;
REVOKE
gebua=# REVOKE SELECT ON geb.payroll   FROM teja;
REVOKE
gebua=#
gebua=# \dp geb.emp;
                               Access privileges
 Schema | Name | Type  |   Access privileges    | Column privileges | Policies
--------+------+-------+------------------------+-------------------+----------
 geb    | emp  | table | gebadm=arwdDxtm/gebadm+|                   |
        |      |       | geb_ro=r/gebadm       +|                   |
        |      |       | geb_rw=arwd/gebadm     |                   |
(1 row)

gebua=# \dp geb.dept;
                               Access privileges
 Schema | Name | Type  |   Access privileges    | Column privileges | Policies
--------+------+-------+------------------------+-------------------+----------
 geb    | dept | table | gebadm=arwdDxtm/gebadm+|                   |
        |      |       | geb_ro=r/gebadm       +|                   |
        |      |       | geb_rw=arwd/gebadm     |                   |
(1 row)

gebua=# \dp geb.payroll;
                                Access privileges
 Schema |  Name   | Type  |   Access privileges    | Column privileges | Policies
--------+---------+-------+------------------------+-------------------+----------
 geb    | payroll | table | gebadm=arwdDxtm/gebadm+|                   |
        |         |       | geb_ro=r/gebadm       +|                   |
        |         |       | geb_rw=arwd/gebadm     |                   |
(1 row)

gebua=#
gebua=# \c gebua raj
You are now connected to database "gebua" as user "raj".
gebua=> select * from geb.emp;
ERROR:  permission denied for table emp
gebua=>
gebua=> \c gebua sugi;
You are now connected to database "gebua" as user "sugi".
gebua=>
gebua=> select * from geb.dept;
ERROR:  permission denied for table dept
gebua=>
gebua=> \c gebua teja
You are now connected to database "gebua" as user "teja".
gebua=>
gebua=> select * from geb.payroll;
ERROR:  permission denied for table payroll
gebua=>

-- Re GRANT 

gebua=# \c
You are now connected to database "gebua" as user "postgres".
gebua=# GRANT SELECT ON geb.emp TO raj;
GRANT
gebua=# GRANT SELECT ON geb.dept TO sugi;
GRANT
gebua=# GRANT SELECT ON geb.payroll TO teja;
GRANT
gebua=#

You can confirm what’s actually in effect with \dn+ geb (schema-level owner and ACL) and \dp geb.* (table-level ACL for every object).

7. The PUBLIC Pseudo-Role

Every role you create — including ones that don’t exist yet — is automatically and silently a member of PUBLIC. You never grant membership to it; it’s implicit and universal. Anything granted “TO PUBLIC” is inherited by every current and future user.

Fresh databases ship with a few defaults granted to PUBLIC:

  • CONNECT and TEMP on every new database
  • EXECUTE on every newly created function or procedure
  • USAGE + CREATE on the public schema (PG < 15 only — PG15+ removed CREATE from PUBLIC by default)

gebua=# \c gebua raj
You are now connected to database “gebua” as user “raj”.
gebua=> create table test (id init);
ERROR: permission denied for schema public
LINE 1: create table test (id init);
^
gebua=>

gebua=> \c gebua postgres
You are now connected to database "gebua" as user "postgres".
gebua=# REVOKE ALL ON DATABASE gebua FROM PUBLIC;
REVOKE
gebua=# REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE
gebua=#

gebua=# \c gebua raj
connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: FATAL: permission denied for database "gebua"
DETAIL: User does not have CONNECT privilege.
Previous connection kept
gebua=#

gebua=# GRANT CONNECT ON DATABASE gebua TO raj, sugi, teja, gebadm, gebusr;
GRANT
gebua=#
gebua=# \c gebua raj
You are now connected to database "gebua" as user "raj".
gebua=>

8. GRANT ALL

GRANT ALL is broader than it sounds. On a table, it grants every applicable privilege — SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER — not just the common four.

The extra three matter:

  • TRUNCATE — can wipe the entire table instantly
  • REFERENCES — can create a foreign key pointing at this table
  • TRIGGER — can create triggers on this table
gebua=# CREATE USER anu WITH PASSWORD 'anu';
CREATE ROLE
gebua=# GRANT CONNECT ON DATABASE GEBUA TO ANU;
GRANT
gebua=# GRANT ALL PRIVILEGES ON GEB.EMP TO ANU;
GRANT
gebua=#
gebua=# \dp geb.emp;
                               Access privileges
 Schema | Name | Type  |   Access privileges    | Column privileges | Policies
--------+------+-------+------------------------+-------------------+----------
 geb    | emp  | table | gebadm=arwdDxtm/gebadm+|                   |
        |      |       | geb_ro=r/gebadm       +|                   |
        |      |       | geb_rw=arwd/gebadm    +|                   |
        |      |       | raj=r/gebadm          +|                   |
        |      |       | anu=arwdDxtm/gebadm    |                   |
(1 row)

gebua=#

9. Inspecting Roles: pg_roles, pg_user, pg_shadow, pg_group

PostgreSQL exposes several system catalogs for auditing roles, each with a different scope:

  • pg_rolesall roles, both login users and NOLOGIN roles.
  • pg_user — a view filtered from pg_roles showing only roles where rolcanlogin = true. Roles like geb_ro/geb_rw never appear here.
  • pg_shadow — password hashes (SCRAM/MD5, never plaintext), visible only to superusers or for your own row.
  • pg_group — a legacy compatibility view predating the unified role system; it only lists NOLOGIN roles and derives its data live from pg_auth_members.

For a complete, modern view of role memberships — including login roles that are members of other roles — query pg_auth_members directly:

gebua=# select * from pg_roles;
           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolconnlimit | rolpassword | rolvaliduntil | rolbypassrls | rolconfig |  oid
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+-------------+---------------+--------------+-----------+-------
 pg_database_owner           | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  6171
 pg_read_all_data            | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  6181
 pg_write_all_data           | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  6182
 pg_monitor                  | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  3373
 pg_read_all_settings        | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  3374
 pg_read_all_stats           | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  3375
 pg_stat_scan_tables         | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  3377
 pg_read_server_files        | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4569
 pg_write_server_files       | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4570
 pg_execute_server_program   | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4571
 pg_signal_backend           | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4200
 pg_checkpoint               | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4544
 pg_maintain                 | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  6337
 pg_use_reserved_connections | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  4550
 pg_create_subscription      | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           |  6304
 postgres                    | t        | t          | t             | t           | t           | t              |           -1 | ********    |               | t            |           |    10
 rama                        | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41527
 ravi                        | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           | 41528
 raj                         | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41531
 sugi                        | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41532
 teja                        | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41533
 gebadm                      | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41534
 gebusr                      | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41535
 geb_ro                      | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           | 41536
 geb_rw                      | f        | t          | f             | f           | f           | f              |           -1 | ********    |               | f            |           | 41537
 anu                         | f        | t          | f             | f           | t           | f              |           -1 | ********    |               | f            |           | 41572
(26 rows)

gebua=#
gebua=# select * from pg_user;
 usename  | usesysid | usecreatedb | usesuper | userepl | usebypassrls |  passwd  | valuntil | useconfig
----------+----------+-------------+----------+---------+--------------+----------+----------+-----------
 postgres |       10 | t           | t        | t       | t            | ******** |          |
 rama     |    41527 | f           | f        | f       | f            | ******** |          |
 raj      |    41531 | f           | f        | f       | f            | ******** |          |
 sugi     |    41532 | f           | f        | f       | f            | ******** |          |
 teja     |    41533 | f           | f        | f       | f            | ******** |          |
 gebadm   |    41534 | f           | f        | f       | f            | ******** |          |
 gebusr   |    41535 | f           | f        | f       | f            | ******** |          |
 anu      |    41572 | f           | f        | f       | f            | ******** |          |
(8 rows)

gebua=#

gebua=# select * from pg_shadow;
 usename  | usesysid | usecreatedb | usesuper | userepl | usebypassrls |                                                                passwd                                                                 | valuntil | useconfig
----------+----------+-------------+----------+---------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+----------+-----------
 postgres |       10 | t           | t        | t       | t            | SCRAM-SHA-256$4096:IXXmstZ3yAPBDYk9SaYwoQ==$XERgxvz+WfnEzw0FO5wdHWgkZDNXGTJwh+9ku0fAHUE=:i9zQsVpTsUxxBpe3Rs08ValuokiqjnrNHjtk9eKDT8A= |          |
 rama     |    41527 | f           | f        | f       | f            |                                                                                                                                       |          |
 raj      |    41531 | f           | f        | f       | f            | SCRAM-SHA-256$4096:83y0vE12G/RH9B6pxOx9FA==$lYuVyvhTsvaGvjUXqWBir2F34gZLq7C7fW3zg8L6jZA=:R1PzeM8NTQjT0kHUASeSgnSXhFWiX5dcTwd9fNiy7Lg= |          |
 sugi     |    41532 | f           | f        | f       | f            | SCRAM-SHA-256$4096:obX6SJ88dSoqJDh9YgKQ6Q==$sH5xVe6Li+95vTluTyell1y/IUIVbip9nUINWkQirQk=:SpJjKrDCnv3LNvwZBzoh6nY23D9fCCtKNYoMl9AKpKg= |          |
 teja     |    41533 | f           | f        | f       | f            | SCRAM-SHA-256$4096:VD9NCq0s1UbQv/ucdEtOMg==$FaWKK/ovAC45oxgOfGKmTqNQqvavQaPdKRUGEgAi7Rc=:EQwYGJhvD89OGrkqVrOWr27I0OftzNgdO7kiZgGnaPM= |          |
 gebadm   |    41534 | f           | f        | f       | f            | SCRAM-SHA-256$4096:Xo6mPeiuPXBaIqN6tFbH7Q==$WG4m8yARZ9ZV40H7HgZVkG5TrvS83HgSFj59Uc1GlWo=:p/1zT72iOnajg7dwwo9xjmD2wRIuOugtr/0lJWXA6t0= |          |
 gebusr   |    41535 | f           | f        | f       | f            | SCRAM-SHA-256$4096:mvnPDdeNDh+olgPT6d05Yg==$19cDwk3Yy/hQRC8SmGcgHkwoHT86YFkYHAsdRTatzyQ=:CZupkFOFhqRIyK4LUhg7OcQZFpzpKB8VKdzNZphrkrQ= |          |
 anu      |    41572 | f           | f        | f       | f            | SCRAM-SHA-256$4096:mXGdfJgtLi3531WT+4S93g==$wlYQ1SLzh0aNcESBEQ7jVgLkQqr4F6d674xQsNpSo1U=:WRudEkJcQvuHqcOpTgqWf6wQlizqPcS9MJ6lyT2AsyI= |          |
(8 rows)

gebua=# select * from pg_authid;

gebua=# SELECT * FROM pg_ident_file_mappings;
 map_number | file_name | line_number | map_name | sys_name | pg_username | error
------------+-----------+-------------+----------+----------+-------------+-------
(0 rows)

gebua=#

gebua=# select * from pg_group;
           groname           | grosysid | grolist
-----------------------------+----------+---------
 pg_database_owner           |     6171 | {}
 pg_read_all_data            |     6181 | {}
 pg_write_all_data           |     6182 | {}
 pg_monitor                  |     3373 | {}
 pg_read_all_settings        |     3374 | {3373}
 pg_read_all_stats           |     3375 | {3373}
 pg_stat_scan_tables         |     3377 | {3373}
 pg_read_server_files        |     4569 | {}
 pg_write_server_files       |     4570 | {}
 pg_execute_server_program   |     4571 | {}
 pg_signal_backend           |     4200 | {}
 pg_checkpoint               |     4544 | {}
 pg_maintain                 |     6337 | {}
 pg_use_reserved_connections |     4550 | {}
 pg_create_subscription      |     6304 | {}
 ravi                        |    41528 | {}
 geb_ro                      |    41536 | {41535}
 geb_rw                      |    41537 | {}
(18 rows)

gebua=#

-- Granted Roles
gebua=# SELECT r.rolname AS member, g.rolname AS granted_role FROM pg_auth_members m JOIN pg_roles r ON m.member = r.oid JOIN pg_roles g ON m.roleid = g.oid ORDER BY g.rolname, r.rolname;
   member   |     granted_role
------------+----------------------
 gebusr     | geb_ro
 pg_monitor | pg_read_all_settings
 pg_monitor | pg_read_all_stats
 pg_monitor | pg_stat_scan_tables
(4 rows)

gebua=#

One more distinction worth clearing up: pg_ident.conf is not a password catalog at all. It’s a config file mapping OS-level usernames to Postgres role names, used only for ident/peer/GSSAPI/cert authentication. It contains zero passwords — pg_shadow answers “is this the right password for this role?”, while pg_ident answers “which Postgres role does this externally-authenticated identity map to?”

10. Password Management

PostgreSQL has no native equivalent to “force password change on first login.” The closest built-in tool is VALID UNTIL, which can only block login after a timestamp — it can’t force a change flow. Common workarounds:

  • Set VALID UNTIL to a timestamp already in the past, so the account is “pre-expired” and a DBA must reset it before the user’s first login.
  • Track a must_change_password flag in your own application table and enforce it in app logic.
  • Use community extensions like passwordcheck or credcheck for complexity/expiry policy enforcement.
gebua=# show password_encryption;
 password_encryption
---------------------
 scram-sha-256
(1 row)

gebua=#

[postgres@pgdb02 data]$ PGPASSWORD='raj_pwd123' psql -h 192.168.2.23 -d gebua -U raj
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "raj".
gebua=>

-- Simulate a "pre-expired" account

gebua=# ALTER USER raj VALID UNTIL '2020-01-01';
ALTER ROLE
gebua=# SELECT usename, valuntil FROM pg_user WHERE usename = 'raj';
 usename |        valuntil
---------+------------------------
 raj     | 2020-01-01 00:00:00+08   <------------
(1 row)

gebua=# \du raj
                      List of roles
 Role name |                 Attributes
-----------+---------------------------------------------
 raj       | Password valid until 2020-01-01 00:00:00+08

gebua=#

[postgres@pgdb02 data]$ cat pg_hba.conf | grep -i raj
host       gebua         raj            0.0.0.0/0              md5
[postgres@pgdb02 data]$

[postgres@pgdb02 data]$ pg_ctl reload -D /pgData/pgsql17/data/
server signaled
[postgres@pgdb02 data]$

[postgres@pgdb02 data]$ PGPASSWORD='raj_pwd123' psql -h 192.168.2.23 -d gebua -U raj
psql: error: connection to server at "192.168.2.23", port 5432 failed: FATAL: password authentication failed for user "raj"
[postgres@pgdb02 data]$ 

-- Extend validity again later

ALTER USER raj VALID UNTIL '2027-08-06'; ---- OR ------ ALTER USER raj VALID UNTIL 'infinity';

postgres=# ALTER USER raj VALID UNTIL 'infinity';
ALTER ROLE
postgres=# exit
[postgres@pgdb02 data]$ PGPASSWORD='raj_pwd123' psql -h 192.168.2.23 -d gebua -U raj
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "raj".
gebua=>

postgres=# \du raj
               List of roles
 Role name |          Attributes
-----------+-------------------------------
 raj       | Password valid until infinity

postgres=#

--- Restore the OLD password using the saved hash

[postgres@pgdb02 data]$ cat pg_hba.conf | grep -i anu
host         gebua         raj,anu              0.0.0.0/0         md5
[postgres@pgdb02 data]$
[postgres@pgdb02 data]$ pg_ctl reload -D /pgData/pgsql17/data/
server signaled
[postgres@pgdb02 data]$ 

[postgres@pgdb02 data]$ PGPASSWORD='anu' psql -h 192.168.2.23 -d gebua -U anu
psql (17.10)
Type "help" for help.

gebua=> 

1. Show current hash

select rolpassword from pg_authid where rolname='anu';

--- OR ---
postgres=# SELECT usename, passwd FROM pg_shadow WHERE usename = 'anu';
 usename |                                                                passwd
---------+---------------------------------------------------------------------------------------------------------------------------------------
 anu     | SCRAM-SHA-256$4096:mXGdfJgtLi3531WT+4S93g==$wlYQ1SLzh0aNcESBEQ7jVgLkQqr4F6d674xQsNpSo1U=:WRudEkJcQvuHqcOpTgqWf6wQlizqPcS9MJ6lyT2AsyI=
(1 row)

postgres=#

2. Change password (simulate "forgot to save old one")

postgres=# ALTER USER anu WITH PASSWORD 'temporary_new_pass';
ALTER ROLE
postgres=#

3. "Restore" old password using saved hash string

[postgres@pgdb02 ~]$ PGPASSWORD='anu' psql -h 192.168.2.23 -d gebua -U anu
psql: error: connection to server at "192.168.2.23", port 5432 failed: FATAL: password authentication failed for user "anu"
[postgres@pgdb02 ~]$

postgres=# \c
You are now connected to database "postgres" as user "postgres".
postgres=#
postgres=# ALTER USER anu WITH PASSWORD 'SCRAM-SHA-256$4096:mXGdfJgtLi3531WT+4S93g==$wlYQ1SLzh0aNcESBEQ7jVgLkQqr4F6d674xQsNpSo1U=:WRudEkJcQvuHqcOpTgqWf6wQlizqPcS9MJ6lyT2AsyI=';
ALTER ROLE
postgres=#

4. Confirm hash matches original

postgres=# SELECT usename, passwd FROM pg_shadow WHERE usename = 'anu';
 usename |                                                                passwd
---------+---------------------------------------------------------------------------------------------------------------------------------------
 anu     | SCRAM-SHA-256$4096:mXGdfJgtLi3531WT+4S93g==$wlYQ1SLzh0aNcESBEQ7jVgLkQqr4F6d674xQsNpSo1U=:WRudEkJcQvuHqcOpTgqWf6wQlizqPcS9MJ6lyT2AsyI=
(1 row)

postgres=#

5. Login test — original plaintext password works again
[postgres@pgdb02 ~]$ PGPASSWORD='anu' psql -h 192.168.2.23 -d gebua -U anu
psql (17.10)
Type "help" for help.

gebua=> \c
You are now connected to database "gebua" as user "anu".
gebua=>

Note: Resetting the password does not extend the account's validity. The password will still expire unless you update it using one of the following commands
 ALTER USER raj VALID UNTIL '2027-08-06'; ---- OR ------ ALTER USER raj VALID UNTIL 'infinity';

Note that VALID UNTIL only controls the time window — it never touches the password hash itself. Extending expiry, without also running ALTER USER ... PASSWORD, leaves the original password value completely unchanged. Postgres also deliberately shows the same generic authentication error for an expired account as for a wrong password, so failed logins don’t leak the cause.

11. session_user vs. current_user

These two functions answer different questions:

  • session_user — the role that actually authenticated the connection. Fixed for the whole session.
  • current_user — the role whose privileges are being checked right now for the current statement. Changes with SET ROLE, or inside a SECURITY DEFINER function.
---- Difference between session_user and current_user
postgres=# SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 postgres     | postgres
(1 row)

postgres=# \c postgres sam
You are now connected to database "postgres" as user "sam".
postgres=>
postgres=> SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 sam          | sam
(1 row)

postgres=>
postgres=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=#
postgres=# SET ROLE sam;
SET
postgres=>
postgres=> SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 sam          | postgres
(1 row)

postgres=>
postgres=> CREATE TABLE test(a int);
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE test(a int);
                     ^
postgres=>
postgres=> SET ROLE NONE;
SET
postgres=#
postgres=# SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 postgres     | postgres
(1 row)

postgres=#

--- SET SESSION AUTHORIZATION
postgres=# SET SESSION AUTHORIZATION sam;
SET
postgres=>
postgres=> SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 sam          | sam
(1 row)

postgres=> CREATE TABLE test(a int);
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE test(a int);
                     ^
postgres=> SET ROLE NONE;
SET
postgres=>
postgres=> SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 sam          | sam
(1 row)

postgres=> RESET ROLE;
RESET
postgres=> SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 sam          | sam
(1 row)

postgres=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=#
postgres=# SELECT current_user, session_user;
 current_user | session_user
--------------+--------------
 postgres     | postgres
(1 row)

postgres=#

12. search_path

gebua=# 
postgres=# \c gebua
You are now connected to database "gebua" as user "postgres".
gebua=#
gebua=# show search_path;
   search_path
-----------------
 "$user", public
(1 row)

gebua=# \dt geb.*
         List of relations
 Schema |  Name   | Type  | Owner
--------+---------+-------+--------
 geb    | dept    | table | gebadm
 geb    | emp     | table | gebadm
 geb    | payroll | table | gebadm
(3 rows)

gebua=# select * from geb.emp;
 emp_id | emp_name | dept_id |  salary
--------+----------+---------+----------
      1 | Arjun    |       1 | 75000.00
      2 | Meena    |       2 | 65000.00
      3 | Kabir    |       3 | 60000.00
(3 rows)

gebua=#
gebua=# select * from emp;
ERROR:  relation "emp" does not exist
LINE 1: select * from emp;
                      ^
gebua=#
gebua=# SET search_path to geb;
SET
gebua=# show search_path;
 search_path
-------------
 geb   <------------
(1 row)

gebua=# select * from emp;
 emp_id | emp_name | dept_id |  salary
--------+----------+---------+----------
      1 | Arjun    |       1 | 75000.00
      2 | Meena    |       2 | 65000.00
      3 | Kabir    |       3 | 60000.00
(3 rows)

gebua=#

Caution: Your use of any information or materials on this website is entirely at your own risk. It is provided for educational purposes only. It has been tested internally, however, we do not guarantee that it will work for you. Ensure that you run it in your test environment before using.

Thank you,
Rajasekhar Amudala
Email: br8dba@gmail.com
Linkedin: https://www.linkedin.com/in/rajasekhar-amudala/