</listitem>
</varlistentry>
+ <varlistentry id="guc-password-expiration-warning-threshold" xreflabel="password_expiration_warning_threshold">
+ <term><varname>password_expiration_warning_threshold</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>password_expiration_warning_threshold</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When this parameter is greater than zero, the server will emit a
+ <literal>WARNING</literal> upon successful password authentication if
+ less than this amount of time remains until the authenticated role's
+ password expires. Note that a role's password only expires if a date
+ was specified in a <literal>VALID UNTIL</literal> clause for
+ <command>CREATE ROLE</command> or <command>ALTER ROLE</command>. If
+ this value is specified without units, it is taken as seconds. The
+ default is 7 days. This parameter can only be set in the
+ <filename>postgresql.conf</filename> file or on the server command
+ line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-md5-password-warnings" xreflabel="md5_password_warnings">
<term><varname>md5_password_warnings</varname> (<type>boolean</type>)
<indexterm>
#include "common/scram-common.h"
#include "libpq/crypt.h"
#include "libpq/scram.h"
+#include "miscadmin.h"
#include "utils/builtins.h"
+#include "utils/memutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/* Threshold for password expiration warnings. */
+int password_expiration_warning_threshold = 604800;
+
/* Enables deprecation warnings for MD5 passwords. */
bool md5_password_warnings = true;
ReleaseSysCache(roleTup);
/*
- * Password OK, but check to be sure we are not past rolvaliduntil
+ * Password OK, but check to be sure we are not past rolvaliduntil or
+ * password_expiration_warning_threshold.
*/
- if (!isnull && vuntil < GetCurrentTimestamp())
+ if (!isnull)
{
- *logdetail = psprintf(_("User \"%s\" has an expired password."),
- role);
- return NULL;
+ TimestampTz now = GetCurrentTimestamp();
+ uint64 expire_time = TimestampDifferenceMicroseconds(now, vuntil);
+
+ /*
+ * If we're past rolvaliduntil, the connection attempt should fail, so
+ * update logdetail and return NULL.
+ */
+ if (vuntil < now)
+ {
+ *logdetail = psprintf(_("User \"%s\" has an expired password."),
+ role);
+ return NULL;
+ }
+
+ /*
+ * If we're past the warning threshold, the connection attempt should
+ * succeed, but we still want to emit a warning. To do so, we queue
+ * the warning message using StoreConnectionWarning() so that it will
+ * be emitted at the end of InitPostgres(), and we return normally.
+ */
+ if (expire_time / USECS_PER_SEC < password_expiration_warning_threshold)
+ {
+ MemoryContext oldcontext;
+ int days;
+ int hours;
+ int minutes;
+ char *warning;
+ char *detail;
+
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ days = expire_time / USECS_PER_DAY;
+ hours = (expire_time % USECS_PER_DAY) / USECS_PER_HOUR;
+ minutes = (expire_time % USECS_PER_HOUR) / USECS_PER_MINUTE;
+
+ warning = pstrdup(_("role password will expire soon"));
+
+ if (days > 0)
+ detail = psprintf(ngettext("The password for role \"%s\" will expire in %d day.",
+ "The password for role \"%s\" will expire in %d days.",
+ days),
+ role, days);
+ else if (hours > 0)
+ detail = psprintf(ngettext("The password for role \"%s\" will expire in %d hour.",
+ "The password for role \"%s\" will expire in %d hours.",
+ hours),
+ role, hours);
+ else if (minutes > 0)
+ detail = psprintf(ngettext("The password for role \"%s\" will expire in %d minute.",
+ "The password for role \"%s\" will expire in %d minutes.",
+ minutes),
+ role, minutes);
+ else
+ detail = psprintf(_("The password for role \"%s\" will expire in less than 1 minute."),
+ role);
+
+ StoreConnectionWarning(warning, detail);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
}
return shadow_pass;
#include "utils/syscache.h"
#include "utils/timeout.h"
+/* has this backend called EmitConnectionWarnings()? */
+static bool ConnectionWarningsEmitted;
+
+/* content of warnings to send via EmitConnectionWarnings() */
+static List *ConnectionWarningMessages;
+static List *ConnectionWarningDetails;
+
static HeapTuple GetDatabaseTuple(const char *dbname);
static HeapTuple GetDatabaseTupleByOid(Oid dboid);
static void PerformAuthentication(Port *port);
static bool ThereIsAtLeastOneRole(void);
static void process_startup_options(Port *port, bool am_superuser);
static void process_settings(Oid databaseid, Oid roleid);
+static void EmitConnectionWarnings(void);
/*** InitPostgres support ***/
/* close the transaction we started above */
CommitTransactionCommand();
+ /* send any WARNINGs we've accumulated during initialization */
+ EmitConnectionWarnings();
+
return;
}
/* close the transaction we started above */
if (!bootstrap)
CommitTransactionCommand();
+
+ /* send any WARNINGs we've accumulated during initialization */
+ EmitConnectionWarnings();
}
/*
return result;
}
+
+/*
+ * Stores a warning message to be sent later via EmitConnectionWarnings().
+ * Both msg and detail must be non-NULL.
+ *
+ * NB: Caller should ensure the strings are allocated in a long-lived context
+ * like TopMemoryContext.
+ */
+void
+StoreConnectionWarning(char *msg, char *detail)
+{
+ MemoryContext oldcontext;
+
+ Assert(msg);
+ Assert(detail);
+
+ if (ConnectionWarningsEmitted)
+ elog(ERROR, "StoreConnectionWarning() called after EmitConnectionWarnings()");
+
+ oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+ ConnectionWarningMessages = lappend(ConnectionWarningMessages, msg);
+ ConnectionWarningDetails = lappend(ConnectionWarningDetails, detail);
+
+ MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Sends the warning messages saved via StoreConnectionWarning() and frees the
+ * strings and lists.
+ *
+ * NB: This can only be called once per backend.
+ */
+static void
+EmitConnectionWarnings(void)
+{
+ ListCell *lc_msg;
+ ListCell *lc_detail;
+
+ if (ConnectionWarningsEmitted)
+ elog(ERROR, "EmitConnectionWarnings() called more than once");
+ else
+ ConnectionWarningsEmitted = true;
+
+ forboth(lc_msg, ConnectionWarningMessages,
+ lc_detail, ConnectionWarningDetails)
+ {
+ ereport(WARNING,
+ (errmsg("%s", (char *) lfirst(lc_msg)),
+ errdetail("%s", (char *) lfirst(lc_detail))));
+ }
+
+ list_free_deep(ConnectionWarningMessages);
+ list_free_deep(ConnectionWarningDetails);
+}
options => 'password_encryption_options',
},
+{ name => 'password_expiration_warning_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+ short_desc => 'Threshold for password expiration warnings.',
+ long_desc => '0 means not to emit these warnings.',
+ flags => 'GUC_UNIT_S',
+ variable => 'password_expiration_warning_threshold',
+ boot_val => '604800',
+ min => '0',
+ max => 'INT_MAX',
+},
+
{ name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
short_desc => 'Controls the planner\'s selection of custom or generic plan.',
long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better. This can be set to override the default behavior.',
#authentication_timeout = 1min # 1s-600s
#password_encryption = scram-sha-256 # scram-sha-256 or (deprecated) md5
#scram_iterations = 4096
-#md5_password_warnings = on # display md5 deprecation warnings?
+#password_expiration_warning_threshold = 7d # threshold for expiration warnings
+#md5_password_warnings = on # display md5 deprecation warnings?
#oauth_validator_libraries = '' # comma-separated list of trusted validator modules
# GSSAPI using Kerberos
*/
#define MAX_ENCRYPTED_PASSWORD_LEN (512)
+/* Threshold for password expiration warnings. */
+extern PGDLLIMPORT int password_expiration_warning_threshold;
+
/* Enables deprecation warnings for MD5 passwords. */
extern PGDLLIMPORT bool md5_password_warnings;
bits32 flags,
char *out_dbname);
extern void BaseInit(void);
+extern void StoreConnectionWarning(char *msg, char *detail);
/* in utils/init/miscinit.c */
extern PGDLLIMPORT bool IgnoreSystemIndexes;
$node->append_conf('postgresql.conf', "log_connections = on\n");
# Needed to allow connect_fails to inspect postmaster log:
$node->append_conf('postgresql.conf', "log_min_messages = debug2");
+$node->append_conf('postgresql.conf', "password_expiration_warning_threshold = '1100d'");
$node->start;
+# Set up roles for password_expiration_warning_threshold test
+my $current_year = 1900 + ${ [ localtime(time) ] }[5];
+my $expire_year = $current_year - 1;
+$node->safe_psql(
+ 'postgres',
+ "CREATE ROLE expired LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'");
+$expire_year = $current_year + 2;
+$node->safe_psql(
+ 'postgres',
+ "CREATE ROLE expiration_warnings LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'");
+$expire_year = $current_year + 5;
+$node->safe_psql(
+ 'postgres',
+ "CREATE ROLE no_warnings LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'");
+
# Test behavior of log_connections GUC
#
# There wasn't another test file where these tests obviously fit, and we don't
qr/authentication method requirement "!password,!md5,!scram-sha-256" failed: server requested SCRAM-SHA-256 authentication/
);
+# Test password_expiration_warning_threshold
+$node->connect_fails(
+ "user=expired dbname=postgres",
+ "connection fails due to expired password",
+ expected_stderr =>
+ qr/password authentication failed for user "expired"/
+);
+$node->connect_ok(
+ "user=expiration_warnings dbname=postgres",
+ "connection succeeds with password expiration warning",
+ expected_stderr =>
+ qr/role password will expire soon/
+);
+$node->connect_ok(
+ "user=no_warnings dbname=postgres",
+ "connection succeeds with no password expiration warning"
+);
+
# Test SYSTEM_USER <> NULL with parallel workers.
$node->safe_psql(
'postgres',