From f7b32ce723a85f8e968acd820c062bffc69ec511 Mon Sep 17 00:00:00 2001 From: Gabriele Bartolini Date: Fri, 21 Aug 2026 12:39:37 +1000 Subject: [PATCH] Track role modification timestamp in pg_authid.rollastupdated Add a rollastupdated timestamptz column to pg_authid, recording when the most recent CREATE ROLE or ALTER ROLE command for a role was executed. Tools that manage roles declaratively (Kubernetes operators, Ansible, Terraform) currently have no cheap way to ask whether a role changed since they last looked, and must either re-issue ALTER ROLE unconditionally on every reconcile pass or reimplement attribute-by- attribute diffing client side. The timestamp is set by CreateRole(), AlterRole() and RenameRole(), and by AlterRoleSet() for ALTER ROLE ... SET/RESET, since role-level configuration parameters are role state too even though they are stored in pg_db_role_setting. The value advances whenever such a command completes successfully, even if it did not actually change anything: for passwords this is not merely a simplification but a necessity, as pg_be_scram_build_secret() generates a fresh random salt per call, so re-issuing an identical password cannot be distinguished from a change. The column is null for roles created during initdb that have never been altered. It is not preserved by pg_dumpall or pg_upgrade, which replay CREATE ROLE and ALTER ROLE against the target cluster, so restored roles carry the time of the restore. Exposed as rollastupdated in pg_roles and as lastupdated in pg_shadow and pg_user. Assisted-by: Claude Signed-off-by: Gabriele Bartolini --- doc/src/sgml/catalogs.sgml | 17 ++++ doc/src/sgml/system-views.sgml | 36 +++++++++ src/backend/catalog/system_views.sql | 9 ++- src/backend/commands/user.c | 67 +++++++++++++++ src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_authid.dat | 51 ++++++++---- src/include/catalog/pg_authid.h | 2 + .../regress/expected/role_lastupdated.out | 81 +++++++++++++++++++ src/test/regress/expected/rules.out | 9 ++- src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/role_lastupdated.sql | 53 ++++++++++++ 11 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 src/test/regress/expected/role_lastupdated.out create mode 100644 src/test/regress/sql/role_lastupdated.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 6066c4784f4..208432c88fb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1624,6 +1624,23 @@ null if no expiration + + + + rollastupdated timestamptz + + + Time at which the most recent or + command for this role was executed + (including a rename or a role-level configuration parameter + change). The timestamp advances whenever such a command completes + successfully, even if it did not change any value. Null for roles + created during initdb that have never + been altered. Not preserved by pg_dumpall + or pg_upgrade, which recreate roles in + the target cluster. + + diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 5ea19d68622..550e470a2d3 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -3333,6 +3333,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx ID of role + + + + rollastupdated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value. Null for roles created during + initdb that have never been altered + + @@ -4165,6 +4177,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Session defaults for run-time configuration variables + + + + lastupdated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value. Null for roles created during + initdb that have never been altered + + @@ -5529,6 +5553,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Session defaults for run-time configuration variables + + + + lastupdated timestamptz + + + Time at which the most recent or + command for this role was executed, + even if it did not change any value; null for roles created during + initdb that have never been altered + + diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 8612d99a890..802fa8ca056 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -33,7 +33,8 @@ CREATE VIEW pg_roles AS rolvaliduntil, rolbypassrls, setconfig as rolconfig, - pg_authid.oid + pg_authid.oid, + rollastupdated FROM pg_authid LEFT JOIN pg_db_role_setting s ON (pg_authid.oid = setrole AND setdatabase = 0); @@ -47,7 +48,8 @@ CREATE VIEW pg_shadow AS rolbypassrls AS usebypassrls, rolpassword AS passwd, rolvaliduntil AS valuntil, - setconfig AS useconfig + setconfig AS useconfig, + rollastupdated AS lastupdated FROM pg_authid LEFT JOIN pg_db_role_setting s ON (pg_authid.oid = setrole AND setdatabase = 0) WHERE rolcanlogin; @@ -72,7 +74,8 @@ CREATE VIEW pg_user AS usebypassrls, '********'::text as passwd, valuntil, - useconfig + useconfig, + lastupdated FROM pg_shadow; CREATE VIEW pg_policies AS diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 04b270c08a8..26eb41fff4a 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -39,6 +39,7 @@ #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/syscache.h" +#include "utils/timestamp.h" #include "utils/varlena.h" /* @@ -116,6 +117,7 @@ static void plan_recursive_revoke(CatCList *memlist, bool revoke_admin_option_only, DropBehavior behavior); static void InitGrantRoleOptions(GrantRoleOptions *popt); +static void update_role_lastupdated(Oid roleid); /* Check if current user has createrole privileges */ @@ -463,6 +465,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls); + new_record[Anum_pg_authid_rollastupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + /* * pg_largeobject_metadata contains pg_authid.oid's, so we use the * binary-upgrade override. @@ -960,6 +965,14 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) new_record_repl[Anum_pg_authid_rolbypassrls - 1] = true; } + /* + * Record that an ALTER ROLE command was executed for this role. + * The timestamp advances even when the command did not actually change any value. + */ + new_record[Anum_pg_authid_rollastupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + new_record_repl[Anum_pg_authid_rollastupdated - 1] = true; + new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, new_record_nulls, new_record_repl); CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple); @@ -999,6 +1012,48 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) return roleid; } +/* + * Set pg_authid.rollastupdated for the specified role to the current timestamp. + * + * Used by auxiliary commands (like ALTER ROLE ... SET) to ensure all role + * modifications update the timestamp consistently. + * + */ +static void +update_role_lastupdated(Oid roleid) +{ + Relation pg_authid_rel; + TupleDesc pg_authid_dsc; + HeapTuple tuple, + new_tuple; + Datum new_record[Natts_pg_authid] = {0}; + bool new_record_nulls[Natts_pg_authid] = {0}; + bool new_record_repl[Natts_pg_authid] = {0}; + + pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock); + pg_authid_dsc = RelationGetDescr(pg_authid_rel); + + /* + * The caller is expected to have already verified that the role exists + * and locked it, so a cache miss here indicates a bug. + */ + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for role %u", roleid); + + new_record[Anum_pg_authid_rollastupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + new_record_repl[Anum_pg_authid_rollastupdated - 1] = true; + + new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, + new_record_nulls, new_record_repl); + CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple); + + ReleaseSysCache(tuple); + heap_freetuple(new_tuple); + + table_close(pg_authid_rel, NoLock); +} /* * ALTER ROLE ... SET @@ -1086,6 +1141,13 @@ AlterRoleSet(AlterRoleSetStmt *stmt) AlterSetting(databaseid, roleid, stmt->setstmt); + /* + * ALTER ROLE ... SET alters role state outside AlterRole(), so update + * rollastupdated here. Ignore if stmt->role is NULL (global/database SET). + */ + if (OidIsValid(roleid)) + update_role_lastupdated(roleid); + return roleid; } @@ -1467,6 +1529,11 @@ RenameRole(const char *oldname, const char *newname) (errmsg("MD5 password cleared because of role rename"))); } + repl_repl[Anum_pg_authid_rollastupdated - 1] = true; + repl_val[Anum_pg_authid_rollastupdated - 1] = + TimestampTzGetDatum(GetCurrentTimestamp()); + repl_null[Anum_pg_authid_rollastupdated - 1] = false; + newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl); CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 31e49d5a8a7..17661cdedf3 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202608183 +#define CATALOG_VERSION_NO 202608201 #endif diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 2960707c729..0ba6e56f40b 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -25,86 +25,103 @@ rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6171', oid_symbol => 'ROLE_PG_DATABASE_OWNER', rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6181', oid_symbol => 'ROLE_PG_READ_ALL_DATA', rolname => 'pg_read_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6182', oid_symbol => 'ROLE_PG_WRITE_ALL_DATA', rolname => 'pg_write_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '3373', oid_symbol => 'ROLE_PG_MONITOR', rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '3374', oid_symbol => 'ROLE_PG_READ_ALL_SETTINGS', rolname => 'pg_read_all_settings', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '3375', oid_symbol => 'ROLE_PG_READ_ALL_STATS', rolname => 'pg_read_all_stats', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '3377', oid_symbol => 'ROLE_PG_STAT_SCAN_TABLES', rolname => 'pg_stat_scan_tables', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4569', oid_symbol => 'ROLE_PG_READ_SERVER_FILES', rolname => 'pg_read_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4570', oid_symbol => 'ROLE_PG_WRITE_SERVER_FILES', rolname => 'pg_write_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4571', oid_symbol => 'ROLE_PG_EXECUTE_SERVER_PROGRAM', rolname => 'pg_execute_server_program', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4200', oid_symbol => 'ROLE_PG_SIGNAL_BACKEND', rolname => 'pg_signal_backend', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINT', rolname => 'pg_checkpoint', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6337', oid_symbol => 'ROLE_PG_MAINTAIN', rolname => 'pg_maintain', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '4550', oid_symbol => 'ROLE_PG_USE_RESERVED_CONNECTIONS', rolname => 'pg_use_reserved_connections', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6304', oid_symbol => 'ROLE_PG_CREATE_SUBSCRIPTION', rolname => 'pg_create_subscription', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, { oid => '6392', oid_symbol => 'ROLE_PG_SIGNAL_AUTOVACUUM_WORKER', rolname => 'pg_signal_autovacuum_worker', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolpassword => '_null_', rolvaliduntil => '_null_', + rollastupdated => '_null_' }, ] diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index baf5b099797..ade9b078986 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -47,6 +47,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 #ifdef CATALOG_VARLEN /* variable-length fields start here */ text rolpassword; /* password, if any */ timestamptz rolvaliduntil; /* password expiration time, if any */ + timestamptz rollastupdated; /* time of last CREATE ROLE / ALTER ROLE + * on this role, if any */ #endif } FormData_pg_authid; diff --git a/src/test/regress/expected/role_lastupdated.out b/src/test/regress/expected/role_lastupdated.out new file mode 100644 index 00000000000..8bea8d6a6cf --- /dev/null +++ b/src/test/regress/expected/role_lastupdated.out @@ -0,0 +1,81 @@ +-- +-- Tests for pg_authid.rollastupdated +-- +-- rollastupdated records when the most recent CREATE ROLE or ALTER ROLE +-- command for a role was executed, so that tools managing roles +-- declaratively can cheaply detect that a role may have changed. +-- +-- Each command below is checked twice against the timestamp recorded +-- before it ran: first while nothing has happened yet, where no row +-- should qualify, and again afterwards, where the role should appear. +-- +-- roles created during initdb have no recorded modification time +SELECT rollastupdated IS NULL AS bootstrap_role_is_null + FROM pg_authid WHERE rolname = 'pg_read_all_data'; + bootstrap_role_is_null +------------------------ + t +(1 row) + +-- CREATE ROLE records a timestamp +CREATE ROLE regress_lastupdated_role; +SELECT rollastupdated IS NOT NULL AS create_sets_lastupdated + FROM pg_authid WHERE rolname = 'regress_lastupdated_role'; + create_sets_lastupdated +------------------------- + t +(1 row) + +SELECT rollastupdated AS ts_created + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset +-- changing an attribute advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_created'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_lastupdated_role WITH CONNECTION LIMIT 5; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_created'; + rolname +-------------------------- + regress_lastupdated_role +(1 row) + +SELECT rollastupdated AS ts_altered + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset +-- ALTER ROLE ... SET stores the setting outside pg_authid, but still +-- advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_altered'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_lastupdated_role SET work_mem = '10MB'; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_altered'; + rolname +-------------------------- + regress_lastupdated_role +(1 row) + +SELECT rollastupdated AS ts_set + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset +-- a rename advances it too, and the value is visible through pg_roles +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_set'; + rolname +--------- +(0 rows) + +ALTER ROLE regress_lastupdated_role RENAME TO regress_lastupdated_role2; +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_lastupdated_role2' AND rollastupdated > :'ts_set'; + rolname +--------------------------- + regress_lastupdated_role2 +(1 row) + +DROP ROLE regress_lastupdated_role2; diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 1a29d46213e..41512033566 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1525,7 +1525,8 @@ pg_roles| SELECT pg_authid.rolname, pg_authid.rolvaliduntil, pg_authid.rolbypassrls, s.setconfig AS rolconfig, - pg_authid.oid + pg_authid.oid, + pg_authid.rollastupdated FROM (pg_authid LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid)))); pg_rules| SELECT n.nspname AS schemaname, @@ -1763,7 +1764,8 @@ pg_shadow| SELECT pg_authid.rolname AS usename, pg_authid.rolbypassrls AS usebypassrls, pg_authid.rolpassword AS passwd, pg_authid.rolvaliduntil AS valuntil, - s.setconfig AS useconfig + s.setconfig AS useconfig, + pg_authid.rollastupdated AS lastupdated FROM (pg_authid LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid)))) WHERE pg_authid.rolcanlogin; @@ -2876,7 +2878,8 @@ pg_user| SELECT usename, usebypassrls, '********'::text AS passwd, valuntil, - useconfig + useconfig, + lastupdated FROM pg_shadow; pg_user_mappings| SELECT u.oid AS umid, s.oid AS srvid, diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..f27e642d181 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -76,7 +76,7 @@ test: brin_bloom brin_multi # ---------- # Another group of parallel tests # ---------- -test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions nls sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual +test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions nls sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role role_lastupdated without_overlaps generated_virtual # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other # psql depends on create_am diff --git a/src/test/regress/sql/role_lastupdated.sql b/src/test/regress/sql/role_lastupdated.sql new file mode 100644 index 00000000000..a8c4dbcb71c --- /dev/null +++ b/src/test/regress/sql/role_lastupdated.sql @@ -0,0 +1,53 @@ +-- +-- Tests for pg_authid.rollastupdated +-- +-- rollastupdated records when the most recent CREATE ROLE or ALTER ROLE +-- command for a role was executed, so that tools managing roles +-- declaratively can cheaply detect that a role may have changed. +-- +-- Each command below is checked twice against the timestamp recorded +-- before it ran: first while nothing has happened yet, where no row +-- should qualify, and again afterwards, where the role should appear. +-- + +-- roles created during initdb have no recorded modification time +SELECT rollastupdated IS NULL AS bootstrap_role_is_null + FROM pg_authid WHERE rolname = 'pg_read_all_data'; + +-- CREATE ROLE records a timestamp +CREATE ROLE regress_lastupdated_role; +SELECT rollastupdated IS NOT NULL AS create_sets_lastupdated + FROM pg_authid WHERE rolname = 'regress_lastupdated_role'; + +SELECT rollastupdated AS ts_created + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset + +-- changing an attribute advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_created'; +ALTER ROLE regress_lastupdated_role WITH CONNECTION LIMIT 5; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_created'; + +SELECT rollastupdated AS ts_altered + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset + +-- ALTER ROLE ... SET stores the setting outside pg_authid, but still +-- advances it +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_altered'; +ALTER ROLE regress_lastupdated_role SET work_mem = '10MB'; +SELECT rolname FROM pg_authid + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_altered'; + +SELECT rollastupdated AS ts_set + FROM pg_authid WHERE rolname = 'regress_lastupdated_role' \gset + +-- a rename advances it too, and the value is visible through pg_roles +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_lastupdated_role' AND rollastupdated > :'ts_set'; +ALTER ROLE regress_lastupdated_role RENAME TO regress_lastupdated_role2; +SELECT rolname FROM pg_roles + WHERE rolname = 'regress_lastupdated_role2' AND rollastupdated > :'ts_set'; + +DROP ROLE regress_lastupdated_role2; -- 2.55.0