diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7a59e65f2d3..91a982e8ba2 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -34,6 +34,7 @@ #include "access/parallel.h" #include "access/printtup.h" #include "access/xact.h" +#include "catalog/pg_authid.h" #include "catalog/pg_type.h" #include "commands/async.h" #include "commands/event_trigger.h" @@ -76,10 +77,12 @@ #include "tcop/utility.h" #include "utils/guc_hooks.h" #include "utils/injection_point.h" +#include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/snapmgr.h" +#include "utils/syscache.h" #include "utils/timeout.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -115,6 +118,13 @@ int client_connection_check_interval = 0; /* flags for non-system relation kinds to restrict use */ int restrict_nonsystem_relation_kind; +/* + * Flag set by syscache listener to indicate if the user's password validity + * (rolvaliduntil) needs to be checked for expiration before the next + * command execution. + */ +static bool AuthCheckNeeded = false; + /* ---------------- * private typedefs etc * ---------------- @@ -196,6 +206,7 @@ static void drop_unnamed_stmt(void); static void log_disconnections(int code, Datum arg); static void enable_statement_timeout(void); static void disable_statement_timeout(void); +static void CheckPasswordExpiration(void); /* ---------------------------------------------------------------- @@ -1074,6 +1085,13 @@ exec_simple_query(const char *query_string) */ start_xact_command(); + /* + * Check for password expiration if a relevant pg_authid change has occurred. + * AuthCheckNeeded is set by the syscache listener (AuthCacheInvalidated). + */ + if (AuthCheckNeeded) + CheckPasswordExpiration(); + /* * Zap any pre-existing unnamed statement. (While not strictly necessary, * it seems best to define simple-Query mode as if it used the unnamed @@ -4213,6 +4231,81 @@ PostgresSingleUserMain(int argc, char *argv[], PostgresMain(dbname, username); } +/* + * CheckPasswordExpiration + * + * Checks the current user's pg_authid.rolvaliduntil timestamp against the + * current transaction start time. If the role's valid-until time is in the + * past, the function terminates the backend process with a FATAL error, + * enforcing password expiration policy. + * This function is typically called only when the AuthCheckNeeded flag is set + * by the AuthCacheInvalidated syscache callback. + */ +static void +CheckPasswordExpiration(void) +{ + HeapTuple tuple; + + /* + * Look up the current user's entry in pg_authid. We must do this, even + * if only AuthCheckNeeded is set, because GetUserId() might return a + * different user ID than the one that triggered the invalidation (though + * that's unlikely for AUTHOID). + */ + + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(GetUserId())); + + if (HeapTupleIsValid(tuple)) + { + Datum rolvaliduntil_datum; + bool validUntil_null; + + /* Get the expiration time column */ + rolvaliduntil_datum = SysCacheGetAttr(AUTHNAME, tuple, + Anum_pg_authid_rolvaliduntil, + &validUntil_null); + + if (!validUntil_null) + { + TimestampTz expiration_time = DatumGetTimestampTz(rolvaliduntil_datum); + TimestampTz now = GetCurrentTransactionStartTimestamp(); + + if (expiration_time < now) + { + ReleaseSysCache(tuple); + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("Connection expired due to internal password policy enforcement"), + errdetail("User's password expired at %s.", + timestamptz_to_str(expiration_time)), + errhint("Reconnect with a renewed password or obtain new authorization."))); + } + } + + ReleaseSysCache(tuple); + } + /* Reset the flag after performing the check */ + AuthCheckNeeded = false; +} + +/* + * AuthCacheInvalidated + * Syscache callback function registered for the AUTHOID cache (pg_authid). + * + * This function is executed whenever a tuple in pg_authid is updated, inserted, + * or deleted. Its primary purpose is to catch changes to the currently + * connected user's 'rolvaliduntil' field. + * + * It sets the static flag AuthCheckNeeded to true, signaling to the main + * execution loop (exec_simple_query) that the user's password expiration + * status must be checked before the next command runs. + */ +static void +AuthCacheInvalidated(Datum arg, int cacheid, uint32 hashvalue) +{ + /* This callback is executed when an entry in pg_authid changes */ + AuthCheckNeeded = true; +} /* ---------------------------------------------------------------- * PostgresMain @@ -4554,6 +4647,16 @@ PostgresMain(const char *dbname, const char *username) if (!ignore_till_sync) send_ready_for_query = true; /* initially, or after error */ + + /* + * Register a SysCache listener for pg_authid changes (specifically for + * rolvaliduntil). This provides an event-driven mechanism to enforce + * password/authorization expiration immediately upon change, rather than + * relying on polling. The callback sets a flag (AuthCheckNeeded) which + * is checked before executing each simple query. + */ + CacheRegisterSyscacheCallback(AUTHOID, AuthCacheInvalidated, (Datum) 0); + /* * Non-error queries loop here. */