From c85188d01ca253d9d6262dbbbacf4c6480389021 Mon Sep 17 00:00:00 2001 From: Ajit Awekar Date: Tue, 16 Jun 2026 14:12:16 +0530 Subject: [PATCH 1/3] 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 session role must still exist and must not have passed its rolvaliduntil expiration. 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 | 75 +++++ src/backend/libpq/auth-validate.c | 227 +++++++++++++++ src/backend/libpq/meson.build | 2 + src/backend/tcop/postgres.c | 27 ++ 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 | 263 ++++++++++++++++++ 17 files changed, 768 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 fa566c9e553..e1a1eec2538 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..f371a36906a --- /dev/null +++ b/src/backend/libpq/auth-validate-methods.c @@ -0,0 +1,75 @@ +/*------------------------------------------------------------------------- + * + * 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 session 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. + * + * 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(GetSessionUserId())); + + 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..fd31be1ca99 --- /dev/null +++ b/src/backend/libpq/auth-validate.c @@ -0,0 +1,227 @@ +/*------------------------------------------------------------------------- + * + * 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 "access/xlog.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; + + /* 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 or recovery + * - non-client backends (autovacuum, background workers, etc.) + * - while authentication is still in progress + */ + if (proc_exit_inprogress || + RecoveryInProgress() || + !IsExternalConnectionBackend(MyBackendType) || + 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 dbef734a93f..ced544db2f7 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -45,6 +45,7 @@ #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/pqsignal.h" +#include "libpq/auth-validate.h" #include "mb/pg_wchar.h" #include "mb/stringinfo_mb.h" #include "miscadmin.h" @@ -1443,6 +1444,7 @@ exec_parse_message(const char *query_string, /* string to execute */ */ start_xact_command(); + /* * Switch to appropriate context for constructing parsetrees. * @@ -4681,6 +4683,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 { @@ -4733,6 +4740,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 */ @@ -4835,6 +4847,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 3d8c9bdebd5..c4f59b2e9a0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -34,6 +34,7 @@ #include "catalog/pg_db_role_setting.h" #include "catalog/pg_tablespace.h" #include "libpq/auth.h" +#include "libpq/auth-validate.h" #include "libpq/libpq-be.h" #include "mb/pg_wchar.h" #include "miscadmin.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); @@ -805,6 +807,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); } /* @@ -1268,6 +1272,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 @@ -1474,6 +1484,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 afaa058b046..2d6fe7be45a 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -570,6 +570,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 290ccbc543e..fa7509c558a 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -52,6 +52,7 @@ #include "common/scram-common.h" #include "jit/jit.h" #include "libpq/auth.h" +#include "libpq/auth-validate.h" #include "libpq/libpq.h" #include "libpq/oauth.h" #include "libpq/scram.h" diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index ac38cddaaf9..04697ada6aa 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -921,6 +921,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 7170a4bff98..19d7d89df42 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 0965b590b34..d4673a8a408 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..8bb7c4848bc --- /dev/null +++ b/src/test/authentication/t/008_continuous_validation.pl @@ -0,0 +1,263 @@ +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; + +# Clean up +$node->stop; +done_testing(); -- 2.52.0