mirror of
https://github.com/postgres/postgres.git
synced 2026-03-23 02:43:22 -04:00
Add password expiration warnings.
This commit adds a new parameter called password_expiration_warning_threshold that controls when the server begins emitting imminent-password-expiration warnings upon successful password authentication. By default, this parameter is set to 7 days, but this functionality can be disabled by setting it to 0. This patch also introduces a new "connection warning" infrastructure that can be reused elsewhere. For example, we may want to warn about the use of MD5 passwords for a couple of releases before removing MD5 password support. Author: Gilles Darold <gilles@darold.net> Co-authored-by: Nathan Bossart <nathandbossart@gmail.com> Reviewed-by: Japin Li <japinli@hotmail.com> Reviewed-by: songjinzhou <tsinghualucky912@foxmail.com> Reviewed-by: liu xiaohui <liuxh.zj.cn@gmail.com> Reviewed-by: Yuefei Shi <shiyuefei1004@gmail.com> Reviewed-by: Steven Niu <niushiji@gmail.com> Reviewed-by: Soumya S Murali <soumyamurali.work@gmail.com> Reviewed-by: Euler Taveira <euler@eulerto.com> Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com> Reviewed-by: Chao Li <li.evan.chao@gmail.com> Reviewed-by: Greg Sabino Mullane <htamfids@gmail.com> Reviewed-by: Peter Eisentraut <peter@eisentraut.org> Discussion: https://postgr.es/m/129bcfbf-47a6-e58a-190a-62fc21a17d03%40migops.com
This commit is contained in:
parent
a3fd53babb
commit
1d92e0c2cc
8 changed files with 209 additions and 6 deletions
|
|
@ -1157,6 +1157,28 @@ include_dir 'conf.d'
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -20,10 +20,15 @@
|
|||
#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;
|
||||
|
||||
|
|
@ -71,13 +76,71 @@ get_role_password(const char *role, const char **logdetail)
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@
|
|||
#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);
|
||||
|
|
@ -85,6 +92,7 @@ static void ClientCheckTimeoutHandler(void);
|
|||
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 ***/
|
||||
|
|
@ -987,6 +995,9 @@ InitPostgres(const char *in_dbname, Oid dboid,
|
|||
/* close the transaction we started above */
|
||||
CommitTransactionCommand();
|
||||
|
||||
/* send any WARNINGs we've accumulated during initialization */
|
||||
EmitConnectionWarnings();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1232,6 +1243,9 @@ InitPostgres(const char *in_dbname, Oid dboid,
|
|||
/* close the transaction we started above */
|
||||
if (!bootstrap)
|
||||
CommitTransactionCommand();
|
||||
|
||||
/* send any WARNINGs we've accumulated during initialization */
|
||||
EmitConnectionWarnings();
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -1446,3 +1460,58 @@ ThereIsAtLeastOneRole(void)
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2251,6 +2251,16 @@
|
|||
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.',
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@
|
|||
#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
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
*/
|
||||
#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;
|
||||
|
||||
|
|
|
|||
|
|
@ -507,6 +507,7 @@ extern void InitPostgres(const char *in_dbname, Oid dboid,
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -68,8 +68,24 @@ $node->init;
|
|||
$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
|
||||
|
|
@ -531,6 +547,24 @@ $node->connect_fails(
|
|||
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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue