From 256426ff6b1cfd39d07cf2a426e46d3f677d1e58 Mon Sep 17 00:00:00 2001 From: Ajit Awekar Date: Thu, 17 Sep 2026 13:03:46 +0530 Subject: [PATCH v2 1/5] Add continuous credential validation framework Introduce a mechanism that periodically re-validates the credentials of an active session and terminates the session if they are no longer valid. A per-backend timer (CREDENTIAL_VALIDATION_TIMEOUT) fires at a configurable interval; the pending flag is acted on at the next command boundary in the main loop, where the check runs inside a short-lived transaction (reusing the session's open transaction if one exists, so a long-running transaction block is still validated at each command boundary). This commit adds the framework and the baseline, auth-method-independent check: the authenticated (login) role must still exist and must not have passed its rolvaliduntil expiration. This is checked against GetAuthenticatedUserId(), not the current session or effective role, so SET SESSION AUTHORIZATION / SET ROLE cannot mask the original login role's expiry or removal. Method-specific validators plug in via RegisterCredentialValidator() and are added in following commits. Two GUCs control the feature: credential_validation_enabled (default off) and credential_validation_interval (5..3600 seconds, default 60). --- doc/src/sgml/config.sgml | 45 +++ src/backend/libpq/Makefile | 2 + src/backend/libpq/auth-validate-methods.c | 79 ++++ src/backend/libpq/auth-validate.c | 243 ++++++++++++ src/backend/libpq/meson.build | 2 + src/backend/tcop/postgres.c | 26 ++ src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 18 + src/backend/utils/misc/guc_parameters.dat | 16 + src/backend/utils/misc/guc_tables.c | 1 + src/backend/utils/misc/postgresql.conf.sample | 6 + src/include/libpq/auth-validate-methods.h | 28 ++ src/include/libpq/auth-validate.h | 54 +++ src/include/miscadmin.h | 1 + src/include/utils/timeout.h | 1 + src/test/authentication/meson.build | 1 + .../t/008_continuous_validation.pl | 355 ++++++++++++++++++ 17 files changed, 879 insertions(+) create mode 100644 src/backend/libpq/auth-validate-methods.c create mode 100644 src/backend/libpq/auth-validate.c create mode 100644 src/include/libpq/auth-validate-methods.h create mode 100644 src/include/libpq/auth-validate.h create mode 100755 src/test/authentication/t/008_continuous_validation.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 0165eb9ec02..cee36ace30c 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1124,6 +1124,51 @@ include_dir 'conf.d' + + credential_validation_enabled (boolean) + + credential_validation_enabled configuration parameter + + + + + + When enabled, each backend periodically re-validates the credentials of + its active session and terminates the session if they are no longer + valid. The baseline check verifies that the authenticated role still + exists and has not passed its VALID UNTIL expiration; + depending on the authentication method, an additional method-specific + check is applied, such as expiration of an OAuth + bearer token or of the client certificate. The default is + off. + + + The re-validation period is controlled by + . Validation is + performed at command boundaries, so a session is never interrupted in + the middle of a running statement. + + + + + + credential_validation_interval (integer) + + credential_validation_interval configuration parameter + + + + + + Sets the interval between the periodic credential re-validations that are + performed when is + enabled. If this value is specified without units, it is taken as + seconds. The valid range is from 5 seconds to 3600 seconds (one hour), + and the default is 60 seconds. + + + + password_encryption (enum) diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 98eb2a8242d..32e4c7280e5 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -18,6 +18,8 @@ OBJS = \ auth-oauth.o \ auth-sasl.o \ auth-scram.o \ + auth-validate-methods.o \ + auth-validate.o \ auth.o \ be-fsstubs.o \ be-secure-common.o \ diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c new file mode 100644 index 00000000000..e91d5d3c084 --- /dev/null +++ b/src/backend/libpq/auth-validate-methods.c @@ -0,0 +1,79 @@ +/*------------------------------------------------------------------------- + * + * auth-validate-methods.c + * Implementation of authentication credential validation methods + * + * This module implements the credential validators. The baseline role-level + * check (rolvaliduntil / role existence) implemented here is applied to every + * authenticated session, regardless of authentication method. Method-specific + * validators are registered with the framework via + * RegisterCredentialValidator(). + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/libpq/auth-validate-methods.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_authid.h" +#include "libpq/auth-validate-methods.h" +#include "miscadmin.h" +#include "utils/syscache.h" +#include "utils/timestamp.h" + +/* + * Initialize validation methods + */ +void +InitializeValidationMethods(void) +{ + /* No method-specific validators are registered yet. */ +} + +/* + * Baseline role-level credential check, applied to every authenticated + * session regardless of authentication method. + * + * Checks pg_authid.rolvaliduntil for the authenticated role; this is + * role-level and auth-method-independent, so it governs password, + * certificate, OAuth, etc. sessions alike. Also treats a role that no + * longer exists as invalid. + * + * Uses GetAuthenticatedUserId(), not GetSessionUserId(), so that SET SESSION + * AUTHORIZATION can't mask the original login role's expiry or removal. + * + * Returns true if the role is still valid, false if it has expired or has + * been dropped. + */ +bool +ValidateRoleValidity(void) +{ + HeapTuple tuple; + Datum datum; + bool isnull; + TimestampTz valid_until; + bool result; + + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(GetAuthenticatedUserId())); + + if (!HeapTupleIsValid(tuple)) + return false; /* role no longer exists */ + + datum = SysCacheGetAttr(AUTHOID, tuple, + Anum_pg_authid_rolvaliduntil, + &isnull); + if (!isnull) + { + valid_until = DatumGetTimestampTz(datum); + result = (valid_until >= GetCurrentTimestamp()); + } + else + result = true; /* no expiration set */ + + ReleaseSysCache(tuple); + return result; +} diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c new file mode 100644 index 00000000000..c3adbf948e0 --- /dev/null +++ b/src/backend/libpq/auth-validate.c @@ -0,0 +1,243 @@ +/*------------------------------------------------------------------------- + * + * auth-validate.c + * Implementation of authentication credential validation + * + * This module provides a mechanism for validating credentials during + * an active PostgreSQL session. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/libpq/auth-validate.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xact.h" +#include "libpq/auth-validate-methods.h" +#include "libpq/auth-validate.h" +#include "libpq/auth.h" +#include "libpq/libpq-be.h" +#include "miscadmin.h" +#include "postmaster/postmaster.h" +#include "storage/ipc.h" +#include "utils/timeout.h" + +/* GUC variables */ +bool credential_validation_enabled; +int credential_validation_interval; + + +/* Registered credential validators */ +static CredentialValidationCallback validators[CVT_COUNT]; + + +/* + * Convert UserAuth enum to CredentialValidationType for validator selection + */ +static CredentialValidationType +UserAuthToValidationType(UserAuth auth_method) +{ + switch (auth_method) + { + case uaOAuth: + return CVT_OAUTH; + case uaCert: + return CVT_CERT; + default: + /* + * No method-specific validator for other auth methods. Password + * methods (password/md5/scram) fall here intentionally: their only + * credential check is role-level (rolvaliduntil), which is handled + * for every session by the baseline ValidateRoleValidity(). + */ + return CVT_COUNT; /* Invalid value */ + } +} + +/* + * ProcessCredentialValidation + * + * Called from the main command loop when a credential validation cycle is + * due. Runs a full validity check and terminates the session with FATAL if + * the credentials have expired. + */ +void +ProcessCredentialValidation(void) +{ + bool valid; + bool own_xact = false; + + if (ClientAuthInProgress || IsInitProcessingMode() || IsBootstrapProcessingMode()) + return; + + if (!credential_validation_enabled || MyClientConnectionInfo.authn_id == NULL) + return; + + /* + * The validators read the system catalogs, which requires a live, + * non-aborted transaction. In an aborted transaction block catalog + * access is not possible, so skip this cycle and retry at the next + * interval. + */ + if (IsAbortedTransactionBlockState()) + return; + + /* + * Between commands there is no transaction, so start a short-lived one of + * our own. Inside an open transaction block (or a multi-message + * extended-query sequence) reuse the existing transaction, so that + * validation still happens at each command boundary within the block; but + * do not commit it, since it belongs to the user. + */ + if (!IsTransactionState()) + { + StartTransactionCommand(); + own_xact = true; + } + + valid = CheckCredentialValidity(); + + if (own_xact) + CommitTransactionCommand(); + + if (!valid) + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("session credentials have expired"), + errhint("Please reconnect to establish a new authenticated session."))); +} + +/* + * InitializeCredentialValidation + * + * Called from InitPostgres after authentication completes. Registers all + * method-specific validation callbacks. + */ +void +InitializeCredentialValidation(void) +{ + int i; + + /* Initialize validator callbacks to NULL */ + for (i = 0; i < CVT_COUNT; i++) + validators[i] = NULL; + + /* Register all method-specific validation callbacks */ + InitializeValidationMethods(); +} + +/* + * Enable or re-enable the credential validation timeout timer. + * Called at session startup and after each validation or error recovery. + */ +void +EnableCredentialValidationTimeout(void) +{ + int interval_ms; + + /* Only enable if credential validation is configured */ + if (!credential_validation_enabled) + return; + + /* Skip for non-client backends */ + if (!IsExternalConnectionBackend(MyBackendType)) + return; + + /* + * Skip for replication connections. Once a walsender enters its own + * streaming loop it never returns to the command dispatch loop where + * CredentialValidationTimeoutPending is consumed, so arming this timer + * for it would just leave a pending flag that is never acted on. + * Replication connections are out of scope for this feature. + */ + if (AmWalSenderProcess()) + return; + + /* Convert interval from seconds to milliseconds */ + interval_ms = credential_validation_interval * 1000; + + enable_timeout_after(CREDENTIAL_VALIDATION_TIMEOUT, interval_ms); + + elog(DEBUG1, "credential validation timeout enabled, interval=%d s", credential_validation_interval); +} + +/* + * Register a validator callback for a specific authentication method + */ +void +RegisterCredentialValidator(CredentialValidationType method_type, CredentialValidationCallback validator) +{ + if (method_type < 0 || method_type >= CVT_COUNT) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid validation method type: %d", method_type))); + + validators[method_type] = validator; +} + +/* + * Check credential validity for the current session. + * + * Returns true if the credentials are still valid, false if they have expired. + * Must be called within a transaction (the validators read the catalogs). + */ +bool +CheckCredentialValidity(void) +{ + CredentialValidationCallback validator = NULL; + CredentialValidationType validation_type; + bool result; + + /* + * Skip validation (treat as valid) for any process that does not have a + * client session with credentials to validate: + * - during shutdown + * - non-client backends (autovacuum, background workers, etc.) + * - replication connections (walsenders never return to the command + * dispatch loop that consumes CredentialValidationTimeoutPending once + * they start streaming, so they are out of scope for this feature) + * - while authentication is still in progress + * + * Note: we do NOT skip validation during RecoveryInProgress() -- a + * hot-standby server accepts ordinary read-only client sessions, and + * those need the same periodic re-validation as on a primary. + */ + if (proc_exit_inprogress || + !IsExternalConnectionBackend(MyBackendType) || + AmWalSenderProcess() || + AmAutoVacuumLauncherProcess() || + AmAutoVacuumWorkerProcess() || + AmBackgroundWorkerProcess() || + ClientAuthInProgress) + return true; + + /* Without an authenticated session there is nothing to validate. */ + if (MyClientConnectionInfo.authn_id == NULL) + return true; + + elog(DEBUG1, "credential validation: checking auth_method=%d", + (int) MyClientConnectionInfo.auth_method); + + /* + * Role-level validity (rolvaliduntil / role existence) is a baseline that + * applies to every authenticated session, regardless of auth method. + */ + result = ValidateRoleValidity(); + + /* + * Additionally run the method-specific validator if one is registered for + * this auth method (e.g. OAuth token expiry, client certificate expiry). + */ + validation_type = UserAuthToValidationType(MyClientConnectionInfo.auth_method); + if (validation_type < CVT_COUNT) + validator = validators[validation_type]; + + if (result && validator != NULL) + result = validator(); + + return result; +} diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build index 8571f652844..2e69685672b 100644 --- a/src/backend/libpq/meson.build +++ b/src/backend/libpq/meson.build @@ -4,6 +4,8 @@ backend_sources += files( 'auth-oauth.c', 'auth-sasl.c', 'auth-scram.c', + 'auth-validate-methods.c', + 'auth-validate.c', 'auth.c', 'be-fsstubs.c', 'be-secure-common.c', diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b6bdfe213fe..89c61a5d7f2 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -42,6 +42,7 @@ #include "commands/repack.h" #include "common/pg_prng.h" #include "jit/jit.h" +#include "libpq/auth-validate.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/pqsignal.h" @@ -4771,6 +4772,11 @@ PostgresMain(const char *dbname, const char *username) enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT, IdleInTransactionSessionTimeout); } + + /* Re-enable credential validation timer if needed */ + if (credential_validation_enabled && + !get_timeout_active(CREDENTIAL_VALIDATION_TIMEOUT)) + EnableCredentialValidationTimeout(); } else { @@ -4823,6 +4829,11 @@ PostgresMain(const char *dbname, const char *username) enable_timeout_after(IDLE_SESSION_TIMEOUT, IdleSessionTimeout); } + + /* Re-enable credential validation timer if needed */ + if (credential_validation_enabled && + !get_timeout_active(CREDENTIAL_VALIDATION_TIMEOUT)) + EnableCredentialValidationTimeout(); } /* Report any recently-changed GUC options */ @@ -4925,6 +4936,21 @@ PostgresMain(const char *dbname, const char *username) if (ignore_till_sync && firstchar != EOF) continue; + /* + * If a credential validation cycle came due while we were processing + * the previous command or waiting for input, run it now -- a single + * check-point that covers every command type below. This is a safe + * spot: we are between commands and hold no locks. Skip it while + * still starting up (e.g. during replication command setup), matching + * the IsNormalProcessingMode() guard used elsewhere. + */ + if (CredentialValidationTimeoutPending && IsNormalProcessingMode()) + { + CredentialValidationTimeoutPending = false; + ProcessCredentialValidation(); /* may FATAL if credentials expired */ + EnableCredentialValidationTimeout(); + } + switch (firstchar) { case PqMsg_Query: diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index bbd28d14d99..5b9df8fd3f1 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -34,6 +34,7 @@ volatile sig_atomic_t QueryCancelPending = false; volatile sig_atomic_t ProcDiePending = false; volatile sig_atomic_t CheckClientConnectionPending = false; volatile sig_atomic_t ClientConnectionLost = false; +volatile sig_atomic_t CredentialValidationTimeoutPending = false; volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false; volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 8b6ea195eca..959b1482e67 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -33,6 +33,7 @@ #include "catalog/pg_database.h" #include "catalog/pg_db_role_setting.h" #include "catalog/pg_tablespace.h" +#include "libpq/auth-validate.h" #include "libpq/auth.h" #include "libpq/libpq-be.h" #include "mb/pg_wchar.h" @@ -96,6 +97,7 @@ static void TransactionTimeoutHandler(void); static void IdleSessionTimeoutHandler(void); static void IdleStatsUpdateTimeoutHandler(void); static void ClientCheckTimeoutHandler(void); +static void CredentialValidationTimeoutHandler(void); static bool ThereIsAtLeastOneRole(void); static void process_startup_options(Port *port, bool am_superuser); static void process_settings(Oid databaseid, Oid roleid); @@ -806,6 +808,8 @@ InitPostgres(const char *in_dbname, Oid dboid, RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler); RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT, IdleStatsUpdateTimeoutHandler); + RegisterTimeout(CREDENTIAL_VALIDATION_TIMEOUT, + CredentialValidationTimeoutHandler); } /* @@ -1269,6 +1273,12 @@ InitPostgres(const char *in_dbname, Oid dboid, /* Initialize this backend's session state. */ InitializeSession(); + /* Initialize credential validation system */ + InitializeCredentialValidation(); + + /* Enable credential validation timeout if configured */ + EnableCredentialValidationTimeout(); + /* * If this is an interactive session, load any libraries that should be * preloaded at backend start. Since those are determined by GUCs, this @@ -1475,6 +1485,14 @@ IdleStatsUpdateTimeoutHandler(void) SetLatch(MyLatch); } +static void +CredentialValidationTimeoutHandler(void) +{ + CredentialValidationTimeoutPending = true; + InterruptPending = true; + SetLatch(MyLatch); +} + static void ClientCheckTimeoutHandler(void) { diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index c57441f7d98..d313ed5af0d 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -569,6 +569,22 @@ assign_hook => 'assign_createrole_self_grant', }, +{ name => 'credential_validation_enabled', type => 'bool', context => 'PGC_SUSET', group => 'CONN_AUTH_AUTH', + short_desc => 'Enables periodic re-validation of session credentials.', + long_desc => 'When enabled, each backend periodically re-checks that the authenticated role has not expired and that any method-specific credential (OAuth token, client certificate) is still valid.', + variable => 'credential_validation_enabled', + boot_val => 'false', +}, + +{ name => 'credential_validation_interval', type => 'int', context => 'PGC_SUSET', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets the interval in seconds between credential re-validation checks.', + flags => 'GUC_UNIT_S', + variable => 'credential_validation_interval', + boot_val => '60', + min => '5', + max => '3600', +}, + { name => 'cursor_tuple_fraction', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', short_desc => 'Sets the planner\'s estimate of the fraction of a cursor\'s rows that will be retrieved.', flags => 'GUC_EXPLAIN', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 342aaeef59a..fd782b8b5fe 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -51,6 +51,7 @@ #include "common/file_utils.h" #include "common/scram-common.h" #include "jit/jit.h" +#include "libpq/auth-validate.h" #include "libpq/auth.h" #include "libpq/libpq.h" #include "libpq/oauth.h" diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e759f06b50f..9813a43b66e 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -929,6 +929,12 @@ #include_if_exists = '...' # include file only if it exists #include = '...' # include file +#------------------------------------------------------------------------------ +# CREDENTIAL VALIDATION +#------------------------------------------------------------------------------ + +#credential_validation_enabled = off # re-validate session credentials periodically +#credential_validation_interval = 60 # revalidation interval in seconds (5-3600) #------------------------------------------------------------------------------ # CUSTOMIZED OPTIONS diff --git a/src/include/libpq/auth-validate-methods.h b/src/include/libpq/auth-validate-methods.h new file mode 100644 index 00000000000..9a03583d6e1 --- /dev/null +++ b/src/include/libpq/auth-validate-methods.h @@ -0,0 +1,28 @@ +/*------------------------------------------------------------------------- + * + * auth-validate-methods.h + * Interface for authentication credential validation methods + * + * This file provides declarations for various credential validation methods + * used with the credential validation system. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/libpq/auth-validate-methods.h + * + *------------------------------------------------------------------------- + */ +#ifndef AUTH_VALIDATE_METHODS_H +#define AUTH_VALIDATE_METHODS_H + +/* Initialize all validation methods */ +extern void InitializeValidationMethods(void); + +/* + * Baseline role-level validity check (rolvaliduntil / role existence), + * applied to every authenticated session regardless of auth method. + */ +extern bool ValidateRoleValidity(void); + +#endif /* AUTH_VALIDATE_METHODS_H */ diff --git a/src/include/libpq/auth-validate.h b/src/include/libpq/auth-validate.h new file mode 100644 index 00000000000..b0b5a1144a7 --- /dev/null +++ b/src/include/libpq/auth-validate.h @@ -0,0 +1,54 @@ +/*------------------------------------------------------------------------- + * + * auth-validate.h + * Interface for authentication credential validation + * + * This file provides a common interface for validating credentials + * during an active PostgreSQL session. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/libpq/auth-validate.h + * + *------------------------------------------------------------------------- + */ +#ifndef AUTH_VALIDATE_H +#define AUTH_VALIDATE_H + +/* Define credential validation method types as an enum */ +typedef enum CredentialValidationType +{ + CVT_OAUTH = 0, /* OAuth bearer token authentication */ + CVT_CERT, /* TLS client certificate authentication */ + CVT_COUNT /* Total number of credential validation types */ +} CredentialValidationType; + +/* Process credential validation */ +extern void ProcessCredentialValidation(void); + +/* GUC variables */ +extern PGDLLIMPORT bool credential_validation_enabled; +extern PGDLLIMPORT int credential_validation_interval; + +/* Common credential validation callback prototype */ +typedef bool (*CredentialValidationCallback) (void); + +/* Initialize credential validation system */ +extern void InitializeCredentialValidation(void); + +/* Register a validation callback for a specific authentication method */ +extern void RegisterCredentialValidator(CredentialValidationType method_type, + CredentialValidationCallback validator); + +/* + * Check credential validity for the current session. Returns true if the + * credentials are still valid, false if they have expired. Must be called + * within a transaction, since the validators read the system catalogs. + */ +extern bool CheckCredentialValidity(void); + +/* Enable credential validation timeout timer */ +extern void EnableCredentialValidationTimeout(void); + +#endif /* AUTH_VALIDATE_H */ diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 0fc59af02b9..7d8eadab705 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -101,6 +101,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; +extern PGDLLIMPORT volatile sig_atomic_t CredentialValidationTimeoutPending; /* these are marked volatile because they are examined by signal handlers: */ extern PGDLLIMPORT volatile uint32 InterruptHoldoffCount; diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h index 4bae392fbd3..06450a5d412 100644 --- a/src/include/utils/timeout.h +++ b/src/include/utils/timeout.h @@ -36,6 +36,7 @@ typedef enum TimeoutId IDLE_STATS_UPDATE_TIMEOUT, CLIENT_CONNECTION_CHECK_TIMEOUT, STARTUP_PROGRESS_TIMEOUT, + CREDENTIAL_VALIDATION_TIMEOUT, /* First user-definable timeout reason */ USER_TIMEOUT, /* Maximum number of timeout reasons */ diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build index 282a5054e2c..bfb8350a3f8 100644 --- a/src/test/authentication/meson.build +++ b/src/test/authentication/meson.build @@ -16,6 +16,7 @@ tests += { 't/005_sspi.pl', 't/006_login_trigger.pl', 't/007_pre_auth.pl', + 't/008_continuous_validation.pl', ], }, } diff --git a/src/test/authentication/t/008_continuous_validation.pl b/src/test/authentication/t/008_continuous_validation.pl new file mode 100755 index 00000000000..e3149f6aa29 --- /dev/null +++ b/src/test/authentication/t/008_continuous_validation.pl @@ -0,0 +1,355 @@ +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if (!$use_unix_sockets) +{ + plan skip_all => "authentication tests cannot run without Unix-domain sockets"; +} + +# Helper to reset pg_hba.conf with specific auth method for test users +sub reset_pg_hba +{ + my ($node, $hba_method, @users) = @_; + + unlink($node->data_dir . '/pg_hba.conf'); + # Each specified user uses the given method + foreach my $user (@users) + { + $node->append_conf('pg_hba.conf', "local all $user $hba_method\n"); + } + # Others use trust + $node->append_conf('pg_hba.conf', "local all all trust\n"); + $node->reload; +} + +# 1. Initialize and start the PostgreSQL cluster +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; + +# Enable credential validation with short interval (5 seconds minimum) +$node->append_conf('postgresql.conf', "credential_validation_enabled = on\n"); +$node->append_conf('postgresql.conf', "credential_validation_interval = 5\n"); + +$node->start; + +# Configure password auth for user1 and user2 (must be BEFORE "all all trust") +reset_pg_hba($node, 'md5', 'user1', 'user2'); + +# Create test users with passwords +$node->safe_psql('postgres', "CREATE USER user1 LOGIN PASSWORD 'secret';"); +$node->safe_psql('postgres', "CREATE USER user2 LOGIN PASSWORD 'secret2';"); + +############################################################################# +# Test 1: VALID UNTIL expiration +############################################################################# +note "=== Test 1: VALID UNTIL expiration ==="; + +$ENV{PGPASSWORD} = 'secret'; +my $session1 = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user1'] +); + +# Verify user1 can execute a query normally +my ($stdout, $ret) = $session1->query('SELECT 1 AS success;'); +like($stdout, qr/1/, 'user1 can execute queries initially'); +is($ret, 0, 'no errors during initial query for user1'); + +# Admin alters the VALID UNTIL date to the past +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2025-11-02 16:59:37+05:30';"); + +# Wait for the credential validation timeout to fire +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +# User1 attempts to execute another query - should be terminated +eval { + ($stdout, $ret) = $session1->query('SELECT 2 AS failure_expected;'); +}; + +# Check the server log for the expected FATAL error +my $log_contents = slurp_file($node->logfile); +like( + $log_contents, + qr/FATAL:.*session credentials have expired/, + 'Test 1: server log shows session terminated due to expired credentials' +); + +eval { $session1->quit; }; + +############################################################################# +# Test 2: User dropped while session is active +############################################################################# +note "=== Test 2: User dropped while session is active ==="; + +$ENV{PGPASSWORD} = 'secret2'; +my $session2 = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user2'] +); + +# Verify user2 can execute a query normally +($stdout, $ret) = $session2->query('SELECT 1 AS success;'); +like($stdout, qr/1/, 'user2 can execute queries initially'); +is($ret, 0, 'no errors during initial query for user2'); + +# Admin drops user2 while the session is still active +$node->safe_psql('postgres', "DROP USER user2;"); + +# Wait for the credential validation timeout to fire +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +# User2 attempts to execute another query - should be terminated +eval { + ($stdout, $ret) = $session2->query('SELECT 2 AS failure_expected;'); +}; + +# Check the server log for the expected FATAL error (user no longer exists) +$log_contents = slurp_file($node->logfile); +like( + $log_contents, + qr/FATAL:.*session credentials have expired/, + 'Test 2: server log shows session terminated after user was dropped' +); + +eval { $session2->quit; }; + +############################################################################# +# Test 3: VALID UNTIL extended keeps session alive (positive test) +############################################################################# +note "=== Test 3: VALID UNTIL extended keeps session alive ==="; + +# Reset user1 for this test (user1 still exists from Test 1, just expired) +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';"); +reset_pg_hba($node, 'md5', 'user1'); + +$ENV{PGPASSWORD} = 'secret'; +my $session3 = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user1'] +); + +# Set VALID UNTIL to far future +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2099-12-31 23:59:59';"); + +# Wait for validation cycle +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +# Session should still be alive +($stdout, $ret) = $session3->query('SELECT 1 AS still_alive;'); +like($stdout, qr/1/, 'Test 3: session remains alive with valid VALID UNTIL'); +is($ret, 0, 'Test 3: no errors when VALID UNTIL is in the future'); + +eval { $session3->quit; }; + +############################################################################# +# Test 4: Multiple sessions terminated when user expires +############################################################################# +note "=== Test 4: Multiple sessions terminated when user expires ==="; + +# Reset user1 +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';"); + +$ENV{PGPASSWORD} = 'secret'; +my $session4a = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user1'] +); +my $session4b = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user1'] +); + +# Verify both sessions work +($stdout, $ret) = $session4a->query('SELECT 1;'); +like($stdout, qr/1/, 'session4a works initially'); +($stdout, $ret) = $session4b->query('SELECT 1;'); +like($stdout, qr/1/, 'session4b works initially'); + +# Expire user1 +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2020-01-01';"); + +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +# Both sessions should fail +eval { $session4a->query('SELECT 2;'); }; +eval { $session4b->query('SELECT 2;'); }; + +$log_contents = slurp_file($node->logfile); +# Count occurrences of the termination message +my @matches = ($log_contents =~ /FATAL:.*session credentials have expired/g); +cmp_ok(scalar(@matches), '>=', 3, 'Test 4: multiple sessions terminated for same user'); + +eval { $session4a->quit; }; +eval { $session4b->quit; }; + +############################################################################# +# Test 5: Trust auth sessions are not affected +############################################################################# +note "=== Test 5: Trust auth sessions are not affected ==="; + +# Create user3 with trust auth (no password validation registered) +$node->safe_psql('postgres', "CREATE USER user3 LOGIN;"); +reset_pg_hba($node, 'trust', 'user3'); + +delete $ENV{PGPASSWORD}; +my $session5 = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user3'] +); + +# Set expired VALID UNTIL (but trust auth has no validator) +$node->safe_psql('postgres', "ALTER USER user3 VALID UNTIL '2020-01-01';"); + +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +# Session should still work - trust has no registered validator +($stdout, $ret) = $session5->query('SELECT 1 AS trust_still_works;'); +like($stdout, qr/1/, 'Test 5: trust auth session not terminated (no validator)'); + +eval { $session5->quit; }; + +############################################################################# +# Test 6: Credential validation disabled +############################################################################# +note "=== Test 6: Credential validation disabled ==="; + +# Disable credential validation +$node->safe_psql('postgres', "ALTER SYSTEM SET credential_validation_enabled = off;"); +$node->reload; + +# Reset user1 +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';"); +reset_pg_hba($node, 'md5', 'user1'); + +$ENV{PGPASSWORD} = 'secret'; +my $session6 = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'user1'] +); + +# Expire user1 +$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2020-01-01';"); + +note "Waiting 7 seconds..."; +sleep(7); + +# Session should still work since validation is disabled +($stdout, $ret) = $session6->query('SELECT 1 AS validation_disabled;'); +like($stdout, qr/1/, 'Test 6: session survives when validation is disabled'); + +eval { $session6->quit; }; + +# Re-enable for any subsequent tests +$node->safe_psql('postgres', "ALTER SYSTEM SET credential_validation_enabled = on;"); +$node->reload; + +############################################################################# +# Test 7: SET SESSION AUTHORIZATION does not change whose credentials are +# checked. Regression test for using GetAuthenticatedUserId() (the role +# that actually authenticated) rather than GetSessionUserId() (the mutable +# current session role) in the baseline role-validity check. +############################################################################# +note "=== Test 7: SET SESSION AUTHORIZATION does not change whose credentials are checked ==="; + +# authsuperN logs in over the wire, so its credentials are what actually got +# authenticated. sesstargetN is only ever reached via SET SESSION +# AUTHORIZATION, never over the wire, so it needs no pg_hba entry or +# password of its own. +$node->safe_psql('postgres', "CREATE USER authsuper1 LOGIN SUPERUSER PASSWORD 'secret7';"); +$node->safe_psql('postgres', "CREATE ROLE sesstarget1;"); +$node->safe_psql('postgres', "CREATE USER authsuper2 LOGIN SUPERUSER PASSWORD 'secret8';"); +$node->safe_psql('postgres', "CREATE ROLE sesstarget2;"); +reset_pg_hba($node, 'md5', 'authsuper1', 'authsuper2'); + +############################################################################# +# Test 7a: the *originally authenticated* role's expiry still terminates the +# session even after switching to a still-valid session role. If the +# baseline check regressed to GetSessionUserId(), this session would survive +# instead (sesstarget1 is never touched), so this test would catch that. +############################################################################# +note "--- Test 7a: original login role expiring still terminates session after SET SESSION AUTHORIZATION ---"; + +$ENV{PGPASSWORD} = 'secret7'; +my $session7a = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'authsuper1'] +); + +($stdout, $ret) = $session7a->query('SET SESSION AUTHORIZATION sesstarget1;'); +is($ret, 0, 'Test 7a: SET SESSION AUTHORIZATION succeeds for superuser'); + +($stdout, $ret) = $session7a->query('SELECT current_user;'); +like($stdout, qr/sesstarget1/, 'Test 7a: session is now running as sesstarget1'); + +# Expire the ORIGINAL login role (authsuper1), not the current session role. +$node->safe_psql('postgres', "ALTER USER authsuper1 VALID UNTIL '2020-01-01';"); + +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +eval { + ($stdout, $ret) = $session7a->query('SELECT 2 AS failure_expected;'); +}; + +$log_contents = slurp_file($node->logfile); +like( + $log_contents, + qr/FATAL:.*session credentials have expired/, + 'Test 7a: session terminated on original login role expiry, despite SET SESSION AUTHORIZATION to a still-valid role' +); + +eval { $session7a->quit; }; + +############################################################################# +# Test 7b: the *current session role's* own expiry is irrelevant -- only the +# originally authenticated role is checked. If the baseline check regressed +# to GetSessionUserId(), this session would be wrongly terminated instead of +# surviving, so this test would catch that. +############################################################################# +note "--- Test 7b: session role's own expiry is irrelevant after SET SESSION AUTHORIZATION ---"; + +$ENV{PGPASSWORD} = 'secret8'; +my $session7b = $node->background_psql( + 'postgres', + on_error_stop => 0, + extra_params => ['-U', 'authsuper2'] +); + +($stdout, $ret) = $session7b->query('SET SESSION AUTHORIZATION sesstarget2;'); +is($ret, 0, 'Test 7b: SET SESSION AUTHORIZATION succeeds for superuser'); + +($stdout, $ret) = $session7b->query('SELECT current_user;'); +like($stdout, qr/sesstarget2/, 'Test 7b: session is now running as sesstarget2'); + +# Expire the CURRENT session role (sesstarget2); authsuper2, the role that +# actually authenticated, is left untouched (still valid). +$node->safe_psql('postgres', "ALTER ROLE sesstarget2 VALID UNTIL '2020-01-01';"); + +note "Waiting 7 seconds for credential validation timeout to fire..."; +sleep(7); + +($stdout, $ret) = $session7b->query('SELECT 1 AS still_alive;'); +like($stdout, qr/1/, 'Test 7b: session survives when only the session role (not the login role) has expired'); +is($ret, 0, 'Test 7b: no errors -- validation checks the authenticated role, not the session role'); + +eval { $session7b->quit; }; + +# Clean up +$node->stop; +done_testing(); -- 2.52.0