Proposal: Save user's original authenticated identity for logging

Started by Jacob Championalmost 5 years ago80 messages
#1Jacob Champion
pchampion@vmware.com
1 attachment(s)

Hello all,

First, the context: recently I've been digging into the use of third-
party authentication systems with Postgres. One sticking point is the
need to have a Postgres role corresponding to the third-party user
identity, which becomes less manageable at scale. I've been trying to
come up with ways to make that less painful, and to start peeling off
smaller feature requests.

= Problem =

For auth methods that allow pg_ident mapping, there's a way around the
one-role-per-user problem, which is to have all users that match some
pattern map to a single role. For Kerberos, you might specify that all
user principals under @EXAMPLE.COM are allowed to connect as some
generic user role, and that everyone matching */admin@EXAMPLE.COM is
additionally allowed to connect as an admin role.

Unfortunately, once you've been assigned a role, Postgres either makes
the original identity difficult to retrieve, or forgets who you were
entirely:

- for GSS, the original principal is saved in the Port struct, and you
need to either pull it out of pg_stat_gssapi, or enable log_connections
and piece the log line together with later log entries;
- for LDAP, the bind DN is discarded entirely;
- for TLS client certs, the DN has to be pulled from pg_stat_ssl or the
sslinfo extension (and it's truncated to 64 characters, so good luck if
you have a particularly verbose PKI tree);
- for peer auth, the username of the peereid is discarded;
- etc.

= Proposal =

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

Attached is a proof of concept that implements this for a handful of
auth methods:

- ldap uses the final bind DN as its authenticated identity
- gss uses the user principal
- cert uses the client's Subject DN
- scram-sha-256 just uses the Postgres username

With this patch, the authenticated identity can be inserted into
log_line_prefix using the placeholder %Z.

= Implementation Notes =

- Client certificates can be combined with other authentication methods
using the clientcert option, but that doesn't provide an authenticated
identity in my proposal. *Only* the cert auth method populates the
authenticated identity from a client certificate. This keeps the patch
from having to deal with two simultaneous identity sources.

- The trust auth method has an authenticated identity of NULL, logged
as [unknown]. I kept this property even when clientcert=verify-full is
in use (which would otherwise be identical to the cert auth method), to
hammer home that 1) trust is not an authentication method and 2) the
clientcert option does not provide an authenticated identity. Whether
this is a useful property, or just overly pedantic, is probably
something that could be debated.

- The cert method's Subject DN string formatting needs the same
considerations that are currently under discussion in Andrew's DN patch
[1]: /messages/by-id/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net

- I'm not crazy about the testing method -- it leads to a lot of log
file proliferation in the tests -- but I wanted to make sure that we
had test coverage for the log lines themselves. The ability to
correctly audit user behavior depends on us logging the correct
identity after authentication, but not a moment before.

Would this be generally useful for those of you using pg_ident in
production? Have I missed something that already provides this
functionality?

Thanks,
--Jacob

[1]: /messages/by-id/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net
/messages/by-id/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net

Attachments:

WIP-log-authenticated-identity-from-multiple-auth-ba.patchtext/x-patch; name=WIP-log-authenticated-identity-from-multiple-auth-ba.patchDownload
From 3f6e87a744be339748fc707cd071896e81e0323c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Fri, 22 Jan 2021 14:03:14 -0800
Subject: [PATCH] WIP: log authenticated identity from multiple auth backends

This is stored into port->authn_id according to the auth method:

  ldap: the final bind DN
  gss: the user principal
  cert: the client's Subject DN
  scram-sha-256: the Postgres username

It can be logged with the %Z specifier in log_line_prefix.
---
 src/backend/libpq/auth.c              | 21 ++++++++++-
 src/backend/libpq/be-secure-openssl.c | 22 +++++++++++
 src/backend/utils/error/elog.c        | 18 +++++++++
 src/include/libpq/libpq-be.h          | 12 ++++++
 src/test/kerberos/t/001_auth.pl       | 15 +++++++-
 src/test/ldap/t/001_auth.pl           | 22 ++++++++++-
 src/test/ssl/t/001_ssltests.pl        | 43 ++++++++++++++++++++-
 src/test/ssl/t/002_scram.pl           | 54 ++++++++++++++++++++++++++-
 8 files changed, 200 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 545635f41a..2bff140d3c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1021,6 +1021,10 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
 		return STATUS_ERROR;
 	}
 
+	/* TODO: move this up the stack so that it's shared by other password
+	 * authentication methods, once tests are written. */
+	port->authn_id = port->user_name;
+
 	return STATUS_OK;
 }
 
@@ -1208,9 +1212,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
-	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	port->authn_id = port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+
 
 	/*
 	 * Split the username at the realm separator
@@ -2809,6 +2814,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2861,6 +2869,15 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity. Unfortunately
+		 * there is no canonical string representation; do the best we can.
+		 */
+		port->authn_id = be_tls_get_peer_full_subject_name(port);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e2ecc6e7a..1c7f74dc22 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -562,12 +562,34 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
 		port->peer_cert_valid = true;
 	}
 
 	return 0;
 }
 
+const char *
+be_tls_get_peer_full_subject_name(Port *port)
+{
+	if (!port->peer)
+		return NULL;
+
+	/*
+	 * TODO: decide on the format for the name, and perform sanity checks to
+	 * prohibit nasty edge cases (e.g. embedded NULLs) if the format doesn't
+	 * perform escaping itself.
+	 *
+	 * See also
+	 *     https://www.postgresql.org/message-id/daf119af-60a3-54d9-978e-8c97a602ca28%40dunslane.net
+	 *
+	 * TODO: allocate this string in an explicit context
+	 * TODO: reconcile this with be_tls_get_peer_subject_name, which silently
+	 *       truncates if your buffer isn't big enough
+	 */
+	return X509_NAME_oneline(X509_get_subject_name(port->peer), NULL, 0);
+}
+
 void
 be_tls_close(Port *port)
 {
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 80c2672461..f74f7571ac 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2717,6 +2717,24 @@ log_line_prefix(StringInfo buf, ErrorData *edata)
 				else
 					appendStringInfoString(buf, unpack_sql_state(edata->sqlerrcode));
 				break;
+			case 'Z':
+				if (MyProcPort)
+				{
+					const char *authn_id = MyProcPort->authn_id;
+
+					/* TODO: should we consider the empty ID as unknown too? */
+					if (authn_id == NULL)
+						authn_id = _("[unknown]");
+					if (padding != 0)
+						appendStringInfo(buf, "%*s", padding, authn_id);
+					else
+						appendStringInfoString(buf, authn_id);
+
+				}
+				else if (padding != 0)
+					appendStringInfoSpaces(buf,
+										   padding > 0 ? padding : -padding);
+				break;
 			default:
 				/* format error - ignore it */
 				break;
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 66a8673d93..69ea0d5dfb 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,17 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * the "left side" of a pg_ident usermap.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
@@ -270,6 +281,7 @@ extern bool be_tls_get_compression(Port *port);
 extern const char *be_tls_get_version(Port *port);
 extern const char *be_tls_get_cipher(Port *port);
 extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
+extern const char *be_tls_get_peer_full_subject_name(Port *port);
 extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
 
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..8685d19487 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 43;
 }
 else
 {
@@ -170,6 +170,7 @@ $node->append_conf(
 	'postgresql.conf', qq{
 listen_addresses = '$hostaddr'
 krb_server_keyfile = '$keytab'
+log_line_prefix = '%m [%p] %q%a (%u:%Z) '
 logging_collector = on
 log_connections = on
 # these ensure stability of test results:
@@ -268,10 +269,18 @@ $node->restart;
 
 test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
 
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'does not log unauthenticated principal',
+	"(test1:[unknown]) FATAL:  GSSAPI authentication failed");
+
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
 test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
 
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'logs authenticated principal even without authorized mapping',
+	"(test1:test1\@$realm) LOG:  no match in usermap");
+
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
 
@@ -304,6 +313,10 @@ test_access(
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
+test_access($node, 'test1', 'SELECT true', 0, 'gssencmode=require',
+	'logs authenticated principal for authorized connection',
+	"(test1:test1\@$realm) LOG:  connection authorized:");
+
 # Test that we can transport a reasonable amount of data.
 test_query(
 	$node,
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..34a1e9a10d 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -187,6 +191,22 @@ test_access($node, 'test1', 2,
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+# make sure LDAP DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/\(test1:uid=test1,dc=example,dc=net\).*SELECT 1/,
+	"authenticated DNs are logged");
+
+unlike(
+	$log_contents,
+	qr/\(test.:uid=test.,dc=example,dc=net\).*FATAL:.*LDAP authentication failed/,
+	"unauthenticated DNs are not logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index fd2727b568..58f371a306 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,7 +13,7 @@ use SSLServer;
 
 if ($ENV{with_openssl} eq 'yes')
 {
-	plan tests => 93;
+	plan tests => 96;
 }
 else
 {
@@ -74,6 +74,10 @@ $node->start;
 my $result = $node->safe_psql('postgres', "SHOW ssl_library");
 is($result, 'OpenSSL', 'ssl_library parameter');
 
+$node->append_conf(
+	'postgresql.conf',
+	"log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
+
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	'trust');
 
@@ -399,6 +403,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -406,6 +413,16 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+$node->start;
+
+like(
+	$log_contents,
+	qr/\(ssltestuser:\/CN=ssltestuser\).*SELECT \$\$connected/,
+	"authenticated DNs are logged");
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -499,6 +516,9 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -506,12 +526,24 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+	$log_contents,
+	qr/\(\[unknown\]:\[unknown\]\).*could not accept SSL connection: certificate verify failed/,
+	"unauthenticated DNs are not logged");
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -533,6 +565,15 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+unlike(
+	$log_contents,
+	qr/\(.*:\/CN=ssltestuser\)/,
+	"trust auth method does not set authenticated identity");
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index a088f71a1a..d8dd670c7a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -38,6 +38,10 @@ note "setting up data directory";
 my $node = get_new_node('primary');
 $node->init;
 
+$node->append_conf(
+	'postgresql.conf',
+	"log_line_prefix = '%m [%p] %q%a (%u:%Z) '\n");
+
 # PGHOST is enforced here to set up the node, subsequent connections
 # will use a dedicated connection string.
 $ENV{PGHOST} = $node->host;
@@ -48,14 +52,42 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+$log = $node->rotate_logfile();
+$node->start;
+
+unlike(
+	$log_contents,
+	qr/\(ssltestuser:ssltestuser\)/,
+	"SCRAM does not set authenticated identity with bad password");
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+	$log_contents,
+	qr/\(ssltestuser:ssltestuser\)/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +134,24 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+$node->start;
+
+like(
+	$log_contents,
+	qr/\(ssltestuser:ssltestuser\)/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#2Stephen Frost
sfrost@snowman.net
In reply to: Jacob Champion (#1)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Jacob Champion (pchampion@vmware.com) wrote:

First, the context: recently I've been digging into the use of third-
party authentication systems with Postgres. One sticking point is the
need to have a Postgres role corresponding to the third-party user
identity, which becomes less manageable at scale. I've been trying to
come up with ways to make that less painful, and to start peeling off
smaller feature requests.

Yeah, it'd be nice to improve things in this area.

= Problem =

For auth methods that allow pg_ident mapping, there's a way around the
one-role-per-user problem, which is to have all users that match some
pattern map to a single role. For Kerberos, you might specify that all
user principals under @EXAMPLE.COM are allowed to connect as some
generic user role, and that everyone matching */admin@EXAMPLE.COM is
additionally allowed to connect as an admin role.

Unfortunately, once you've been assigned a role, Postgres either makes
the original identity difficult to retrieve, or forgets who you were
entirely:

- for GSS, the original principal is saved in the Port struct, and you
need to either pull it out of pg_stat_gssapi, or enable log_connections
and piece the log line together with later log entries;

This has been improved on of late, but it's been done piece-meal.

- for LDAP, the bind DN is discarded entirely;

We don't support pg_ident.conf-style entries for LDAP, meaning that the
user provided has to match what we check, so I'm not sure what would be
improved with this change..? I'm also just generally not thrilled with
putting much effort into LDAP as it's a demonstrably insecure
authentication mechanism.

- for TLS client certs, the DN has to be pulled from pg_stat_ssl or the
sslinfo extension (and it's truncated to 64 characters, so good luck if
you have a particularly verbose PKI tree);

Yeah, it'd be nice to improve on this.

- for peer auth, the username of the peereid is discarded;

Would be good to improve this too.

= Proposal =

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

This seems like it would be good to include the CSV format log files
also.

Would this be generally useful for those of you using pg_ident in
production? Have I missed something that already provides this
functionality?

For some auth methods, eg: GSS, we've recently added information into
the authentication method which logs what the authenticated identity
was. The advantage with that approach is that it avoids bloating the
log by only logging that information once upon connection rather than
on every log line... I wonder if we should be focusing on a similar
approach for other pg_ident.conf use-cases instead of having it via
log_line_prefix, as the latter means we'd be logging the same value over
and over again on every log line.

Thanks,

Stephen

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#2)
Re: Proposal: Save user's original authenticated identity for logging

Stephen Frost <sfrost@snowman.net> writes:

* Jacob Champion (pchampion@vmware.com) wrote:

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

This seems like it would be good to include the CSV format log files
also.

What happens if ALTER USER RENAME is done while the session is still
alive?

More generally, exposing this in log_line_prefix seems like an awfully
narrow-minded view of what people will want it for. I'd personally
think pg_stat_activity a better place to look, for example.

on every log line... I wonder if we should be focusing on a similar
approach for other pg_ident.conf use-cases instead of having it via
log_line_prefix, as the latter means we'd be logging the same value over
and over again on every log line.

Yeah, this seems like about the most expensive way that we could possibly
choose to make the info available.

regards, tom lane

#4Jacob Champion
pchampion@vmware.com
In reply to: Stephen Frost (#2)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-01-29 at 17:01 -0500, Stephen Frost wrote:

- for LDAP, the bind DN is discarded entirely;

We don't support pg_ident.conf-style entries for LDAP, meaning that the
user provided has to match what we check, so I'm not sure what would be
improved with this change..?

For simple binds, this gives you almost nothing. For bind+search,
logging the actual bind DN is still important, in my opinion, since the
mechanism for determining it is more opaque (and may change over time).

But as Tom noted -- for both cases, if the role name changes, this
mechanism can still help you audit who the user _actually_ bound as,
not who you think they should have bound as based on their current role
name.

(There's also the fact that I think pg_ident mapping for LDAP would be
just as useful as it is for GSS or certs. That's for a different
conversation.)

I'm also just generally not thrilled with
putting much effort into LDAP as it's a demonstrably insecure
authentication mechanism.

Because Postgres has to proxy the password? Or is there something else?

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

This seems like it would be good to include the CSV format log files
also.

Agreed in principle... Is the CSV format configurable? Forcing it into
CSV logs by default seems like it'd be a hard sell, especially for
people not using pg_ident.

For some auth methods, eg: GSS, we've recently added information into
the authentication method which logs what the authenticated identity
was. The advantage with that approach is that it avoids bloating the
log by only logging that information once upon connection rather than
on every log line... I wonder if we should be focusing on a similar
approach for other pg_ident.conf use-cases instead of having it via
log_line_prefix, as the latter means we'd be logging the same value over
and over again on every log line.

As long as the identity can be easily logged and reviewed by DBAs, I'm
happy.

--Jacob

#5Jacob Champion
pchampion@vmware.com
In reply to: Tom Lane (#3)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-01-29 at 17:30 -0500, Tom Lane wrote:

What happens if ALTER USER RENAME is done while the session is still
alive?

IMO the authenticated identity should be write-once. Especially since
one of my goals is to have greater auditability into events as they've
actually happened. So ALTER USER RENAME should have no effect.

This also doesn't really affect third-party auth methods. If I'm bound
as pchampion@EXAMPLE.COM and a superuser changes my username to tlane,
you _definitely_ don't want to see my authenticated identity change to
tlane@EXAMPLE.COM. That's not who I am.

So the potential confusion would come into play with first-party authn.
From an audit perspective, I think it's worth it. I did authenticate as
pchampion, not tlane.

More generally, exposing this in log_line_prefix seems like an awfully
narrow-minded view of what people will want it for. I'd personally
think pg_stat_activity a better place to look, for example.
[...]
Yeah, this seems like about the most expensive way that we could possibly
choose to make the info available.

I'm happy as long as it's _somewhere_. :D It's relatively easy to
expose a single location through multiple avenues, but currently there
is no single location.

--Jacob

#6Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jacob Champion (#5)
Re: Proposal: Save user's original authenticated identity for logging

Jacob Champion <pchampion@vmware.com> writes:

On Fri, 2021-01-29 at 17:30 -0500, Tom Lane wrote:

What happens if ALTER USER RENAME is done while the session is still
alive?

IMO the authenticated identity should be write-once. Especially since
one of my goals is to have greater auditability into events as they've
actually happened. So ALTER USER RENAME should have no effect.

This also doesn't really affect third-party auth methods. If I'm bound
as pchampion@EXAMPLE.COM and a superuser changes my username to tlane,
you _definitely_ don't want to see my authenticated identity change to
tlane@EXAMPLE.COM. That's not who I am.

Ah. So basically, this comes into play when you consider that some
outside-the-database entity is your "real" authenticated identity.
That seems reasonable when using Kerberos or the like, though it's
not real meaningful for traditional password-type authentication.
I'd misunderstood your point before.

So, if we store this "real" identity, is there any security issue
involved in exposing it to other users (via pg_stat_activity or
whatever)?

I remain concerned about the cost and inconvenience of exposing
it via log_line_prefix, but at least that shouldn't be visible
to anyone who's not entitled to know who's logged in ...

regards, tom lane

#7Jacob Champion
pchampion@vmware.com
In reply to: Tom Lane (#6)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-01-29 at 18:40 -0500, Tom Lane wrote:

Ah. So basically, this comes into play when you consider that some
outside-the-database entity is your "real" authenticated identity.
That seems reasonable when using Kerberos or the like, though it's
not real meaningful for traditional password-type authentication.

Right.

So, if we store this "real" identity, is there any security issue
involved in exposing it to other users (via pg_stat_activity or
whatever)?

I think that could be a concern for some, yeah. Besides being able to
get information on other logged-in users, the ability to connect an
authenticated identity to a username also gives you some insight into
the pg_hba configuration.

--Jacob

#8Magnus Hagander
magnus@hagander.net
In reply to: Tom Lane (#6)
Re: Proposal: Save user's original authenticated identity for logging

On Sat, Jan 30, 2021 at 12:40 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Jacob Champion <pchampion@vmware.com> writes:

On Fri, 2021-01-29 at 17:30 -0500, Tom Lane wrote:

What happens if ALTER USER RENAME is done while the session is still
alive?

IMO the authenticated identity should be write-once. Especially since
one of my goals is to have greater auditability into events as they've
actually happened. So ALTER USER RENAME should have no effect.

This also doesn't really affect third-party auth methods. If I'm bound
as pchampion@EXAMPLE.COM and a superuser changes my username to tlane,
you _definitely_ don't want to see my authenticated identity change to
tlane@EXAMPLE.COM. That's not who I am.

Ah. So basically, this comes into play when you consider that some
outside-the-database entity is your "real" authenticated identity.
That seems reasonable when using Kerberos or the like, though it's
not real meaningful for traditional password-type authentication.

I think the usecases where it's relevant is a relatively close match
to the usecases where we support user mapping in pg_ident.conf. There
is a small exception in the ldap search+bind since it's a two-step
operation and the interesting part would be in the mid-step, but I'm
not sure there is any other case than those where it adds a lot of
value.

I'd misunderstood your point before.

So, if we store this "real" identity, is there any security issue
involved in exposing it to other users (via pg_stat_activity or
whatever)?

I'd say it might. It might for example reveal where in a hierarchical
authentication setup your "real identity" lives. I think it'd at least
have to be limited to superusers.

I remain concerned about the cost and inconvenience of exposing
it via log_line_prefix, but at least that shouldn't be visible
to anyone who's not entitled to know who's logged in ...

What if we logged it as part of log_connection=on, but only there and
only once? It could still be traced through the rest of that sessions
logging using the fields identifying the session, and we'd only end up
logging it once.

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#9Magnus Hagander
magnus@hagander.net
In reply to: Jacob Champion (#4)
Re: Proposal: Save user's original authenticated identity for logging

On Sat, Jan 30, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:

On Fri, 2021-01-29 at 17:01 -0500, Stephen Frost wrote:

- for LDAP, the bind DN is discarded entirely;

We don't support pg_ident.conf-style entries for LDAP, meaning that the
user provided has to match what we check, so I'm not sure what would be
improved with this change..?

For simple binds, this gives you almost nothing. For bind+search,
logging the actual bind DN is still important, in my opinion, since the
mechanism for determining it is more opaque (and may change over time).

Yeah, that's definitely a piece of information that can be hard to get at today.

(There's also the fact that I think pg_ident mapping for LDAP would be
just as useful as it is for GSS or certs. That's for a different
conversation.)

Specifically for search+bind, I would assume?

I'm also just generally not thrilled with
putting much effort into LDAP as it's a demonstrably insecure
authentication mechanism.

Because Postgres has to proxy the password? Or is there something else?

Stephen is on a bit of a crusade against ldap :) Mostly for good
reasons of course. A large amount of those who choose ldap also have a
kerberos system (because, say, active directory) and the pick ldap
only because they think it's good, not because it is...

But yes, I think the enforced cleartext password proxying is at the
core of the problem. LDAP also encourages the idea of centralized
password-reuse, which is not exactly a great thing for security.

That said, I don't think either of those are reasons not to improve on
LDAP. It can certainly be a reason for somebody not to want to spend
their own time on it, but there's no reason it should prevent
improvements.

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

This seems like it would be good to include the CSV format log files
also.

Agreed in principle... Is the CSV format configurable? Forcing it into
CSV logs by default seems like it'd be a hard sell, especially for
people not using pg_ident.

For CVS, all columns are always included, and that's a feature -- it
makes it predictable.

To make it optional it would have to be a configuration parameter that
turns the field into an empty one. but it should still be there.

For some auth methods, eg: GSS, we've recently added information into
the authentication method which logs what the authenticated identity
was. The advantage with that approach is that it avoids bloating the
log by only logging that information once upon connection rather than
on every log line... I wonder if we should be focusing on a similar
approach for other pg_ident.conf use-cases instead of having it via
log_line_prefix, as the latter means we'd be logging the same value over
and over again on every log line.

As long as the identity can be easily logged and reviewed by DBAs, I'm
happy.

Yeah, per my previous mail, I think this is a better way - make it
part of log_connections. But it would be good to find a way that we
can log it the same way for all of them -- rather than slightly
different ways depending on authentication method.

With that I think it would also be useful to have it available in the
system as well -- either as a column in pg_stat_activity or maybe just
as a function like pg_get_authenticated_identity() since it might be
something that's interesting to a smallish subset of users (but very
interesting to those).

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#10Greg Stark
stark@mit.edu
In reply to: Tom Lane (#6)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 29 Jan 2021 at 18:41, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Ah. So basically, this comes into play when you consider that some
outside-the-database entity is your "real" authenticated identity.
That seems reasonable when using Kerberos or the like, though it's
not real meaningful for traditional password-type authentication.
I'd misunderstood your point before.

I wonder if there isn't room to handle this the other way around. To
configure Postgres to not need a CREATE ROLE for every role but
delegate the user management to the external authentication service.

So Postgres would consider the actual role to be the one kerberos said
it was even if that role didn't exist in pg_role. Presumably you would
want to delegate to a corresponding authorization system as well so if
the role was absent from pg_role (or more likely fit some pattern)
Postgres would ignore pg_role and consult the authorization system
configured like AD or whatever people use with Kerberos these days.

--
greg

#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Magnus Hagander (#8)
Re: Proposal: Save user's original authenticated identity for logging

Magnus Hagander <magnus@hagander.net> writes:

On Sat, Jan 30, 2021 at 12:40 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

I remain concerned about the cost and inconvenience of exposing
it via log_line_prefix, but at least that shouldn't be visible
to anyone who's not entitled to know who's logged in ...

What if we logged it as part of log_connection=on, but only there and
only once? It could still be traced through the rest of that sessions
logging using the fields identifying the session, and we'd only end up
logging it once.

I'm certainly fine with including this info in the log_connection output.
Perhaps it'd also be good to have a superuser-only column in
pg_stat_activity, or some other restricted way to get the info from an
existing session. I doubt we really want a log_line_prefix option.

regards, tom lane

#12Tom Lane
tgl@sss.pgh.pa.us
In reply to: Greg Stark (#10)
Re: Proposal: Save user's original authenticated identity for logging

Greg Stark <stark@mit.edu> writes:

I wonder if there isn't room to handle this the other way around. To
configure Postgres to not need a CREATE ROLE for every role but
delegate the user management to the external authentication service.

So Postgres would consider the actual role to be the one kerberos said
it was even if that role didn't exist in pg_role. Presumably you would
want to delegate to a corresponding authorization system as well so if
the role was absent from pg_role (or more likely fit some pattern)
Postgres would ignore pg_role and consult the authorization system
configured like AD or whatever people use with Kerberos these days.

This doesn't sound particularly workable: how would you manage
inside-the-database permissions? Kerberos isn't going to know
what "view foo" is, let alone know whether you should be allowed
to read or write it. So ISTM there has to be a role to hold
those permissions. Certainly, you could allow multiple external
identities to share a role ... but that works today.

regards, tom lane

#13Stephen Frost
sfrost@snowman.net
In reply to: Magnus Hagander (#9)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Magnus Hagander (magnus@hagander.net) wrote:

On Sat, Jan 30, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:

I'm also just generally not thrilled with
putting much effort into LDAP as it's a demonstrably insecure
authentication mechanism.

Because Postgres has to proxy the password? Or is there something else?

Yes.

Stephen is on a bit of a crusade against ldap :) Mostly for good
reasons of course. A large amount of those who choose ldap also have a
kerberos system (because, say, active directory) and the pick ldap
only because they think it's good, not because it is...

This is certainly one area of frustration, but even if Kerberos isn't
available, it doesn't make it a good idea to use LDAP.

But yes, I think the enforced cleartext password proxying is at the
core of the problem. LDAP also encourages the idea of centralized
password-reuse, which is not exactly a great thing for security.

Right- passing around a user's password in the clear (or even through an
encrypted tunnel) has been strongly discouraged for a very long time,
for very good reason. LDAP does double-down on that by being a
centralized password, meaning that someone's entire identity (for all
the services that share that LDAP system, at least) are compromised if
any one system in the environment is.

Ideally, we'd have a 'PasswordAuthentication' option which would
disallow cleartext passwords, as has been discussed elsewhere, which
would make things like ldap and pam auth methods disallowed.

That said, I don't think either of those are reasons not to improve on
LDAP. It can certainly be a reason for somebody not to want to spend
their own time on it, but there's no reason it should prevent
improvements.

I realize that this isn't a popular opinion, but I'd much rather we
actively move in the direction of deprecating auth methods which use
cleartext passwords. The one auth method we have that works that way
and isn't terrible is radius, though it also isn't great since the pin
doesn't change and would be compromised, not to mention that it likely
depends on the specific system as to if an attacker might be able to use
the exact same code provided to log into other systems if done fast
enough.

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs. This field can then be exposed in
log_line_prefix. (It could additionally be exposed through a catalog
table or SQL function, if that were deemed useful.) This would let a
DBA more easily audit user activity when using more complicated
pg_ident setups.

This seems like it would be good to include the CSV format log files
also.

Agreed in principle... Is the CSV format configurable? Forcing it into
CSV logs by default seems like it'd be a hard sell, especially for
people not using pg_ident.

For CVS, all columns are always included, and that's a feature -- it
makes it predictable.

To make it optional it would have to be a configuration parameter that
turns the field into an empty one. but it should still be there.

Yeah, we've been around this before and, as I recall anyway, there was
actually a prior patch proposed to add this information to the CSV log.
There is the question about if it's valuable enough to repeat on every
line or not. These days, I think I lean in the same direction as the
majority on this thread that it's sufficient to log as part of the
connection authorized message.

For some auth methods, eg: GSS, we've recently added information into
the authentication method which logs what the authenticated identity
was. The advantage with that approach is that it avoids bloating the
log by only logging that information once upon connection rather than
on every log line... I wonder if we should be focusing on a similar
approach for other pg_ident.conf use-cases instead of having it via
log_line_prefix, as the latter means we'd be logging the same value over
and over again on every log line.

As long as the identity can be easily logged and reviewed by DBAs, I'm
happy.

Yeah, per my previous mail, I think this is a better way - make it
part of log_connections. But it would be good to find a way that we
can log it the same way for all of them -- rather than slightly
different ways depending on authentication method.

+1.

With that I think it would also be useful to have it available in the
system as well -- either as a column in pg_stat_activity or maybe just
as a function like pg_get_authenticated_identity() since it might be
something that's interesting to a smallish subset of users (but very
interesting to those).

We've been trending in the direction of having separate functions/views
for the different types of auth, as the specific information you'd want
varies (SSL has a different set than GSS, for example). Maybe it makes
sense to have the one string that's used to match against in pg_ident
included in pg_stat_activity also but I'm not completely sure- after
all, there's a reason we have the separate views. Also, if we do add
it, I would think we'd have it under the same check as the other
sensitive pg_stat_activity fields and not be superuser-only.

Thanks,

Stephen

#14Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#12)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Greg Stark <stark@mit.edu> writes:

I wonder if there isn't room to handle this the other way around. To
configure Postgres to not need a CREATE ROLE for every role but
delegate the user management to the external authentication service.

So Postgres would consider the actual role to be the one kerberos said
it was even if that role didn't exist in pg_role. Presumably you would
want to delegate to a corresponding authorization system as well so if
the role was absent from pg_role (or more likely fit some pattern)
Postgres would ignore pg_role and consult the authorization system
configured like AD or whatever people use with Kerberos these days.

This doesn't sound particularly workable: how would you manage
inside-the-database permissions? Kerberos isn't going to know
what "view foo" is, let alone know whether you should be allowed
to read or write it. So ISTM there has to be a role to hold
those permissions. Certainly, you could allow multiple external
identities to share a role ... but that works today.

Agreed- we would need something in the database to tie it to and I don't
see it making much sense to try to invent something else for that when
that's what roles are. What's been discussed before and would certainly
be nice, however, would be a way to have roles automatically created.
There's pg_ldap_sync for that today but it'd be nice to have something
built-in and which happens at connection/authentication time, or maybe a
background worker that connects to an ldap server and listens for
changes and creates appropriate roles when they're created. Considering
we've got the LDAP code already, that'd be a really nice capability.

Thanks,

Stephen

#15Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#14)
Re: Proposal: Save user's original authenticated identity for logging

Stephen Frost <sfrost@snowman.net> writes:

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

This doesn't sound particularly workable: how would you manage
inside-the-database permissions? Kerberos isn't going to know
what "view foo" is, let alone know whether you should be allowed
to read or write it. So ISTM there has to be a role to hold
those permissions. Certainly, you could allow multiple external
identities to share a role ... but that works today.

Agreed- we would need something in the database to tie it to and I don't
see it making much sense to try to invent something else for that when
that's what roles are. What's been discussed before and would certainly
be nice, however, would be a way to have roles automatically created.
There's pg_ldap_sync for that today but it'd be nice to have something
built-in and which happens at connection/authentication time, or maybe a
background worker that connects to an ldap server and listens for
changes and creates appropriate roles when they're created. Considering
we've got the LDAP code already, that'd be a really nice capability.

That's still got the same issue though: where does the role get any
permissions from?

I suppose you could say "allow auto-creation of new roles and make them
members of group X", where X holds the permissions that "everybody"
should have. But I'm not sure how much that buys compared to just
letting everyone log in as X.

regards, tom lane

#16Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#15)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Stephen Frost <sfrost@snowman.net> writes:

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

This doesn't sound particularly workable: how would you manage
inside-the-database permissions? Kerberos isn't going to know
what "view foo" is, let alone know whether you should be allowed
to read or write it. So ISTM there has to be a role to hold
those permissions. Certainly, you could allow multiple external
identities to share a role ... but that works today.

Agreed- we would need something in the database to tie it to and I don't
see it making much sense to try to invent something else for that when
that's what roles are. What's been discussed before and would certainly
be nice, however, would be a way to have roles automatically created.
There's pg_ldap_sync for that today but it'd be nice to have something
built-in and which happens at connection/authentication time, or maybe a
background worker that connects to an ldap server and listens for
changes and creates appropriate roles when they're created. Considering
we've got the LDAP code already, that'd be a really nice capability.

That's still got the same issue though: where does the role get any
permissions from?

I suppose you could say "allow auto-creation of new roles and make them
members of group X", where X holds the permissions that "everybody"
should have. But I'm not sure how much that buys compared to just
letting everyone log in as X.

Right, pg_ldap_sync already supports making new roles a member of
another role in PG such as a group role, we'd want to do something
similar. Also- once the role exists, then permissions could be assigned
directly as well, of course, which would be the advantage of a
background worker that's keeping the set of roles in sync, as the role
would be created at nearly the same time in both the authentication
system itself (eg: AD) and in PG. That kind of integration exists in
other products and would go a long way to making PG easier to use and
administer.

Thanks,

Stephen

#17Magnus Hagander
magnus@hagander.net
In reply to: Tom Lane (#15)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Feb 1, 2021 at 6:32 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Stephen Frost <sfrost@snowman.net> writes:

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

This doesn't sound particularly workable: how would you manage
inside-the-database permissions? Kerberos isn't going to know
what "view foo" is, let alone know whether you should be allowed
to read or write it. So ISTM there has to be a role to hold
those permissions. Certainly, you could allow multiple external
identities to share a role ... but that works today.

Agreed- we would need something in the database to tie it to and I don't
see it making much sense to try to invent something else for that when
that's what roles are. What's been discussed before and would certainly
be nice, however, would be a way to have roles automatically created.
There's pg_ldap_sync for that today but it'd be nice to have something
built-in and which happens at connection/authentication time, or maybe a
background worker that connects to an ldap server and listens for
changes and creates appropriate roles when they're created. Considering
we've got the LDAP code already, that'd be a really nice capability.

That's still got the same issue though: where does the role get any
permissions from?

I suppose you could say "allow auto-creation of new roles and make them
members of group X", where X holds the permissions that "everybody"
should have. But I'm not sure how much that buys compared to just
letting everyone log in as X.

What people would *really* want I think is "alow auto-creation of new
roles, and then look up which other roles they should be members of
using ldap" (or "using this script over here" for a more flexible
approach). Which is of course a whole different thing to do in the
process of authentication.

The main thing you'd gain by auto-creating users rather than just
letting them log in is the ability to know exactly which user did
something, and view who it really is through pg_stat_activity. Adding
the "original auth id" as a field or available method would provide
that information in the mapped user case -- making the difference even
smaller. It's really the auto-membership that's the killer feature of
that one, I think.

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#18Jacob Champion
pchampion@vmware.com
In reply to: Magnus Hagander (#9)
Re: Proposal: Save user's original authenticated identity for logging

On Sun, 2021-01-31 at 12:27 +0100, Magnus Hagander wrote:

(There's also the fact that I think pg_ident mapping for LDAP would be
just as useful as it is for GSS or certs. That's for a different
conversation.)

Specifically for search+bind, I would assume?

Even for the simple bind case, I think it'd be useful to be able to
perform a pg_ident mapping of

ldapmap /.* ldapuser

so that anyone who is able to authenticate against the LDAP server is
allowed to assume the ldapuser role. (For this to work, you'd need to
be able to specify your LDAP username as a connection option, similar
to how you can specify a client certificate, so that you could set
PGUSER=ldapuser.)

But again, that's orthogonal to the current discussion.

With that I think it would also be useful to have it available in the
system as well -- either as a column in pg_stat_activity or maybe just
as a function like pg_get_authenticated_identity() since it might be
something that's interesting to a smallish subset of users (but very
interesting to those).

Agreed, it would slot in nicely with the other per-backend stats functions.
--Jacob

#19Jacob Champion
pchampion@vmware.com
In reply to: Stephen Frost (#13)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-01 at 11:49 -0500, Stephen Frost wrote:

* Magnus Hagander (magnus@hagander.net) wrote:

But yes, I think the enforced cleartext password proxying is at the
core of the problem. LDAP also encourages the idea of centralized
password-reuse, which is not exactly a great thing for security.

Right- passing around a user's password in the clear (or even through an
encrypted tunnel) has been strongly discouraged for a very long time,
for very good reason. LDAP does double-down on that by being a
centralized password, meaning that someone's entire identity (for all
the services that share that LDAP system, at least) are compromised if
any one system in the environment is.

Sure. I don't disagree with anything you've said in that paragraph, but
as someone who's implementing solutions for other people who are
actually deploying, I don't have a lot of control over whether a
customer's IT department wants to use LDAP or not. And I'm not holding
my breath for LDAP servers to start implementing federated identity,
though that would be nice.

Also, if we do add
it, I would think we'd have it under the same check as the other
sensitive pg_stat_activity fields and not be superuser-only.

Just the standard HAS_PGSTAT_PERMISSIONS(), then?

To double-check -- since giving this ability to the pg_read_all_stats
role would expand its scope -- could that be dangerous for anyone?

--Jacob

#20Stephen Frost
sfrost@snowman.net
In reply to: Jacob Champion (#19)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Jacob Champion (pchampion@vmware.com) wrote:

On Mon, 2021-02-01 at 11:49 -0500, Stephen Frost wrote:

* Magnus Hagander (magnus@hagander.net) wrote:

But yes, I think the enforced cleartext password proxying is at the
core of the problem. LDAP also encourages the idea of centralized
password-reuse, which is not exactly a great thing for security.

Right- passing around a user's password in the clear (or even through an
encrypted tunnel) has been strongly discouraged for a very long time,
for very good reason. LDAP does double-down on that by being a
centralized password, meaning that someone's entire identity (for all
the services that share that LDAP system, at least) are compromised if
any one system in the environment is.

Sure. I don't disagree with anything you've said in that paragraph, but
as someone who's implementing solutions for other people who are
actually deploying, I don't have a lot of control over whether a
customer's IT department wants to use LDAP or not. And I'm not holding
my breath for LDAP servers to start implementing federated identity,
though that would be nice.

Not sure exactly what you're referring to here but AD already provides
Kerberos with cross-domain trusts (aka forests). The future is here..?
:)

Also, if we do add
it, I would think we'd have it under the same check as the other
sensitive pg_stat_activity fields and not be superuser-only.

Just the standard HAS_PGSTAT_PERMISSIONS(), then?

To double-check -- since giving this ability to the pg_read_all_stats
role would expand its scope -- could that be dangerous for anyone?

I don't agree that this really expands its scope- in fact, you'll see
that the GSSAPI and SSL user authentication information is already
allowed under HAS_PGSTAT_PERMISSIONS().

Thanks,

Stephen

#21Magnus Hagander
magnus@hagander.net
In reply to: Jacob Champion (#18)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Feb 1, 2021 at 10:36 PM Jacob Champion <pchampion@vmware.com> wrote:

On Sun, 2021-01-31 at 12:27 +0100, Magnus Hagander wrote:

(There's also the fact that I think pg_ident mapping for LDAP would be
just as useful as it is for GSS or certs. That's for a different
conversation.)

Specifically for search+bind, I would assume?

Even for the simple bind case, I think it'd be useful to be able to
perform a pg_ident mapping of

ldapmap /.* ldapuser

so that anyone who is able to authenticate against the LDAP server is
allowed to assume the ldapuser role. (For this to work, you'd need to
be able to specify your LDAP username as a connection option, similar
to how you can specify a client certificate, so that you could set
PGUSER=ldapuser.)

But again, that's orthogonal to the current discussion.

Right. I guess that's what I mean -- *just* adding support for user
mapping wouldn't be helpful. You'd have to change how the actual
authentication is done. The way that it's done now, mapping makes no
sense.

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#22Jacob Champion
pchampion@vmware.com
In reply to: Magnus Hagander (#17)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-01 at 18:44 +0100, Magnus Hagander wrote:

What people would *really* want I think is "alow auto-creation of new
roles, and then look up which other roles they should be members of
using ldap" (or "using this script over here" for a more flexible
approach). Which is of course a whole different thing to do in the
process of authentication.

Yep. I think there are at least three separate things:

1) third-party authentication ("tell me who this user is"), which I
think Postgres currently has a fairly good handle on;

2) third-party authorization ("tell me what roles this user can
assume"), which Postgres doesn't do, unless you have a script
automatically update pg_ident -- and even then you can't do it for
every authentication type; and

3) third-party role administration ("tell me what roles should exist in
the database, and what permissions they have"), which currently exists
in a limited handful of third-party tools.

Many users will want all three of these questions to be answered by the
same system, which is fine, but for more advanced use cases I think
it'd be really useful if you could answer them fully independently.

For really gigantic deployments, the overhead of hundreds of Postgres
instances randomly pinging a central server just to see if there have
been any new users can be a concern. Having a solid system for
authorization could potentially decrease the need for a role auto-
creation system, and reduce the number of moving parts. If you have a
small number of core roles (relative to the number of users), it might
not be as important to constantly keep role lists up to date, so long
as the central authority can tell you which of your existing roles a
user is authorized to become.

The main thing you'd gain by auto-creating users rather than just
letting them log in is the ability to know exactly which user did
something, and view who it really is through pg_stat_activity. Adding
the "original auth id" as a field or available method would provide
that information in the mapped user case -- making the difference even
smaller. It's really the auto-membership that's the killer feature of
that one, I think.

Agreed. As long as it's possible for multiple user identities to assume
the same role, storing the original authenticated identity is still
important, regardless of how you administer the roles themselves.

--Jacob

#23Jacob Champion
pchampion@vmware.com
In reply to: Stephen Frost (#20)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-01 at 17:01 -0500, Stephen Frost wrote:

* Jacob Champion (pchampion@vmware.com) wrote:

And I'm not holding
my breath for LDAP servers to start implementing federated identity,
though that would be nice.

Not sure exactly what you're referring to here but AD already provides
Kerberos with cross-domain trusts (aka forests). The future is here..?
:)

If the end user is actually using LDAP-on-top-of-AD, and comfortable
administering the Kerberos-related pieces of AD so that their *nix
servers and users can speak it instead, then sure. But I continue to
hear about customers who don't fit into that mold. :D Enough that I
have to keep an eye on the "pure" LDAP side of things, at least.

To double-check -- since giving this ability to the pg_read_all_stats
role would expand its scope -- could that be dangerous for anyone?

I don't agree that this really expands its scope- in fact, you'll see
that the GSSAPI and SSL user authentication information is already
allowed under HAS_PGSTAT_PERMISSIONS().

Ah, so they are. :) I think that's the way to go, then.

--Jacob

#24Stephen Frost
sfrost@snowman.net
In reply to: Jacob Champion (#23)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Jacob Champion (pchampion@vmware.com) wrote:

On Mon, 2021-02-01 at 17:01 -0500, Stephen Frost wrote:

* Jacob Champion (pchampion@vmware.com) wrote:

And I'm not holding
my breath for LDAP servers to start implementing federated identity,
though that would be nice.

Not sure exactly what you're referring to here but AD already provides
Kerberos with cross-domain trusts (aka forests). The future is here..?
:)

If the end user is actually using LDAP-on-top-of-AD, and comfortable
administering the Kerberos-related pieces of AD so that their *nix
servers and users can speak it instead, then sure. But I continue to
hear about customers who don't fit into that mold. :D Enough that I
have to keep an eye on the "pure" LDAP side of things, at least.

I suppose it's likely that I'll continue to run into people who are
horrified to learn that they've been using pass-the-password auth thanks
to using ldap.

To double-check -- since giving this ability to the pg_read_all_stats
role would expand its scope -- could that be dangerous for anyone?

I don't agree that this really expands its scope- in fact, you'll see
that the GSSAPI and SSL user authentication information is already
allowed under HAS_PGSTAT_PERMISSIONS().

Ah, so they are. :) I think that's the way to go, then.

Ok.. but what's 'go' mean here? We already have views and such for GSS
and SSL, is the idea to add another view for LDAP and add in columns
that are returned by pg_stat_get_activity() which are then pulled out by
that view? Or did you have something else in mind?

Thanks,

Stephen

#25Jacob Champion
pchampion@vmware.com
In reply to: Stephen Frost (#24)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-01 at 18:01 -0500, Stephen Frost wrote:

Ok.. but what's 'go' mean here? We already have views and such for GSS
and SSL, is the idea to add another view for LDAP and add in columns
that are returned by pg_stat_get_activity() which are then pulled out by
that view? Or did you have something else in mind?

Magnus suggested a function like pg_get_authenticated_identity(), which
is what I was thinking of when I said that. I'm not too interested in
an LDAP-specific view, and I don't think anyone so far has asked for
that.

My goal is to get this one single point of reference, for all of the
auth backends. The LDAP mapping conversation is separate.

--Jacob

#26Stephen Frost
sfrost@snowman.net
In reply to: Jacob Champion (#25)
Re: Proposal: Save user's original authenticated identity for logging

Greetings,

* Jacob Champion (pchampion@vmware.com) wrote:

On Mon, 2021-02-01 at 18:01 -0500, Stephen Frost wrote:

Ok.. but what's 'go' mean here? We already have views and such for GSS
and SSL, is the idea to add another view for LDAP and add in columns
that are returned by pg_stat_get_activity() which are then pulled out by
that view? Or did you have something else in mind?

Magnus suggested a function like pg_get_authenticated_identity(), which
is what I was thinking of when I said that. I'm not too interested in
an LDAP-specific view, and I don't think anyone so far has asked for
that.

My goal is to get this one single point of reference, for all of the
auth backends. The LDAP mapping conversation is separate.

Presumably this would be the DN for SSL then..? Not just the CN? How
would the issuer DN be included? And the serial?

Thanks,

Stephen

#27Jacob Champion
pchampion@vmware.com
In reply to: Stephen Frost (#26)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-01 at 18:40 -0500, Stephen Frost wrote:

* Jacob Champion (pchampion@vmware.com) wrote:

My goal is to get this one single point of reference, for all of the
auth backends. The LDAP mapping conversation is separate.

Presumably this would be the DN for SSL then..? Not just the CN?

Correct.

How would the issuer DN be included? And the serial?

In the current proposal, they're not. Seems like only the Subject
should be considered when determining the "identity of the user" --
knowing the issuer or the certificate fingerprint might be useful in
general, and perhaps they should be logged somewhere, but they're not
part of the user's identity.

If there were a feature that considered the issuer or serial number
when making role mappings, I think it'd be easier to make a case for
that. As of right now I don't think they should be incorporated into
this *particular* identifier.

--Jacob

#28Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#1)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, 2021-01-28 at 18:22 +0000, Jacob Champion wrote:

= Proposal =

I propose that every auth method should store the string it uses to
identify a user -- what I'll call an "authenticated identity" -- into
one central location in Port, after authentication succeeds but before
any pg_ident authorization occurs.

Thanks everyone for all of the feedback! Here's my summary of the
conversation so far:

- The idea of storing the user's original identity consistently across
all auth methods seemed to be positively received.

- Exposing this identity through log_line_prefix was not as well-
received, landing somewhere between "meh" and "no thanks". The main
concern was log bloat/expense.

- Exposing it through the CSV log got the same reception: if we expose
it through log_line_prefix, we should expose it through CSV, but no one
seemed particularly excited about either.

- The idea of logging this information once per session, as part of
log_connection, got a more positive response. That way the information
can still be obtained, but it doesn't clutter every log line.

- There was also some interest in exposing this through the statistics
collector, either as a superuser-only feature or via the
pg_read_all_stats role.

- There was some discussion around *which* string to choose as the
identifer for more complicated cases, such as TLS client certificates.

- Other improvements around third-party authorization and role
management were discussed, including the ability to auto-create
nonexistent roles, to sync role definitions as a first-party feature,
and to query an external system for role authorization.

(Let me know if there's something else I've missed.)

== My Plans ==

Given the feedback above, I'll continue to flesh out the PoC patch,
focusing on 1) storing the identity in a single place for all auth
methods and 2) exposing it consistently in the logs as part of
log_connections. I'll drop the log_line_prefix format specifier from
the patch and see what that does to the testing side of things. I also
plan to write a follow-up patch to add the authenticated identity to
the statistics collector, with pg_get_authenticated_identity() to
retrieve it.

I'm excited to see where the third-party authz and role management
conversations go, but I won't focus on those for my initial patchset. I
think this patch has use even if those ideas are implemented too.

--Jacob

#29Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#28)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-02-02 at 22:22 +0000, Jacob Champion wrote:

Given the feedback above, I'll continue to flesh out the PoC patch,
focusing on 1) storing the identity in a single place for all auth
methods and 2) exposing it consistently in the logs as part of
log_connections.

Attached is a v1 patchset. Note that I haven't compiled or tested on
Windows and BSD yet, so the SSPI and BSD auth changes are eyeballed for
now.

The first two patches are preparatory, pulled from other threads on the
mailing list: 0001 comes from my Kerberos test fix thread [1]/messages/by-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel@vmware.com, and 0002
is extracted from Andrew Dunstan's patch [2]/messages/by-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771@dunslane.net to store the subject DN
from a client cert. 0003 has the actual implementation, which now fills
in port->authn_id for all auth methods.

Now that we're using log_connections instead of log_line_prefix,
there's more helpful information we can put into the log when
authentication succeeds. For now, I include the identity of the user,
the auth method in use, and the pg_hba.conf file and line number. E.g.

LOG: connection received: host=[local]
LOG: connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
LOG: connection authorized: user=admin database=postgres application_name=psql

If the overall direction seems good, then I have two questions:

- Since the authenticated identity is more or less an opaque string
that may come from a third party, should I be escaping it in some way
before it goes into the logs? Or is it generally accepted that log
files can contain arbitrary blobs in unspecified encodings?

- For the SSPI auth method, I pick the format of the identity string
based on the compatibility mode: "DOMAIN\user" when using compat_realm,
and "user@DOMAIN" otherwise. For Windows DBAs, is this a helpful way to
visualize the identity, or should I just stick to one format?

I also
plan to write a follow-up patch to add the authenticated identity to
the statistics collector, with pg_get_authenticated_identity() to
retrieve it.

This part turned out to be more work than I'd thought! Now I understand
why pg_stat_ssl truncates several fields to NAMEDATALEN.

Has there been any prior discussion on lifting that restriction for the
statistics collector as a whole, before I go down my own path? I can't
imagine taking up another 64 bytes per connection for a field that
won't be useful for the most common use cases -- and yet it still won't
be long enough for other users...

Thanks,
--Jacob

[1]: /messages/by-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel@vmware.com
[2]: /messages/by-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771@dunslane.net

Attachments:

v1-0001-prep-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v1-0001-prep-test-kerberos-only-search-forward-in-logs.patchDownload
From a56ef4e45b4ad0b4be248a6585d07bf73fe79512 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v1 1/3] prep: test/kerberos: only search forward in logs

See

    https://www.postgresql.org/message-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel%40vmware.com
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v1-0002-prep-add-port-peer_dn.patchtext/x-patch; name=v1-0002-prep-add-port-peer_dn.patchDownload
From c42ee0d1dda878993b476d98a8f2c1f0dda307cc Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v1 2/3] prep: add port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e2ecc6e7a..83c0eb5006 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -523,22 +523,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -562,6 +565,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -590,6 +623,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 66a8673d93..c0988d2404 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v1-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v1-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From 5548a1e074805cedafe15098233f15e5a2913ae8 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v1 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: TODO

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.

TODOs:
- Test SSPI on Windows and decide on the identity format
- Test BSD auth
- Escape the identity string in the logs?
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 118 +++++++++++++++++++++-
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 ++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 392 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5ef1c7ad3c..e59c105537 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6626,7 +6626,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 545635f41a..c86919f136 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,49 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	/* TODO: should the identity strings be escaped before being logged? */
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -766,6 +812,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -825,6 +874,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1208,9 +1261,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1320,6 +1374,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1562,6 +1617,27 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 *
+	 * TODO: will DBAs find the DOMAIN\user construct useful in compat_realm
+	 * mode, or should we use user@DOMAIN for all cases?
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1949,8 +2025,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -2006,8 +2085,12 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
 	peer_user = pstrdup(pw->pw_name);
+	set_authn_id(port, peer_user);
 
 	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
 
@@ -2268,6 +2351,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2303,6 +2389,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2809,6 +2896,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2861,6 +2951,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -3023,6 +3133,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 20bf1461ce..e2c79dd769 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index c0988d2404..c2aae8b080 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 7928de4e7c..8e1108d0d8 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 93;
+	plan tests => 97;
 }
 
 #### Some configuration
@@ -399,6 +399,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -406,6 +409,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -491,6 +505,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -499,6 +516,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -506,12 +534,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -533,6 +574,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#30Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#29)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-02-08 at 23:35 +0000, Jacob Champion wrote:

Note that I haven't compiled or tested on
Windows and BSD yet, so the SSPI and BSD auth changes are eyeballed for
now.

I've now tested on both.

- For the SSPI auth method, I pick the format of the identity string
based on the compatibility mode: "DOMAIN\user" when using compat_realm,
and "user@DOMAIN" otherwise. For Windows DBAs, is this a helpful way to
visualize the identity, or should I just stick to one format?

After testing on Windows, I think switching formats based on
compat_realm is a good approach. For users not on a domain, the
MACHINE\user format is probably more familiar than user@MACHINE.
Inversely, users on a domain probably want to see the modern
user@DOMAIN instead.

v2 just updates the patchset to remove the Windows TODO and fill in the
patch notes; no functional changes. The question about escaping log
contents remains.

--Jacob

Attachments:

v2-0001-prep-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v2-0001-prep-test-kerberos-only-search-forward-in-logs.patchDownload
From 5dcbe2b90781ec833a1886cc800eed18b0352dcf Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v2 1/3] prep: test/kerberos: only search forward in logs

See

    https://www.postgresql.org/message-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel%40vmware.com
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v2-0002-prep-add-port-peer_dn.patchtext/x-patch; name=v2-0002-prep-add-port-peer_dn.patchDownload
From 99d36c0d486e42f5f8a7fadf884efa00c9365333 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v2 2/3] prep: add port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e2ecc6e7a..83c0eb5006 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -523,22 +523,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -562,6 +565,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -590,6 +623,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..c206a859fe 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v2-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v2-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From d9efef8edbf69f4f032a8511b7d5e414772acfdd Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v2 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.

TODOs:
- Escape the identity string in the logs?
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 115 +++++++++++++++++++++-
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 +++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 +++++-
 src/test/ssl/t/001_ssltests.pl            |  55 ++++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 389 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5ef1c7ad3c..e59c105537 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6626,7 +6626,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 545635f41a..2c148b3379 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,49 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	/* TODO: should the identity strings be escaped before being logged? */
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -766,6 +812,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -825,6 +874,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1208,9 +1261,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1320,6 +1374,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1562,6 +1617,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1949,8 +2022,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -2006,8 +2082,12 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
 	peer_user = pstrdup(pw->pw_name);
+	set_authn_id(port, peer_user);
 
 	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
 
@@ -2268,6 +2348,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2303,6 +2386,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2809,6 +2893,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2861,6 +2948,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -3023,6 +3130,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index c206a859fe..0d5eb1931f 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 7928de4e7c..8e1108d0d8 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 93;
+	plan tests => 97;
 }
 
 #### Some configuration
@@ -399,6 +399,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -406,6 +409,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -491,6 +505,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -499,6 +516,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -506,12 +534,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -533,6 +574,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#31Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#30)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, 2021-02-11 at 20:32 +0000, Jacob Champion wrote:

v2 just updates the patchset to remove the Windows TODO and fill in the
patch notes; no functional changes. The question about escaping log
contents remains.

v3 rebases onto latest master, for SSL test conflicts.

Note:
- Since the 0001 patch from [1]/messages/by-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel@vmware.com is necessary for the new Kerberos tests
in 0003, I won't make a separate commitfest entry for it.
- 0002 would be subsumed by [2]/messages/by-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771@dunslane.net if it's committed.

--Jacob

[1]: /messages/by-id/fe7a46f8d46ebb074ba1572d4b5e4af72dc95420.camel@vmware.com
[2]: /messages/by-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771@dunslane.net

Attachments:

v3-0001-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v3-0001-test-kerberos-only-search-forward-in-logs.patchDownload
From ed225e1d1671dcd4da94a2244171a206b88563cc Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v3 1/3] test/kerberos: only search forward in logs

The log content tests searched through the entire log file, from the
beginning, for every match. If two tests shared the same expected log
content, it was possible for the second test to get a false positive by
matching against the first test's output. (This could be seen by
modifying one of the expected-failure tests to expect the same output as
a previous happy-path test.)

Store the offset of the previous match, and search forward from there
during the next match, resetting the offset every time the log file
changes. This could still result in false positives if one test spits
out two or more matching log lines, but it should be an improvement over
what's there now.
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v3-0002-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v3-0002-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From def87ea906d37764d7d6a6e0c6d473ffacf2c801 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v3 2/3] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 4c4f025eb1..d0184a2ce2 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -543,22 +543,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -582,6 +585,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -610,6 +643,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..c206a859fe 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v3-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v3-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From 3b34845a28decfdba7b6c8ebddb8deb3dd420d03 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v3 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.

TODOs:
- Escape the identity string in the logs?
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 115 +++++++++++++++++++++-
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 +++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 +++++-
 src/test/ssl/t/001_ssltests.pl            |  55 ++++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 389 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5718fc136..f1ea071111 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6661,7 +6661,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index baa0712c0f..3176d3859f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,49 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	/* TODO: should the identity strings be escaped before being logged? */
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -766,6 +812,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -825,6 +874,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1208,9 +1261,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1320,6 +1374,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1562,6 +1617,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1949,8 +2022,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -2006,8 +2082,12 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
 	peer_user = pstrdup(pw->pw_name);
+	set_authn_id(port, peer_user);
 
 	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
 
@@ -2268,6 +2348,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2303,6 +2386,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2809,6 +2893,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2861,6 +2948,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -3023,6 +3130,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index c206a859fe..0d5eb1931f 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 864f6e209f..7ee6a63698 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#32Magnus Hagander
magnus@hagander.net
In reply to: Jacob Champion (#31)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Feb 26, 2021 at 8:45 PM Jacob Champion <pchampion@vmware.com> wrote:

On Thu, 2021-02-11 at 20:32 +0000, Jacob Champion wrote:

v2 just updates the patchset to remove the Windows TODO and fill in the
patch notes; no functional changes. The question about escaping log
contents remains.

v3 rebases onto latest master, for SSL test conflicts.

Note:
- Since the 0001 patch from [1] is necessary for the new Kerberos tests
in 0003, I won't make a separate commitfest entry for it.
- 0002 would be subsumed by [2] if it's committed.

It looks like patch 0001 has some leftover debuggnig code at the end?
Or did you intend for that to be included permanently?

As for log escaping, we report port->user_name already unescaped --
surely this shouldn't be a worse case than that?

I wonder if it wouldn't be better to keep the log line on the existing
"connection authorized" line, just as a separate field. I'm kind of
split on it though, because I guess it might make that line very long.
But it's also a lot more convenient to parse it on a single line than
across multiple lines potentially overlapping with other sessions.

With this we store the same value as the authn and as
port->gss->princ, and AFAICT it's only used once. Seems we could just
use the new field for the gssapi usage as well? Especially since that
usage only seems to be there in order to do the gssapi specific
logging of, well, the same thing.

Same goes for peer_user? In fact, if we're storing it in the Port, why
are we even passing it as a separate parameter to check_usermap --
shouldn't that one always use this same value? ISTM that it could be
quite confusing if the logged value is different from whatever we
apply to the user mapping?

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#33Jacob Champion
pchampion@vmware.com
In reply to: Magnus Hagander (#32)
Re: Proposal: Save user's original authenticated identity for logging

On Sat, 2021-03-06 at 18:33 +0100, Magnus Hagander wrote:

It looks like patch 0001 has some leftover debuggnig code at the end?
Or did you intend for that to be included permanently?

I'd intended to keep it -- it works hand-in-hand with the existing
"current_logfiles" log line on 219 and might keep someone from tearing
their hair out. But I can certainly remove it, if it's cluttering up
the logs too much.

As for log escaping, we report port->user_name already unescaped --
surely this shouldn't be a worse case than that?

Ah, that's a fair point. I'll remove the TODO.

I wonder if it wouldn't be better to keep the log line on the existing
"connection authorized" line, just as a separate field. I'm kind of
split on it though, because I guess it might make that line very long.
But it's also a lot more convenient to parse it on a single line than
across multiple lines potentially overlapping with other sessions.

Authentication can succeed even if authorization fails, and it's useful
to see that in the logs. In most cases that looks like a failed user
mapping, but there are other corner cases where we fail the connection
after a successful authentication, such as when using krb_realm.
Currently you get little to no feedback when that happens, but with a
separate log line, it's a lot easier to piece together what's happened.

(In general, I feel pretty strongly that Postgres combines/conflates
authentication and authorization in too many places.)

With this we store the same value as the authn and as
port->gss->princ, and AFAICT it's only used once. Seems we could just
use the new field for the gssapi usage as well? Especially since that
usage only seems to be there in order to do the gssapi specific
logging of, well, the same thing.

Same goes for peer_user? In fact, if we're storing it in the Port, why
are we even passing it as a separate parameter to check_usermap --
shouldn't that one always use this same value? ISTM that it could be
quite confusing if the logged value is different from whatever we
apply to the user mapping?

Seems reasonable; I'll consolidate them.

--Jacob

#34Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#33)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-03-08 at 22:16 +0000, Jacob Champion wrote:

On Sat, 2021-03-06 at 18:33 +0100, Magnus Hagander wrote:

With this we store the same value as the authn and as
port->gss->princ, and AFAICT it's only used once. Seems we could just
use the new field for the gssapi usage as well? Especially since that
usage only seems to be there in order to do the gssapi specific
logging of, well, the same thing.

[...]

Seems reasonable; I'll consolidate them.

A slight hitch in the plan, for the GSS side... port->gss->princ is
exposed by pg_stat_gssapi. I can switch this to use port->authn_id
easily enough.

But it seems like the existence of a user principal for the connection
is independent of whether or not you're using that principal as your
identity. For example, you might connect via a "hostgssenc ... trust"
line in the HBA. (This would be analogous to presenting a user
certificate over TLS but not using it to authenticate to the database.)
I'd argue that the principal should be available through the stats view
in this case as well, just like you can see a client DN in pg_stat_ssl
even if you're using trust auth.

The server doesn't currently support that -- gss->princ is only
populated in the gss auth case, as far as I can tell -- but if I remove
gss->princ entirely, then it'll be that much more work for someone who
wants to expose that info later. I think it should remain independent.

Thoughts?

--Jacob

#35Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#34)
Re: Proposal: Save user's original authenticated identity for logging

On Sat, 2021-03-06 at 18:33 +0100, Magnus Hagander wrote:

In fact, if we're storing it in the Port, why
are we even passing it as a separate parameter to check_usermap --
shouldn't that one always use this same value?

Ah, and now I remember why I didn't consolidate this to begin with.
Several auth methods perform some sort of translation before checking
the usermap: cert pulls the CN out of the Subject DN, SSPI and GSS can
optionally strip the realm, etc.

ISTM that it could be
quite confusing if the logged value is different from whatever we
apply to the user mapping?

Maybe. But it's an accurate reflection of what's actually happening,
and that's the goal of the patch: show enough information to be able to
audit who's logging in. The certificates

/OU=ACME Ltd./C=US/CN=pchampion

and

/OU=Postgres/C=GR/CN=pchampion

are different identities, but Postgres will silently authorize them to
log in as the same user. In my opinion, hiding that information makes
things more confusing in the long term, not less.

--Jacob

#36Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#33)
4 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-03-08 at 22:16 +0000, Jacob Champion wrote:

On Sat, 2021-03-06 at 18:33 +0100, Magnus Hagander wrote:

As for log escaping, we report port->user_name already unescaped --
surely this shouldn't be a worse case than that?

Ah, that's a fair point. I'll remove the TODO.

v4 removes the TODO and the extra allocation for peer_user. I'll hold
off on the other two suggestions pending that conversation.

--Jacob

Attachments:

since-v3.diff.txttext/plain; name=since-v3.diff.txtDownload
commit c323ba88d5a9823765c68cd4c2c169493e4c269a
Author: Jacob Champion <pchampion@vmware.com>
Date:   Tue Mar 9 09:53:03 2021 -0800

    fixup! Log authenticated identity from all auth backends

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 65d10a5ad2..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -354,8 +354,6 @@ set_authn_id(Port *port, const char *id)
 {
 	Assert(id);
 
-	/* TODO: should the identity strings be escaped before being logged? */
-
 	if (port->authn_id)
 	{
 		/*
@@ -2002,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -2038,12 +2035,9 @@ auth_peer(hbaPort *port)
 	 * Make a copy of static getpw*() result area. This is our authenticated
 	 * identity.
 	 */
-	peer_user = pstrdup(pw->pw_name);
-	set_authn_id(port, peer_user);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
v4-0001-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v4-0001-test-kerberos-only-search-forward-in-logs.patchDownload
From 22518e1ca0be717ce001f0c79214924fc62eb75c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v4 1/3] test/kerberos: only search forward in logs

The log content tests searched through the entire log file, from the
beginning, for every match. If two tests shared the same expected log
content, it was possible for the second test to get a false positive by
matching against the first test's output. (This could be seen by
modifying one of the expected-failure tests to expect the same output as
a previous happy-path test.)

Store the offset of the previous match, and search forward from there
during the next match, resetting the offset every time the log file
changes. This could still result in false positives if one test spits
out two or more matching log lines, but it should be an improvement over
what's there now.
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v4-0002-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v4-0002-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From f3e5040fd29c0143b7d9991d90c94d4151f5622a Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v4 2/3] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 4c4f025eb1..d0184a2ce2 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -543,22 +543,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -582,6 +585,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -610,6 +643,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..c206a859fe 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v4-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v4-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From fc5c02d145a12f7a852a37dcb3d5dd767382072b Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v4 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 ++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 388 insertions(+), 39 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 967de73596..94bc870173 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6672,7 +6672,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index c206a859fe..0d5eb1931f 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 864f6e209f..7ee6a63698 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#37Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#36)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-03-09 at 18:03 +0000, Jacob Champion wrote:

v4 removes the TODO and the extra allocation for peer_user. I'll hold
off on the other two suggestions pending that conversation.

And v5 is rebased over this morning's SSL test changes.

--Jacob

Attachments:

v5-0001-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v5-0001-test-kerberos-only-search-forward-in-logs.patchDownload
From 1159ef7f8fb846649c1c36bb1ecd17bd4b687668 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v5 1/3] test/kerberos: only search forward in logs

The log content tests searched through the entire log file, from the
beginning, for every match. If two tests shared the same expected log
content, it was possible for the second test to get a false positive by
matching against the first test's output. (This could be seen by
modifying one of the expected-failure tests to expect the same output as
a previous happy-path test.)

Store the offset of the previous match, and search forward from there
during the next match, resetting the offset every time the log file
changes. This could still result in false positives if one test spits
out two or more matching log lines, but it should be an improvement over
what's there now.
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v5-0002-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v5-0002-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From 5ac20ba6cac339bc4ffc0d765c67bfbf6583425c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v5 2/3] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 8c37381add..a7b09534a8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -546,22 +546,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -585,6 +588,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -613,6 +646,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v5-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v5-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From 11363e6b135b49a628cfd84a8f921e3b8e0b07c4 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v5 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 ++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 388 insertions(+), 39 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 529876895b..2cffccba00 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6672,7 +6672,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index ee97f6f069..84f2206611 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 101;
+	plan tests => 105;
 }
 
 #### Some configuration
@@ -424,6 +424,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -431,6 +434,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -516,6 +530,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -524,6 +541,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -531,12 +559,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -558,6 +599,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#38Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#37)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-03-09 at 19:10 +0000, Jacob Champion wrote:

And v5 is rebased over this morning's SSL test changes.

Rebased again after the SSL test revert (this is the same as v4).

--Jacob

Attachments:

v6-0001-test-kerberos-only-search-forward-in-logs.patchtext/x-patch; name=v6-0001-test-kerberos-only-search-forward-in-logs.patchDownload
From 470bf11f4b8feb6c22dc72626f6f3fcb7971ac26 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v6 1/3] test/kerberos: only search forward in logs

The log content tests searched through the entire log file, from the
beginning, for every match. If two tests shared the same expected log
content, it was possible for the second test to get a false positive by
matching against the first test's output. (This could be seen by
modifying one of the expected-failure tests to expect the same output as
a previous happy-path test.)

Store the offset of the previous match, and search forward from there
during the next match, resetting the offset every time the log file
changes. This could still result in false positives if one test spits
out two or more matching log lines, but it should be an improvement over
what's there now.
---
 src/test/kerberos/t/001_auth.pl | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..c721d24dbd 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -182,6 +182,9 @@ $node->safe_psql('postgres', 'CREATE USER test1;');
 
 note "running tests";
 
+my $current_log_name = '';
+my $current_log_position;
+
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
@@ -221,18 +224,37 @@ sub test_access
 		$lfname =~ s/^stderr //;
 		chomp $lfname;
 
+		if ($lfname ne $current_log_name)
+		{
+			$current_log_name = $lfname;
+
+			# Search from the beginning of this new file.
+			$current_log_position = 0;
+			note "current_log_position = $current_log_position";
+		}
+
 		# might need to retry if logging collector process is slow...
 		my $max_attempts = 180 * 10;
 		my $first_logfile;
 		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 		{
 			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
 			usleep(100_000);
 		}
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		note "current_log_position = $current_log_position";
 	}
 
 	return;
-- 
2.25.1

v6-0002-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v6-0002-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From 7a0aded279d3d1f2e0ea5da4d69af20dd2628143 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v6 2/3] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 8c37381add..a7b09534a8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -546,22 +546,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -585,6 +588,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -613,6 +646,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v6-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v6-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From 158fb1d39ea31ffd0553feddd108ffb8af7f1c32 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v6 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  73 ++++++++-----
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 388 insertions(+), 39 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a218d78bef..4019b012e7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6672,7 +6672,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index c721d24dbd..c2e6f33701 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 48;
 }
 else
 {
@@ -188,7 +188,7 @@ my $current_log_position;
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -212,8 +212,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
 		note "current_logfiles = $current_logfiles";
@@ -233,28 +233,31 @@ sub test_access
 			note "current_log_position = $current_log_position";
 		}
 
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		while (my $expect_log_msg = shift @expect_log_msgs)
 		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-
-			# Don't include previously matched text in the search.
-			$first_logfile = substr $first_logfile, $current_log_position;
-			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			# might need to retry if logging collector process is slow...
+			my $max_attempts = 180 * 10;
+			my $first_logfile;
+			for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
 			{
-				$current_log_position += pos($first_logfile);
-				last;
-			}
+				$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
 
-			usleep(100_000);
-		}
+				# Don't include previously matched text in the search.
+				$first_logfile = substr $first_logfile, $current_log_position;
+				if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+				{
+					$current_log_position += pos($first_logfile);
+					last;
+				}
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+				usleep(100_000);
+			}
 
-		note "current_log_position = $current_log_position";
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			note "current_log_position = $current_log_position";
+		}
 	}
 
 	return;
@@ -288,11 +291,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -304,6 +309,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -314,6 +320,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -323,6 +330,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -359,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -368,10 +377,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -385,10 +395,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -396,6 +407,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -412,5 +424,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#39Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#38)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Mar 15, 2021 at 03:50:48PM +0000, Jacob Champion wrote:

# might need to retry if logging collector process is slow...
my $max_attempts = 180 * 10;
my $first_logfile;
for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
{
$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
+
+			# Don't include previously matched text in the search.
+			$first_logfile = substr $first_logfile, $current_log_position;
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				$current_log_position += pos($first_logfile);
+				last;
+			}
+
usleep(100_000);

Looking at 0001, I am not much a fan of relying on the position of the
matching pattern in the log file. Instead of relying on the logging
collector and one single file, why not just changing the generation of
the logfile and rely on the output of stderr by restarting the server?
That means less tests, no need to wait for the logging collector to do
its business, and it solves your problem. Please see the idea with
the patch attached. Thoughts?
--
Michael

Attachments:

krb5-tap-simplify.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..aca673e97a 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 26;
 }
 else
 {
@@ -170,7 +170,6 @@ $node->append_conf(
 	'postgresql.conf', qq{
 listen_addresses = '$hostaddr'
 krb_server_keyfile = '$keytab'
-logging_collector = on
 log_connections = on
 # these ensure stability of test results:
 log_rotation_age = 0
@@ -212,27 +211,13 @@ sub test_access
 	# Verify specified log message is logged in the log file.
 	if ($expect_log_msg ne '')
 	{
-		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
-		note "current_logfiles = $current_logfiles";
-		like($current_logfiles, qr|^stderr log/postgresql-.*log$|,
-			 'current_logfiles is sane');
-
-		my $lfname = $current_logfiles;
-		$lfname =~ s/^stderr //;
-		chomp $lfname;
-
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
-		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
-			usleep(100_000);
-		}
-
+		my $first_logfile = slurp_file($node->logfile);
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+		     'found expected log file content');
+
+		# Rotate to a new file, for any follow-up check.
+		$node->rotate_logfile;
+		$node->restart;
 	}
 
 	return;
#40Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#39)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, Mar 18, 2021 at 05:14:24PM +0900, Michael Paquier wrote:

Looking at 0001, I am not much a fan of relying on the position of the
matching pattern in the log file. Instead of relying on the logging
collector and one single file, why not just changing the generation of
the logfile and rely on the output of stderr by restarting the server?
That means less tests, no need to wait for the logging collector to do
its business, and it solves your problem. Please see the idea with
the patch attached. Thoughts?

While looking at 0003, I have noticed that the new kerberos tests
actually switch from a logic where one message pattern matches, to a
logic where multiple message patterns match, but I don't see a problem
with what I sent previously, as long as one consume once a log file
and matches all the patterns once, say like the following in
test_access():
my $first_logfile = slurp_file($node->logfile);

# Verify specified log messages are logged in the log file.
while (my $expect_log_msg = shift @expect_log_msgs)
{
like($first_logfile, qr/\Q$expect_log_msg\E/,
'found expected log file content');
}

# Rotate to a new file, for any next check.
$node->rotate_logfile;
$node->restart;

A second solution would be a logrotate, relying on the contents of
current_logfiles to know what is the current file, with an extra wait
after $node->logrotate to check if the contents of current_logfiles
have changed. That's slower for me as this requires a small sleep to
make sure that the new log file name has changed, and I find the
restart solution simpler and more elegant. Please see the attached
based on HEAD for this logrotate idea.

Jacob, what do you think?
--
Michael

Attachments:

krb5-tap-simplify-2.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..dc0f772834 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -233,6 +233,22 @@ sub test_access
 
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
 			 'found expected log file content');
+
+		# Rotate to a new file, for any next check.  Note that
+		# pg_ctl does not wait for the operation to complete
+		# so wait for the result to change first.  Sleep a bit
+		# to have a new log file name.
+		sleep(2);
+		$node->logrotate;
+		my $new_current_logfiles;
+
+		# Wait until the contents of current_logfiles have changed.
+		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
+		{
+			$new_current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
+			last if $new_current_logfiles ne $current_logfiles;
+			usleep(100_000);
+		}
 	}
 
 	return;
#41Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#40)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-03-19 at 17:21 +0900, Michael Paquier wrote:

On Thu, Mar 18, 2021 at 05:14:24PM +0900, Michael Paquier wrote:

Looking at 0001, I am not much a fan of relying on the position of the
matching pattern in the log file. Instead of relying on the logging
collector and one single file, why not just changing the generation of
the logfile and rely on the output of stderr by restarting the server?

For getting rid of the logging collector logic, this is definitely an
improvement. It was briefly discussed in [1]/messages/by-id/f1fd9ccaf7ffb2327bf3c06120afeadd50c1db97.camel@vmware.com but I never got around to
trying it; thanks!

One additional improvement I would suggest, now that the rotation logic
is simpler than it was in my original patch, is to rotate the logfile
regardless of whether the test is checking the logs or not. (Similarly,
we can manually rotate after the block of test_query() calls.) That way
it's harder to match the last test's output.

While looking at 0003, I have noticed that the new kerberos tests
actually switch from a logic where one message pattern matches, to a
logic where multiple message patterns match, but I don't see a problem
with what I sent previously, as long as one consume once a log file
and matches all the patterns once, say like the following in
test_access():

The tradeoff is that if you need to check for log message order, or for
multiple instances of overlapping patterns, you still need some sort of
search-forward functionality. But looking over the tests, I don't see
any that truly *need* that yet. It's nice that the current patchset
enforces an "authenticated" line before an "authorized" line, but I
think it's nicer to not have the extra code.

I'll incorporate this approach into the patchset. Thanks!

--Jacob

[1]: /messages/by-id/f1fd9ccaf7ffb2327bf3c06120afeadd50c1db97.camel@vmware.com

#42Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#41)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-03-19 at 16:54 +0000, Jacob Champion wrote:

One additional improvement I would suggest, now that the rotation logic
is simpler than it was in my original patch, is to rotate the logfile
regardless of whether the test is checking the logs or not. (Similarly,
we can manually rotate after the block of test_query() calls.) That way
it's harder to match the last test's output.

The same effect can be had by moving the log rotation to the top of the
test that needs it, so I've done it that way in v7.

The tradeoff is that if you need to check for log message order, or for
multiple instances of overlapping patterns, you still need some sort of
search-forward functionality.

Turns out it's easy now to have our cake and eat it too; a single if
statement can implement the same search-forward functionality that was
spread across multiple places before. So I've done that too.

Much nicer, thank you for the suggestion!

--Jacob

Attachments:

v7-0001-test-kerberos-rotate-logs-between-tests.patchtext/x-patch; name=v7-0001-test-kerberos-rotate-logs-between-tests.patchDownload
From 29b2de27e6370c883bfab9fa307c9764a1ffd0e1 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:04:42 -0800
Subject: [PATCH v7 1/3] test/kerberos: rotate logs between tests

The log content tests searched through the entire log file, from the
beginning, for every match. If two tests shared the same expected log
content, it was possible for the second test to get a false positive by
matching against the first test's output. (This could be seen by
modifying one of the expected-failure tests to expect the same output as
a previous happy-path test.)

To fix, rotate the logs before every test that checks log files. This
has the advantage that we no longer wait for a long timeout on a failed
match.

Original implementation by Michael Pacquier, with a change by me to
rotate at the beginning of every test instead of the end.
---
 src/test/kerberos/t/001_auth.pl | 33 +++++++++++----------------------
 1 file changed, 11 insertions(+), 22 deletions(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 079321bbfc..df62c28a10 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 26;
 }
 else
 {
@@ -170,7 +170,6 @@ $node->append_conf(
 	'postgresql.conf', qq{
 listen_addresses = '$hostaddr'
 krb_server_keyfile = '$keytab'
-logging_collector = on
 log_connections = on
 # these ensure stability of test results:
 log_rotation_age = 0
@@ -187,6 +186,14 @@ sub test_access
 {
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
 
+	if ($expect_log_msg ne '')
+	{
+		# Rotate to a new log file to prevent previous tests' log output from
+		# matching during this one.
+		$node->rotate_logfile;
+		$node->restart;
+	}
+
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
 		'postgres',
@@ -212,27 +219,9 @@ sub test_access
 	# Verify specified log message is logged in the log file.
 	if ($expect_log_msg ne '')
 	{
-		my $current_logfiles = slurp_file($node->data_dir . '/current_logfiles');
-		note "current_logfiles = $current_logfiles";
-		like($current_logfiles, qr|^stderr log/postgresql-.*log$|,
-			 'current_logfiles is sane');
-
-		my $lfname = $current_logfiles;
-		$lfname =~ s/^stderr //;
-		chomp $lfname;
-
-		# might need to retry if logging collector process is slow...
-		my $max_attempts = 180 * 10;
-		my $first_logfile;
-		for (my $attempts = 0; $attempts < $max_attempts; $attempts++)
-		{
-			$first_logfile = slurp_file($node->data_dir . '/' . $lfname);
-			last if $first_logfile =~ m/\Q$expect_log_msg\E/;
-			usleep(100_000);
-		}
-
+		my $first_logfile = slurp_file($node->logfile);
 		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+		     'found expected log file content');
 	}
 
 	return;
-- 
2.25.1

v7-0002-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v7-0002-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From db83791b1c95fdbc21322dba9bb5f1ea0e73c837 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v7 2/3] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index ad33122e0e..945419b76a 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v7-0003-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v7-0003-Log-authenticated-identity-from-all-auth-backends.patchDownload
From 9d1dc9e4352699886a8771e1bce0df66a610dbf4 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v7 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  52 ++++++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 381 insertions(+), 25 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 863ac31c6b..270cadbade 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6695,7 +6695,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index df62c28a10..6bb23e17eb 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -184,9 +184,9 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
-	if ($expect_log_msg ne '')
+	if (@expect_log_msgs > 0)
 	{
 		# Rotate to a new log file to prevent previous tests' log output from
 		# matching during this one.
@@ -216,12 +216,22 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $first_logfile = slurp_file($node->logfile);
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-		     'found expected log file content');
+
+		while (my $expect_log_msg = shift @expect_log_msgs)
+		{
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				# On a match, exclude the matched portion from future searches.
+				$first_logfile = substr $first_logfile, pos($first_logfile);
+			}
+		}
 	}
 
 	return;
@@ -255,11 +265,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -271,6 +283,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -281,6 +294,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -290,6 +304,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -326,6 +341,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -335,10 +351,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -352,10 +369,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -363,6 +381,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -379,5 +398,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#43Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#42)
2 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Mar 19, 2021 at 06:37:05PM +0000, Jacob Champion wrote:

The same effect can be had by moving the log rotation to the top of the
test that needs it, so I've done it that way in v7.

After thinking more about 0001, I have come up with an even simpler
solution that has resulted in 11e1577. That's similar to what
PostgresNode::issues_sql_like() does. This also makes 0003 simpler
with its changes as this requires to change two lines in test_access.

Turns out it's easy now to have our cake and eat it too; a single if
statement can implement the same search-forward functionality that was
spread across multiple places before. So I've done that too.

I have briefly looked at 0002 (0001 in the attached set), and it seems
sane to me. I still need to look at 0003 (well, now 0002) in details,
which is very sensible as one mistake would likely be a CVE-class
bug.
--
Michael

Attachments:

v8-0001-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-diff; charset=us-asciiDownload
From d82f7b15bb68011eb117c3409e455f05264194fb Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v8 1/2] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/include/libpq/libpq-be.h          |  1 +
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..18321703da 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
-- 
2.31.0

v8-0002-Log-authenticated-identity-from-all-auth-backends.patchtext/x-diff; charset=us-asciiDownload
From c2b9d514b5fe11aa2091de3f4cf81ba2f6fa0991 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 22 Mar 2021 15:08:04 +0900
Subject: [PATCH v8 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer
        (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres
        application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
        compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  34 +++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 doc/src/sgml/config.sgml                  |   2 +-
 10 files changed, 367 insertions(+), 21 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
-
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..00a2c6d3ea 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -207,7 +207,7 @@ sub test_access
 	}
 
 	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ee4925d6d9..db6b6c46e2 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6695,7 +6695,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
-- 
2.31.0

#44Magnus Hagander
magnus@hagander.net
In reply to: Michael Paquier (#43)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Mar 22, 2021 at 7:16 AM Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Mar 19, 2021 at 06:37:05PM +0000, Jacob Champion wrote:

The same effect can be had by moving the log rotation to the top of the
test that needs it, so I've done it that way in v7.

After thinking more about 0001, I have come up with an even simpler
solution that has resulted in 11e1577. That's similar to what
PostgresNode::issues_sql_like() does. This also makes 0003 simpler
with its changes as this requires to change two lines in test_access.

Man that renumbering threw me off :)

Turns out it's easy now to have our cake and eat it too; a single if
statement can implement the same search-forward functionality that was
spread across multiple places before. So I've done that too.

I have briefly looked at 0002 (0001 in the attached set), and it seems
sane to me. I still need to look at 0003 (well, now 0002) in details,
which is very sensible as one mistake would likely be a CVE-class
bug.

The 0002/0001/whateveritisaftertherebase is tracked over at
/messages/by-id/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net
isn't it? I've assumed the expectation is to have that one committed
from that thread, and then rebase using that.

--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/

#45Jacob Champion
pchampion@vmware.com
In reply to: Magnus Hagander (#44)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-03-22 at 18:22 +0100, Magnus Hagander wrote:

On Mon, Mar 22, 2021 at 7:16 AM Michael Paquier <michael@paquier.xyz> wrote:

I have briefly looked at 0002 (0001 in the attached set), and it seems
sane to me. I still need to look at 0003 (well, now 0002) in details,
which is very sensible as one mistake would likely be a CVE-class
bug.

The 0002/0001/whateveritisaftertherebase is tracked over at
https://nam04.safelinks.protection.outlook.com/?url=https%3A%2F%2Fwww.postgresql.org%2Fmessage-id%2Fflat%2F92e70110-9273-d93c-5913-0bccb6562740%40dunslane.net&amp;amp;data=04%7C01%7Cpchampion%40vmware.com%7Cd085c1e56ff045c7af3308d8ed57279a%7Cb39138ca3cee4b4aa4d6cd83d9dd62f0%7C0%7C0%7C637520305878415422%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&amp;amp;sdata=kyW9O1jD0z14z0rC%2BYY9UhIKb7D6bg0nCWoVBJkF8oQ%3D&amp;amp;reserved=0
isn't it? I've assumed the expectation is to have that one committed
from that thread, and then rebase using that.

I think the primary thing that needs to be greenlit for both is the
idea of using the RFC 2253/4514 format for Subject DNs.

Other than that, the version here should only contain the changes
necessary for both features (that is, port->peer_dn), so there's no
hard dependency between the two. It's just on me to make sure my
version is up-to-date. Which I believe it is, as of today.

--Jacob

#46Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#43)
2 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-03-22 at 15:16 +0900, Michael Paquier wrote:

On Fri, Mar 19, 2021 at 06:37:05PM +0000, Jacob Champion wrote:

The same effect can be had by moving the log rotation to the top of the
test that needs it, so I've done it that way in v7.

After thinking more about 0001, I have come up with an even simpler
solution that has resulted in 11e1577. That's similar to what
PostgresNode::issues_sql_like() does. This also makes 0003 simpler
with its changes as this requires to change two lines in test_access.

v8's test_access lost the in-order log search from v7; I've added it
back in v9. The increased resistance to entropy seems worth the few
extra lines. Thoughts?

--Jacob

Attachments:

v9-0001-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v9-0001-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From 1939c94fe9d21b19a456ce99d03dfe8b4682d5af Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v9 1/2] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..18321703da 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v9-0002-Log-authenticated-identity-from-all-auth-backends.patchtext/x-patch; name=v9-0002-Log-authenticated-identity-from-all-auth-backends.patchDownload
From e315f806dc7895a8c1a51cec4d073c5d3ccea74b Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v9 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines in order.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  49 +++++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 379 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5679b40dd5..7f95feac9b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6695,7 +6695,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..40fa498e71 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,13 +206,22 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	if (@expect_log_msgs > 0)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
+		while (my $expect_log_msg = shift @expect_log_msgs)
+		{
+			like($first_logfile, qr/\Q$expect_log_msg\E/,
+				 'found expected log file content');
+
+			if ($first_logfile =~ m/\Q$expect_log_msg\E/g)
+			{
+				# On a match, exclude the matched portion from future searches.
+				$first_logfile = substr $first_logfile, pos($first_logfile);
+			}
+		}
 	}
 
 	# Clean up any existing contents in the node's log file so as
@@ -249,11 +258,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +276,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +287,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +297,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +334,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +344,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +362,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +374,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +391,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#47Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#46)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Mar 22, 2021 at 07:17:26PM +0000, Jacob Champion wrote:

v8's test_access lost the in-order log search from v7; I've added it
back in v9. The increased resistance to entropy seems worth the few
extra lines. Thoughts?

I am not really sure that we need to bother about the ordering of the
entries here, as long as we check for all of them within the same
fragment of the log file, so I would just go down to the simplest
solution that I posted upthread that is enough to make sure that the
verbosity is protected. That's what we do elsewhere, like with
command_checks_all() and such.
--
Michael

#48Michael Paquier
michael@paquier.xyz
In reply to: Magnus Hagander (#44)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Mar 22, 2021 at 06:22:52PM +0100, Magnus Hagander wrote:

The 0002/0001/whateveritisaftertherebase is tracked over at
/messages/by-id/92e70110-9273-d93c-5913-0bccb6562740@dunslane.net
isn't it? I've assumed the expectation is to have that one committed
from that thread, and then rebase using that.

Independent and useful pieces could just be extracted and applied
separately where it makes sense. I am not sure if that's the case
here, so I'll do a patch_to_review++.
--
Michael

#49Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#47)
2 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-03-23 at 14:21 +0900, Michael Paquier wrote:

I am not really sure that we need to bother about the ordering of the
entries here, as long as we check for all of them within the same
fragment of the log file, so I would just go down to the simplest
solution that I posted upthread that is enough to make sure that the
verbosity is protected. That's what we do elsewhere, like with
command_checks_all() and such.

With low-coverage test suites, I think it's useful to allow as little
strange behavior as possible -- in this case, printing authorization
before authentication could signal a serious bug -- but I don't feel
too strongly about it.

v10 attached, which reverts to v8 test behavior, with minor updates to
the commit message and test comment.

--Jacob

Attachments:

v10-0001-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v10-0001-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From 0b8764ac61ef37809a79ded2596c5fcd2caf25bb Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v10 1/2] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..18321703da 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v10-0002-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v10-0002-Log-authenticated-identity-from-all-auth-backend.patchDownload
From ac4812f4c30456849462d77982f9ce8139ba51a4 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v10 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 119 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  60 ++++++++++-
 src/test/kerberos/t/001_auth.pl           |  36 +++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  53 +++++++++-
 10 files changed, 368 insertions(+), 22 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1f0e0fc1fb..cc6a25b1fa 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6672,7 +6672,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..bfcffbdb3b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,47 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +801,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +863,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1224,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1337,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1567,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1972,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2294,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2332,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2839,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2894,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3076,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..aa15877ef7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
+		return NULL;
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..3ac137aebd 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 19;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,84 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..271e56824b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +206,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..d222e086ec 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,14 +48,44 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
-$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Bad password
+$ENV{PGPASSWORD} = "badpass";
+test_connect_fails($common_connstr, "user=ssltestuser",
+	qr/password authentication failed/,
+	"Basic SCRAM authentication with bad password");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Default settings
+$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
+
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -102,6 +132,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+$log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#50Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#49)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, Mar 24, 2021 at 04:45:35PM +0000, Jacob Champion wrote:

With low-coverage test suites, I think it's useful to allow as little
strange behavior as possible -- in this case, printing authorization
before authentication could signal a serious bug -- but I don't feel
too strongly about it.

I got to look at the DN patch yesterday, so now's the turn of this
one. Nice work.

+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
[...]
+   port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
It may not be obvious that all the field is copied to TopMemoryContext
because the Port requires that.
+$node->stop('fast');
+my $log_contents = slurp_file($log);
Like 11e1577, let's just truncate the log files in all those tests.
+   if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+   {
+       Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
What's the point of having the check and the assertion?  NULL does not
really seem like a good default here as this should never really
happen.  Wouldn't a FATAL be actually safer?
+like(
+   $log_contents,
+   qr/connection authenticated: identity="ssltestuser"
method=scram-sha-256/,
+   "Basic SCRAM sets the username as the authenticated identity");
+
+$node->start;
It looks wrong to me to include in the SSL tests some checks related
to SCRAM authentication.  This should remain in 001_password.pl, as of
src/test/authentication/.

port->gss->princ = MemoryContextStrdup(TopMemoryContext, port->gbuf.value);
+ set_authn_id(port, gbuf.value);
I don't think that this position is right for GSSAPI. Shouldn't this
be placed at the end of pg_GSS_checkauth() and only if the status is
OK?

-   ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
-
-   pfree(peer_user);
+   ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
I would also put this one after checking the usermap for peer.
+   /*
+    * We have all of the information necessary to construct the authenticated
+    * identity.
+    */
+   if (port->hba->compat_realm)
+   {
+       /* SAM-compatible format. */
+       authn_id = psprintf("%s\\%s", domainname, accountname);
+   }
+   else
+   {
+       /* Kerberos principal format. */
+       authn_id = psprintf("%s@%s", accountname, domainname);
+   }
+
+   set_authn_id(port, authn_id);
+   pfree(authn_id);
For SSPI, I think that this should be moved down once we are sure that
there is no error and that pg_SSPI_recvauth() reports STATUS_OK to the
caller.  There is a similar issue with CheckCertAuth(), and
set_authn_id() is documented so as it should be called only when we
are sure that authentication succeeded.

Reading through the thread, the consensus is to add the identity
information with log_connections. One question I have is that if we
just log the information with log_connectoins, there is no real reason
to add this information to the Port, except the potential addition of
some system function, a superuser-only column in pg_stat_activity or
to allow extensions to access this information. I am actually in
favor of keeping this information in the Port with those pluggability
reasons. How do others feel about that?
--
Michael

#51Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#50)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, 2021-03-25 at 14:41 +0900, Michael Paquier wrote:

I got to look at the DN patch yesterday, so now's the turn of this
one. Nice work.

Thanks!

+ port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
It may not be obvious that all the field is copied to TopMemoryContext
because the Port requires that.

I've expanded the comment. (v11 attached, with incremental changes over
v10 in since-v10.diff.txt.)

+$node->stop('fast');
+my $log_contents = slurp_file($log);
Like 11e1577, let's just truncate the log files in all those tests.

Hmm... having the full log file contents for the SSL tests has been
incredibly helpful for me with the NSS work. I'd hate to lose them; it
can be very hard to recreate the test conditions exactly.

+   if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+   {
+       Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
What's the point of having the check and the assertion?  NULL does not
really seem like a good default here as this should never really
happen.  Wouldn't a FATAL be actually safer?

I think FATAL makes more sense. Changed, thanks.

It looks wrong to me to include in the SSL tests some checks related
to SCRAM authentication. This should remain in 001_password.pl, as of
src/test/authentication/.

Agreed. Moved the bad-password SCRAM tests over, and removed the
duplicates. The last SCRAM test in that file, which tests the
interaction between client certificates and SCRAM auth, remains.

port->gss->princ = MemoryContextStrdup(TopMemoryContext, port->gbuf.value);
+ set_authn_id(port, gbuf.value);
I don't think that this position is right for GSSAPI. Shouldn't this
be placed at the end of pg_GSS_checkauth() and only if the status is
OK?

No, and the tests will catch you if you try. Authentication happens
before authorization (user mapping), and can succeed independently even
if authz doesn't. See below.

For SSPI, I think that this should be moved down once we are sure that
there is no error and that pg_SSPI_recvauth() reports STATUS_OK to the
caller. There is a similar issue with CheckCertAuth(), and
set_authn_id() is documented so as it should be called only when we
are sure that authentication succeeded.

Authentication *has* succeeded already; that's what the SSPI machinery
has done above. Likewise for CheckCertAuth, which relies on the TLS
subsystem to validate the client signature before setting the peer_cn.
The user mapping is an authorization concern: it answers the question,
"is an authenticated user allowed to use a particular Postgres user
name?"

Postgres currently conflates authn and authz in many places, and in my
experience, that'll make it difficult to maintain new authorization
features like the ones in the wishlist upthread. This patch is only one
step towards a clearer distinction.

I am actually in
favor of keeping this information in the Port with those pluggability
reasons.

That was my intent, yeah. Getting this into the stats framework was
more than I could bite off for this first patchset, but having it
stored in a central location will hopefully help people do more with
it.

--Jacob

Attachments:

since-v10.diff.txttext/plain; name=since-v10.diff.txtDownload
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index bfcffbdb3b..0647d7cc32 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -348,6 +348,10 @@ auth_failed(Port *port, int status, char *logdetail)
  * Auth methods should call this exactly once, as soon as the user is
  * successfully authenticated, even if they have reason to know that
  * authorization will fail later.
+ *
+ * The provided string will be copied into the TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
  */
 static void
 set_authn_id(Port *port, const char *id)
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index aa15877ef7..309bbc06d0 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3128,8 +3128,8 @@ hba_authname(hbaPort *port)
 
 	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
 	{
-		Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
-		return NULL;
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
 	}
 
 	return UserAuthName[auth_method];
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3ac137aebd..adeb3bce33 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 19;
+	plan tests => 21;
 }
 
 
@@ -126,6 +126,23 @@ unlike(
 $log = $node->rotate_logfile();
 $node->start;
 
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2);
+$ENV{"PGPASSWORD"} = 'pass';
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# Make sure authenticated identity isn't set if the password is wrong.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index d222e086ec..c15b9c405b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 15 : 16;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -48,44 +48,14 @@ $node->start;
 configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
 	"scram-sha-256", "pass", "scram-sha-256");
 switch_server_cert($node, 'server-cn-only');
+$ENV{PGPASSWORD} = "pass";
 $common_connstr =
   "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
 
-my $log = $node->rotate_logfile();
-$node->restart;
-
-# Bad password
-$ENV{PGPASSWORD} = "badpass";
-test_connect_fails($common_connstr, "user=ssltestuser",
-	qr/password authentication failed/,
-	"Basic SCRAM authentication with bad password");
-
-$node->stop('fast');
-my $log_contents = slurp_file($log);
-
-unlike(
-	$log_contents,
-	qr/connection authenticated:/,
-	"SCRAM does not set authenticated identity with bad password");
-
-$log = $node->rotate_logfile();
-$node->start;
-
 # Default settings
-$ENV{PGPASSWORD} = "pass";
 test_connect_ok($common_connstr, "user=ssltestuser",
 	"Basic SCRAM authentication with SSL");
 
-$node->stop('fast');
-$log_contents = slurp_file($log);
-
-like(
-	$log_contents,
-	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
-	"Basic SCRAM sets the username as the authenticated identity");
-
-$node->start;
-
 # Test channel_binding
 test_connect_fails(
 	$common_connstr,
@@ -132,7 +102,7 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
-$log = $node->rotate_logfile();
+my $log = $node->rotate_logfile();
 $node->restart;
 
 # Certificate verification at the connection level should still work fine.
@@ -142,7 +112,7 @@ test_connect_ok(
 	"SCRAM with clientcert=verify-full and channel_binding=require");
 
 $node->stop('fast');
-$log_contents = slurp_file($log);
+my $log_contents = slurp_file($log);
 
 like(
 	$log_contents,
v11-0001-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v11-0001-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From 0b8764ac61ef37809a79ded2596c5fcd2caf25bb Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v11 1/2] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..18321703da 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..d970277894 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -190,6 +190,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

v11-0002-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v11-0002-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 47ff6c9c1c93cdf596b13302e2975b132815436c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v11 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 123 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 +++
 src/test/authentication/t/001_password.pl |  77 +++++++++++++-
 src/test/kerberos/t/001_auth.pl           |  36 +++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 +++++++++-
 src/test/ssl/t/002_scram.pl               |  21 +++-
 10 files changed, 358 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1f0e0fc1fb..cc6a25b1fa 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6672,7 +6672,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..0647d7cc32 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into the TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1173,9 +1228,10 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later.
+	 * memory for display later. This is also our authenticated identity.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1341,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1571,24 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1976,11 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/* Success! Store the identity and check the usermap */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2004,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2035,13 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area. This is our authenticated
+	 * identity.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2298,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2336,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2843,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2898,26 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3080,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..309bbc06d0 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index d970277894..a104f811f1 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -154,6 +154,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..adeb3bce33 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 21;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,101 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2);
+$ENV{"PGPASSWORD"} = 'pass';
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# Make sure authenticated identity isn't set if the password is wrong.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..271e56824b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +206,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..c15b9c405b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#52Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#51)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, Mar 25, 2021 at 06:51:22PM +0000, Jacob Champion wrote:

On Thu, 2021-03-25 at 14:41 +0900, Michael Paquier wrote:

+ port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
It may not be obvious that all the field is copied to TopMemoryContext
because the Port requires that.

I've expanded the comment. (v11 attached, with incremental changes over
v10 in since-v10.diff.txt.)

That's the addition of "to match the lifetime of the Port". Looks
good.

+$node->stop('fast');
+my $log_contents = slurp_file($log);
Like 11e1577, let's just truncate the log files in all those tests.

Hmm... having the full log file contents for the SSL tests has been
incredibly helpful for me with the NSS work. I'd hate to lose them; it
can be very hard to recreate the test conditions exactly.

Does it really matter to have the full contents of the file from the
previous tests though? like() would report the contents of
slurp_file() when it fails if the generated output does not match the
expected one, so you actually get less noise this way.

+   if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+   {
+       Assert((0 <= auth_method) && (auth_method <= USER_AUTH_LAST));
What's the point of having the check and the assertion?  NULL does not
really seem like a good default here as this should never really
happen.  Wouldn't a FATAL be actually safer?

I think FATAL makes more sense. Changed, thanks.

Thanks. FWIW, one worry I had here was a corrupted stack that calls
this code path that would remain undetected.

For SSPI, I think that this should be moved down once we are sure that
there is no error and that pg_SSPI_recvauth() reports STATUS_OK to the
caller. There is a similar issue with CheckCertAuth(), and
set_authn_id() is documented so as it should be called only when we
are sure that authentication succeeded.

Authentication *has* succeeded already; that's what the SSPI machinery
has done above. Likewise for CheckCertAuth, which relies on the TLS
subsystem to validate the client signature before setting the peer_cn.
The user mapping is an authorization concern: it answers the question,
"is an authenticated user allowed to use a particular Postgres user
name?"

Okay. Could you make the comments in those various areas more
explicit about the difference and that it is intentional to register
the auth ID before checking the user map? Anybody reading this code
in the future may get confused with the differences in handling all
that according to the auth type involved if that's not clearly
stated.

That was my intent, yeah. Getting this into the stats framework was
more than I could bite off for this first patchset, but having it
stored in a central location will hopefully help people do more with
it.

No problem with that.
--
Michael

#53Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#52)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-03-26 at 09:12 +0900, Michael Paquier wrote:

Does it really matter to have the full contents of the file from the
previous tests though?

For a few of the bugs I was tracking down, it was imperative. The tests
aren't isolated enough (or at all) to keep one from affecting the
others. And if the test is written incorrectly, or becomes incorrect
due to implementation changes, then the log files are really the only
way to debug after a false positive -- with truncation, the bad test
succeeds incorrectly and then swallows the evidence. :)

Could you make the comments in those various areas more
explicit about the difference and that it is intentional to register
the auth ID before checking the user map? Anybody reading this code
in the future may get confused with the differences in handling all
that according to the auth type involved if that's not clearly
stated.

I took a stab at this in v12, attached.

--Jacob

Attachments:

v12-0001-ssl-store-client-s-DN-in-port-peer_dn.patchtext/x-patch; name=v12-0001-ssl-store-client-s-DN-in-port-peer_dn.patchDownload
From c21a83e71d2829accf004f5b8437a6eb115b0860 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 8 Feb 2021 10:53:20 -0800
Subject: [PATCH v12 1/2] ssl: store client's DN in port->peer_dn

Original patch by Andrew Dunstan:

    https://www.postgresql.org/message-id/fd96ae76-a8e3-ef8e-a642-a592f5b76771%40dunslane.net

but I've taken out the clientname=DN functionality; all that will be
needed for the next patch is the DN string.
---
 src/backend/libpq/be-secure-openssl.c | 53 +++++++++++++++++++++++----
 src/include/libpq/libpq-be.h          |  1 +
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 5ce3f27855..18321703da 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -551,22 +551,25 @@ aloop:
 	/* Get client certificate, if available. */
 	port->peer = SSL_get_peer_certificate(port->ssl);
 
-	/* and extract the Common Name from it. */
+	/* and extract the Common Name / Distinguished Name from it. */
 	port->peer_cn = NULL;
+	port->peer_dn = NULL;
 	port->peer_cert_valid = false;
 	if (port->peer != NULL)
 	{
 		int			len;
+		X509_NAME  *x509name = X509_get_subject_name(port->peer);
+		char	   *peer_cn;
+		char	   *peer_dn;
+		BIO		   *bio = NULL;
+		BUF_MEM    *bio_buf = NULL;
 
-		len = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										NID_commonName, NULL, 0);
+		len = X509_NAME_get_text_by_NID(x509name, NID_commonName, NULL, 0);
 		if (len != -1)
 		{
-			char	   *peer_cn;
-
 			peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-			r = X509_NAME_get_text_by_NID(X509_get_subject_name(port->peer),
-										  NID_commonName, peer_cn, len + 1);
+			r = X509_NAME_get_text_by_NID(x509name, NID_commonName, peer_cn,
+										  len + 1);
 			peer_cn[len] = '\0';
 			if (r != len)
 			{
@@ -590,6 +593,36 @@ aloop:
 
 			port->peer_cn = peer_cn;
 		}
+
+		bio = BIO_new(BIO_s_mem());
+		if (!bio)
+		{
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+		/* use commas instead of slashes */
+		X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253);
+		BIO_get_mem_ptr(bio, &bio_buf);
+		peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length + 1);
+		memcpy(peer_dn, bio_buf->data, bio_buf->length);
+		peer_dn[bio_buf->length] = '\0';
+		if (bio_buf->length != strlen(peer_dn))
+		{
+			ereport(COMMERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("SSL certificate's distinguished name contains embedded null")));
+			BIO_free(bio);
+			pfree(peer_dn);
+			pfree(port->peer_cn);
+			port->peer_cn = NULL;
+			return -1;
+		}
+
+		BIO_free(bio);
+
+		port->peer_dn = peer_dn;
+
 		port->peer_cert_valid = true;
 	}
 
@@ -618,6 +651,12 @@ be_tls_close(Port *port)
 		pfree(port->peer_cn);
 		port->peer_cn = NULL;
 	}
+
+	if (port->peer_dn)
+	{
+		pfree(port->peer_dn);
+		port->peer_dn = NULL;
+	}
 }
 
 ssize_t
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 891394b0c3..713c34fedd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -195,6 +195,7 @@ typedef struct Port
 	 */
 	bool		ssl_in_use;
 	char	   *peer_cn;
+	char	   *peer_dn;
 	bool		peer_cert_valid;
 
 	/*
-- 
2.25.1

since-v11.diff.txttext/plain; name=since-v11.diff.txtDownload
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 0647d7cc32..d31fff744c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1228,7 +1228,11 @@ pg_GSS_checkauth(Port *port)
 
 	/*
 	 * Copy the original name of the authenticated principal into our backend
-	 * memory for display later. This is also our authenticated identity.
+	 * memory for display later.
+	 *
+	 * This is also our authenticated identity. Set it now, rather than waiting
+	 * for check_usermap below, because authentication has already succeeded and
+	 * we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
 	set_authn_id(port, gbuf.value);
@@ -1573,7 +1577,9 @@ pg_SSPI_recvauth(Port *port)
 
 	/*
 	 * We have all of the information necessary to construct the authenticated
-	 * identity.
+	 * identity. Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file to
+	 * reflect that.
 	 */
 	if (port->hba->compat_realm)
 	{
@@ -1977,7 +1983,11 @@ ident_inet_done:
 
 	if (ident_return)
 	{
-		/* Success! Store the identity and check the usermap */
+		/*
+		 * Success! Store the identity and check the usermap. (Setting the
+		 * authenticated identity is done before checking the usermap, because
+		 * at this point, authentication has succeeded.)
+		 */
 		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
 	}
@@ -2036,8 +2046,9 @@ auth_peer(hbaPort *port)
 	}
 
 	/*
-	 * Make a copy of static getpw*() result area. This is our authenticated
-	 * identity.
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity. Set it before calling check_usermap, because authentication has
+	 * already succeeded and we want the log file to reflect that.
 	 */
 	set_authn_id(port, pw->pw_name);
 
@@ -2901,7 +2912,9 @@ CheckCertAuth(Port *port)
 	if (port->hba->auth_method == uaCert)
 	{
 		/*
-		 * The client's Subject DN is our authenticated identity.
+		 * The client's Subject DN is our authenticated identity. Set it now,
+		 * rather than waiting for check_usermap below, because authentication
+		 * has already succeeded and we want the log file to reflect that.
 		 */
 		if (!port->peer_dn)
 		{
v12-0002-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v12-0002-Log-authenticated-identity-from-all-auth-backend.patchDownload
From b2ab5c6a694f7026d243186267204e5e01883eaf Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v12 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines.
---
 doc/src/sgml/config.sgml                  |   2 +-
 src/backend/libpq/auth.c                  | 134 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 ++
 src/test/authentication/t/001_password.pl |  77 ++++++++++++-
 src/test/kerberos/t/001_auth.pl           |  36 ++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  55 ++++++++-
 src/test/ssl/t/002_scram.pl               |  21 +++-
 10 files changed, 370 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ddc6d789d8..3b08aa6bbb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6678,7 +6678,7 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..d31fff744c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into the TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity. Set it now, rather than waiting
+	 * for check_usermap below, because authentication has already succeeded and
+	 * we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity. Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file to
+	 * reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success! Store the identity and check the usermap. (Setting the
+		 * authenticated identity is done before checking the usermap, because
+		 * at this point, authentication has succeeded.)
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity. Set it before calling check_usermap, because authentication has
+	 * already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2813,6 +2909,28 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * The client's Subject DN is our authenticated identity. Set it now,
+		 * rather than waiting for check_usermap below, because authentication
+		 * has already succeeded and we want the log file to reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (after all, we were able to get the subject
+			 * CN). This probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2975,6 +3093,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a04c093d5..309bbc06d0 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3110,3 +3110,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8f09b5638f..7cc06808aa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -130,6 +130,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..312edd7fd0 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..adeb3bce33 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 21;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,101 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2);
+$ENV{"PGPASSWORD"} = 'pass';
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# Make sure authenticated identity isn't set if the password is wrong.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..271e56824b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +206,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index bfada03d3e..bb27f2e46d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 100;
+	plan tests => 104;
 }
 
 #### Some configuration
@@ -417,6 +417,9 @@ test_connect_fails(
 	qr/connection requires a valid client certificate/,
 	"certificate authorization fails without client cert");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert in unencrypted PEM
 test_connect_ok(
 	$common_connstr,
@@ -424,6 +427,17 @@ test_connect_ok(
 	"certificate authorization succeeds with correct client cert in PEM format"
 );
 
+# make sure certificate DNs are logged correctly
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # correct client cert in unencrypted DER
 test_connect_ok(
 	$common_connstr,
@@ -509,6 +523,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -517,6 +534,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -524,12 +552,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -551,6 +592,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..c15b9c405b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#54Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#53)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Mar 26, 2021 at 10:41:03PM +0000, Jacob Champion wrote:

For a few of the bugs I was tracking down, it was imperative. The tests
aren't isolated enough (or at all) to keep one from affecting the
others.

If the output of the log file is redirected to stderr and truncated,
while the connection attempts are isolated according to the position
where the file is truncated, I am not quite sure to follow this line
of thoughts. What actually happened? Should we make the tests more
stable instead? The kerberos have been running for one week now with
11e1577a on HEAD, and look stable so it would be good to be consistent
on all fronts.

And if the test is written incorrectly, or becomes incorrect
due to implementation changes, then the log files are really the only
way to debug after a false positive -- with truncation, the bad test
succeeds incorrectly and then swallows the evidence. :)

Hmm, okay. However, I still see a noticeable difference in the tests
without the additional restarts done so I would rather avoid this
cost. For example, on my laptop, the restarts make
authentication/t/001_password.pl last 7s. Truncating the logs without
any restarts bring the test down to 5.3s so that's 20% faster without
impacting its coverage. If you want to keep this information around
for debugging, I guess that we could just print the contents of the
backend logs to regress_log_001_password instead? This could be done
with a simple wrapper routine that prints the past contents of the log
file before truncating them. I am not sure that we need to stop the
server while checking for the logs contents either, to start it again
a bit later in the test while the configuration does not change. that
costs in speed.

Could you make the comments in those various areas more
explicit about the difference and that it is intentional to register
the auth ID before checking the user map? Anybody reading this code
in the future may get confused with the differences in handling all
that according to the auth type involved if that's not clearly
stated.

I took a stab at this in v12, attached.

This part looks good, thanks!

         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
I am wondering if this paragraph can be confusing for the end-user
without more explanation and a link to the "User Name Maps" section,
and if we actually need this addition at all.  The difference is that
the authenticated log is logged before the authorized log, with user
name map checks in-between for some of the auth methods.  HEAD refers
to the existing authorized log as "authentication" in the logs, while
you correct that.
--
Michael
#55Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#54)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-03-29 at 16:50 +0900, Michael Paquier wrote:

On Fri, Mar 26, 2021 at 10:41:03PM +0000, Jacob Champion wrote:

For a few of the bugs I was tracking down, it was imperative. The tests
aren't isolated enough (or at all) to keep one from affecting the
others.

If the output of the log file is redirected to stderr and truncated,
while the connection attempts are isolated according to the position
where the file is truncated, I am not quite sure to follow this line
of thoughts. What actually happened? Should we make the tests more
stable instead?

It's not a matter of the tests being stable, but of the tests needing
to change and evolve as the implementation changes. A big part of that
is visibility into what the tests are doing, so that you can debug
them.

I'm sorry I don't have any explicit examples; the NSS work is pretty
broad.

The kerberos have been running for one week now with
11e1577a on HEAD, and look stable so it would be good to be consistent
on all fronts.

I agree that it would be good in general, as long as the consistency
isn't at the expense of usefulness.

Keep in mind that the rotate-restart-slurp method comes from an
existing test. I assume Andrew chose that method for the same reasons I
did -- it works with what we currently have.

Hmm, okay. However, I still see a noticeable difference in the tests
without the additional restarts done so I would rather avoid this
cost. For example, on my laptop, the restarts make
authentication/t/001_password.pl last 7s. Truncating the logs without
any restarts bring the test down to 5.3s so that's 20% faster without
impacting its coverage.

I agree that it'd be ideal not to have to restart the server. But 20%
of less than ten seconds is less than two seconds, and the test suite
has to run thousands of times to make up a single hour of debugging
time that would be (hypothetically) lost by missing log files. (These
are not easy tests for me to debug and maintain, personally -- maybe
others have a different experience.)

If you want to keep this information around
for debugging, I guess that we could just print the contents of the
backend logs to regress_log_001_password instead? This could be done
with a simple wrapper routine that prints the past contents of the log
file before truncating them. I am not sure that we need to stop the
server while checking for the logs contents either, to start it again
a bit later in the test while the configuration does not change. that
costs in speed.

Is the additional effort to create (and maintain) that new system worth
two seconds per run? I feel like it's not -- but if you feel strongly
then I can definitely look into it.

Personally, I'd rather spend time making it easy for tests to get the
log entries associated with a given connection or query. It seems like
every suite has had to cobble together its own method of checking the
log files, with varying levels of success/correctness. Maybe something
with session_preload_libraries and the emit_log_hook? But that would be
a job for a different changeset.

Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
I am wondering if this paragraph can be confusing for the end-user
without more explanation and a link to the "User Name Maps" section,
and if we actually need this addition at all.  The difference is that
the authenticated log is logged before the authorized log, with user
name map checks in-between for some of the auth methods.  HEAD refers
to the existing authorized log as "authentication" in the logs, while
you correct that.

Which parts would you consider confusing/in need of change? I'm happy
to expand where needed. Would an inline sample be more helpful than a
textual explanation?

Thanks again for all the feedback!

--Jacob

#56Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#55)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Mar 29, 2021 at 11:53:03PM +0000, Jacob Champion wrote:

It's not a matter of the tests being stable, but of the tests needing
to change and evolve as the implementation changes. A big part of that
is visibility into what the tests are doing, so that you can debug
them.

Sure, but I still don't quite see why this applies here? At the point
of any test, like() or unlink() print the contents of the comparison
if there is a failure, so there is no actual loss of data. That's
what issues_sql_like() does, for one.

I'm sorry I don't have any explicit examples; the NSS work is pretty
broad.

Yeah, I saw that..

I agree that it would be good in general, as long as the consistency
isn't at the expense of usefulness.

Keep in mind that the rotate-restart-slurp method comes from an
existing test. I assume Andrew chose that method for the same reasons I
did -- it works with what we currently have.

PostgresNode::rotate_logfile got introduced in c098509, and it is just
used in t/017_shm.pl on HEAD. There could be more simplifications
with 019_replslot_limit.pl, I certainly agree with that.

I agree that it'd be ideal not to have to restart the server. But 20%
of less than ten seconds is less than two seconds, and the test suite
has to run thousands of times to make up a single hour of debugging
time that would be (hypothetically) lost by missing log files. (These
are not easy tests for me to debug and maintain, personally -- maybe
others have a different experience.)

Is the additional effort to create (and maintain) that new system worth
two seconds per run? I feel like it's not -- but if you feel strongly
then I can definitely look into it.

I fear that heavily parallelized runs could feel the difference. Ask
Andres about that, he has been able to trigger in parallel a failure
with pg_upgrade wiping out testtablespace while the main regression
test suite just began :)

Personally, I'd rather spend time making it easy for tests to get the
log entries associated with a given connection or query. It seems like
every suite has had to cobble together its own method of checking the
log files, with varying levels of success/correctness. Maybe something
with session_preload_libraries and the emit_log_hook? But that would be
a job for a different changeset.

Maybe.

Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of client authentication and authorization.
I am wondering if this paragraph can be confusing for the end-user
without more explanation and a link to the "User Name Maps" section,
and if we actually need this addition at all.  The difference is that
the authenticated log is logged before the authorized log, with user
name map checks in-between for some of the auth methods.  HEAD refers
to the existing authorized log as "authentication" in the logs, while
you correct that.

Which parts would you consider confusing/in need of change? I'm happy
to expand where needed. Would an inline sample be more helpful than a
textual explanation?

That's with the use of "authentication and authorization". How can
users make the difference between what one or this other is without
some explanation with the name maps? It seems that there is no place
in the existing docs where this difference is explained. I am
wondering if it would be better to not change this paragraph, or
reword it slightly to outline that this may cause more than one log
entry, say:
"Causes each attempted connection to the server, and each
authentication activity to be logged."
--
Michael

#57Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#56)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-03-30 at 09:55 +0900, Michael Paquier wrote:

On Mon, Mar 29, 2021 at 11:53:03PM +0000, Jacob Champion wrote:

It's not a matter of the tests being stable, but of the tests needing
to change and evolve as the implementation changes. A big part of that
is visibility into what the tests are doing, so that you can debug
them.

Sure, but I still don't quite see why this applies here? At the point
of any test, like() or unlink() print the contents of the comparison
if there is a failure, so there is no actual loss of data. That's
what issues_sql_like() does, for one.

The key there is "if there is a failure" -- false positives need to be
debugged too. Tests I've worked with recently for the NSS work were
succeeding for the wrong reasons. Overly generic logfile matches ("SSL
error"), for example.

Keep in mind that the rotate-restart-slurp method comes from an
existing test. I assume Andrew chose that method for the same reasons I
did -- it works with what we currently have.

PostgresNode::rotate_logfile got introduced in c098509, and it is just
used in t/017_shm.pl on HEAD. There could be more simplifications
with 019_replslot_limit.pl, I certainly agree with that.

modules/ssl_passphrase_callback/t/001_testfunc.pl is where I pulled
this pattern from.

Is the additional effort to create (and maintain) that new system worth
two seconds per run? I feel like it's not -- but if you feel strongly
then I can definitely look into it.

I fear that heavily parallelized runs could feel the difference. Ask
Andres about that, he has been able to trigger in parallel a failure
with pg_upgrade wiping out testtablespace while the main regression
test suite just began :)

Does unilateral log truncation play any nicer with parallel test runs?
I understand not wanting to make an existing problem worse, but it
doesn't seem like the existing tests were written for general
parallelism.

Would it be acceptable to adjust the tests for live rotation using the
logging collector, rather than a full restart? It would unfortunately
mean that we have to somehow wait for the rotation to complete, since
that's asynchronous.

(Speaking of asynchronous: how does the existing check-and-truncate
code make sure that the log entries it's looking for have been flushed
to disk? Shutting down the server guarantees it.)

Which parts would you consider confusing/in need of change? I'm happy
to expand where needed. Would an inline sample be more helpful than a
textual explanation?

That's with the use of "authentication and authorization". How can
users make the difference between what one or this other is without
some explanation with the name maps? It seems that there is no place
in the existing docs where this difference is explained. I am
wondering if it would be better to not change this paragraph, or
reword it slightly to outline that this may cause more than one log
entry, say:
"Causes each attempted connection to the server, and each
authentication activity to be logged."

I took a stab at this in v13: "Causes each attempted connection to the
server to be logged, as well as successful completion of both client
authentication (if necessary) and authorization." (IMO any further in-
depth explanation of authn/z and user mapping probably belongs in the
auth method documentation, and this patch doesn't change any authn/z
behavior.)

v13 also incorporates the latest SSL cert changes, so it's just a
single patch now. Tests now cover the CN and DN clientname modes. I
have not changed the log capture method yet; I'll take a look at it
next.

--Jacob

Attachments:

v13-0001-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v13-0001-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 48cfecfb71ac276bede9abe3e374ebee8f428117 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v13] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 137 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 ++
 src/test/authentication/t/001_password.pl |  77 +++++++++++-
 src/test/kerberos/t/001_auth.pl           |  36 ++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  67 ++++++++++-
 src/test/ssl/t/002_scram.pl               |  21 +++-
 10 files changed, 386 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ddc6d789d8..a322729098 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6678,7 +6678,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dcd02a94c3 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into the TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity. Set it now, rather than waiting
+	 * for check_usermap below, because authentication has already succeeded and
+	 * we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity. Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file to
+	 * reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success! Store the identity and check the usermap. (Setting the
+		 * authenticated identity is done before checking the usermap, because
+		 * at this point, authentication has succeeded.)
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity. Set it before calling check_usermap, because authentication has
+	 * already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,31 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization. Set it
+		 * now, rather than waiting for check_usermap below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (this would mean that peer_cn is being used
+			 * but somehow we couldn't fill in peer_dn at the same time). This
+			 * probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3116,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..312edd7fd0 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..adeb3bce33 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 21;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,101 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2);
+$ENV{"PGPASSWORD"} = 'pass';
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# Make sure authenticated identity isn't set if the password is wrong.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..271e56824b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +206,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index adaa1b4e9b..9fac0689ee 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 108;
 }
 
 #### Some configuration
@@ -454,6 +454,9 @@ test_connect_fails(
 );
 
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # correct client cert using whole DN
 my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
@@ -463,6 +466,16 @@ test_connect_ok(
 	"certificate authorization succeeds with DN mapping"
 );
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
 
@@ -472,6 +485,9 @@ test_connect_ok(
 	"certificate authorization succeeds with DN regex mapping"
 );
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # same thing but using explicit CN
 $dn_connstr = "$common_connstr dbname=certdb_cn";
 
@@ -481,6 +497,16 @@ test_connect_ok(
 	"certificate authorization succeeds with CN mapping"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"full authenticated DNs are logged even in CN mapping mode");
+
+$node->start;
+
 
 
 TODO:
@@ -539,6 +565,9 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -547,6 +576,17 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
+$log = $node->rotate_logfile();
+$node->restart;
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -554,12 +594,25 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
+$node->start;
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
+$log = $node->rotate_logfile();
+$node->restart;
+
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -581,6 +634,18 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# None of the above connections to verifydb should have resulted in
+# authentication.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
+$node->start;
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..c15b9c405b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,25 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
+$node->start;
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#58Jacob Champion
pchampion@vmware.com
In reply to: Jacob Champion (#57)
2 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-03-30 at 17:06 +0000, Jacob Champion wrote:

Would it be acceptable to adjust the tests for live rotation using the
logging collector, rather than a full restart? It would unfortunately
mean that we have to somehow wait for the rotation to complete, since
that's asynchronous.

I wasn't able to make live rotation work in a sane way. So, v14 tries
to thread the needle with a riff on your earlier idea:

If you want to keep this information around
for debugging, I guess that we could just print the contents of the
backend logs to regress_log_001_password instead? This could be done
with a simple wrapper routine that prints the past contents of the log
file before truncating them.

Rather than putting Postgres log data into the Perl logs, I rotate the
logs exactly once at the beginning -- so that there's an
old 001_ssltests_primary.log, and a new 001_ssltests_primary_1.log --
and then every time we truncate the logfile, I shuffle the bits from
the new logfile into the old one. So no one has to learn to find the
log entries in a new place, we don't get an explosion of rotated logs,
we don't lose the log data, we don't match incorrect portions of the
logs, and we only pay the restart price once. This is wrapped into a
small Perl module, LogCollector.

WDYT?

--Jacob

Attachments:

since-v13.diff.txttext/plain; name=since-v13.diff.txtDownload
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 9fac0689ee..b5fb15f794 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -10,6 +10,7 @@ use FindBin;
 use lib $FindBin::RealBin;
 
 use SSLServer;
+use LogCollector;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
@@ -454,8 +455,7 @@ test_connect_fails(
 );
 
 
-my $log = $node->rotate_logfile();
-$node->restart;
+my $log = LogCollector->new($node);
 
 # correct client cert using whole DN
 my $dn_connstr = "$common_connstr dbname=certdb_dn";
@@ -466,16 +466,10 @@ test_connect_ok(
 	"certificate authorization succeeds with DN mapping"
 );
 
-$node->stop('fast');
-my $log_contents = slurp_file($log);
-
-like(
-	$log_contents,
+$log->like(
 	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
 	"authenticated DNs are logged");
 
-$node->start;
-
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
 
@@ -485,8 +479,7 @@ test_connect_ok(
 	"certificate authorization succeeds with DN regex mapping"
 );
 
-$log = $node->rotate_logfile();
-$node->restart;
+$log->rotate;
 
 # same thing but using explicit CN
 $dn_connstr = "$common_connstr dbname=certdb_cn";
@@ -497,17 +490,10 @@ test_connect_ok(
 	"certificate authorization succeeds with CN mapping"
 );
 
-$node->stop('fast');
-$log_contents = slurp_file($log);
-
-like(
-	$log_contents,
+$log->like(
 	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
 	"full authenticated DNs are logged even in CN mapping mode");
 
-$node->start;
-
-
 
 TODO:
 {
@@ -565,8 +551,7 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
-$log = $node->rotate_logfile();
-$node->restart;
+$log->rotate;
 
 # client cert belonging to another user
 test_connect_fails(
@@ -576,17 +561,10 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
-$node->stop('fast');
-$log_contents = slurp_file($log);
-
-like(
-	$log_contents,
+$log->like(
 	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
 	"cert authentication succeeds even if authorization fails");
 
-$log = $node->rotate_logfile();
-$node->restart;
-
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -594,25 +572,16 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
-$node->stop('fast');
-$log_contents = slurp_file($log);
-
-unlike(
-	$log_contents,
+$log->unlike(
 	qr/connection authenticated:/,
 	"revoked certs do not authenticate users");
 
-$node->start;
-
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
 $common_connstr =
   "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
 
-$log = $node->rotate_logfile();
-$node->restart;
-
 test_connect_ok(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
@@ -634,18 +603,12 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
-$node->stop('fast');
-$log_contents = slurp_file($log);
-
 # None of the above connections to verifydb should have resulted in
 # authentication.
-unlike(
-	$log_contents,
+$log->unlike(
 	qr/connection authenticated:/,
 	"trust auth method does not set authenticated identity");
 
-$node->start;
-
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index c15b9c405b..81aaa0b02d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -12,6 +12,7 @@ use FindBin;
 use lib $FindBin::RealBin;
 
 use SSLServer;
+use LogCollector;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
@@ -102,8 +103,7 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
-my $log = $node->rotate_logfile();
-$node->restart;
+my $log = LogCollector->new($node);
 
 # Certificate verification at the connection level should still work fine.
 test_connect_ok(
@@ -111,16 +111,10 @@ test_connect_ok(
 	"dbname=verifydb user=ssltestuser channel_binding=require",
 	"SCRAM with clientcert=verify-full and channel_binding=require");
 
-$node->stop('fast');
-my $log_contents = slurp_file($log);
-
-like(
-	$log_contents,
+$log->like(
 	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
 	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
 
-$node->start;
-
 # clean up
 unlink($client_tmp_key);
 
diff --git a/src/test/ssl/t/LogCollector.pm b/src/test/ssl/t/LogCollector.pm
new file mode 100644
index 0000000000..ce0c4063f2
--- /dev/null
+++ b/src/test/ssl/t/LogCollector.pm
@@ -0,0 +1,84 @@
+# This module defines a class to help match log entries to specific connections
+# or queries.
+
+package LogCollector;
+
+use strict;
+use warnings;
+use TestLib;
+use Test::More ();
+
+use Exporter 'import';
+our @EXPORT = qw(
+  start_log_capture
+);
+
+# Wrap a new collector around $node.
+#
+# The node's current logfile will be saved, then rotated, so that future log
+# entries are written to a new logfile. Whenever the rotate() method is called
+# on the collector, all entries from the new logfile will be appended to the old
+# logfile, and then the new logfile will be truncated. This gives a clean slate
+# to the next tests.
+sub new
+{
+	my ($class, $node) = @_;
+
+	my $self = {
+		_node     => $node,
+		_orig_log => $node->logfile,
+		_log      => $node->rotate_logfile,
+	};
+	bless $self, $class;
+
+	$node->restart; # start writing to the new log
+	return $self;
+}
+
+# Returns the contents of the current (new) logfile. These will be written to
+# the old logfile when rotate() is called.
+sub contents
+{
+	my $self = shift;
+
+	return slurp_file($self->{_log});
+}
+
+# Writes the current contents of the new logfile to the old logfile, then
+# truncates the new logfile.
+sub rotate
+{
+	my $self = shift;
+	my $log_contents = $self->contents;
+
+	append_to_file($self->{_orig_log}, $log_contents);
+	truncate $self->{_log}, 0;
+}
+
+# Calls Test::More::like on the current logfile contents, then rotates the logs
+# so that future calls to like() or unlike() will not match the contents that
+# have already been checked.
+#
+# To avoid the auto-rotation behavior (e.g. if you want to perform multiple
+# checks on the same log contents), use contents() instead and perform a manual
+# rotate() after the checks are complete.
+sub like
+{
+	my ($self, $expected, $name) = @_;
+
+	Test::More::like($self->contents, $expected, $name);
+
+	$self->rotate;
+}
+
+# The opposite of like(), above.
+sub unlike
+{
+	my ($self, $expected, $name) = @_;
+
+	Test::More::unlike($self->contents, $expected, $name);
+
+	$self->rotate;
+}
+
+1;
v14-0001-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v14-0001-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 943f0f253cb9cad901e06f4e53d98c7d5db59795 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Wed, 3 Feb 2021 11:42:05 -0800
Subject: [PATCH v14] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines, and the SSL test suite has a new helper module to let tests
match log entries in isolation.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 137 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  12 ++
 src/test/authentication/t/001_password.pl |  77 +++++++++++-
 src/test/kerberos/t/001_auth.pl           |  36 ++++--
 src/test/ldap/t/001_auth.pl               |  28 ++++-
 src/test/ssl/t/001_ssltests.pl            |  30 ++++-
 src/test/ssl/t/002_scram.pl               |  15 ++-
 src/test/ssl/t/LogCollector.pm            |  84 +++++++++++++
 11 files changed, 427 insertions(+), 20 deletions(-)
 create mode 100644 src/test/ssl/t/LogCollector.pm

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ddc6d789d8..a322729098 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6678,7 +6678,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dcd02a94c3 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user. The provided string
+ * will be copied into the TopMemoryContext. The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this exactly once, as soon as the user is
+ * successfully authenticated, even if they have reason to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into the TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+			errmsg("connection authenticated: identity=\"%s\" method=%s "
+				   "(%s:%d)",
+				   port->authn_id, hba_authname(port), HbaFileName,
+				   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity. Set it now, rather than waiting
+	 * for check_usermap below, because authentication has already succeeded and
+	 * we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity. Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file to
+	 * reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success! Store the identity and check the usermap. (Setting the
+		 * authenticated identity is done before checking the usermap, because
+		 * at this point, authentication has succeeded.)
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity. Set it before calling check_usermap, because authentication has
+	 * already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,31 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization. Set it
+		 * now, rather than waiting for check_usermap below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * Unlikely to happen (this would mean that peer_cn is being used
+			 * but somehow we couldn't fill in peer_dn at the same time). This
+			 * probably indicates problems with the TLS backend.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3116,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..312edd7fd0 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,18 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity. The meaning of this identifier is dependent on
+	 * hba->auth_method; it's the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a role. (It's
+	 * effectively the "SYSTEM-USERNAME" of a pg_ident usermap -- though the
+	 * exact string in use may be different, depending on pg_hba options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char   *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..adeb3bce33 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 21;
 }
 
 
@@ -57,6 +57,7 @@ sub test_role
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -69,27 +70,101 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
+my $log = $node->rotate_logfile();
+$node->restart;
+
 # For "trust" method, all users should be able to connect.
 reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0);
 test_role($node, 'md5_role',   'password', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=password/,
+	"password method logs authenticated identity");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0);
 test_role($node, 'md5_role',   'scram-sha-256', 2);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/,
+	"scram-sha-256 method logs authenticated identity");
+
+unlike(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role"/,
+	"mismatched crypt methods do not result in authentication");
+
+$log = $node->rotate_logfile();
+$node->start;
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2);
+$ENV{"PGPASSWORD"} = 'pass';
+
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+# Make sure authenticated identity isn't set if the password is wrong.
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"SCRAM does not set authenticated identity with bad password");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0);
 test_role($node, 'md5_role',   'md5', 0);
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="md5_role" method=md5/,
+	"md5 method logs authenticated identity");
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="scram_role" method=md5/,
+	"md5 method logs authenticated identity for SCRAM users too");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
 reset_pg_hba($node, 'password');
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..271e56824b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,7 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, @expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +206,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +249,13 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +267,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +278,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +288,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +325,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,10 +335,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -346,10 +353,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +365,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +382,16 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access($node, 'test1', 'SELECT true', 2, '',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss"
+);
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..94f8850ca8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 24;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -173,6 +174,9 @@ sub test_access
 
 note "simple bind";
 
+# save log location for later tests
+my $log = $node->rotate_logfile();
+
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
 	qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net"}
@@ -184,9 +188,31 @@ test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP');
 test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password');
+
+$node->stop('fast');
+my $log_contents = slurp_file($log);
+
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"unauthenticated DNs are not logged");
+
+$log = $node->rotate_logfile();
+$node->start;
+
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds');
 
+$node->stop('fast');
+$log_contents = slurp_file($log);
+
+like(
+	$log_contents,
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/,
+	"authenticated DNs are logged");
+
+$node->start;
+
 note "search+bind";
 
 unlink($node->data_dir . '/pg_hba.conf');
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index adaa1b4e9b..b5fb15f794 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -10,6 +10,7 @@ use FindBin;
 use lib $FindBin::RealBin;
 
 use SSLServer;
+use LogCollector;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
@@ -17,7 +18,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 108;
 }
 
 #### Some configuration
@@ -454,6 +455,8 @@ test_connect_fails(
 );
 
 
+my $log = LogCollector->new($node);
+
 # correct client cert using whole DN
 my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
@@ -463,6 +466,10 @@ test_connect_ok(
 	"certificate authorization succeeds with DN mapping"
 );
 
+$log->like(
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"authenticated DNs are logged");
+
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
 
@@ -472,6 +479,8 @@ test_connect_ok(
 	"certificate authorization succeeds with DN regex mapping"
 );
 
+$log->rotate;
+
 # same thing but using explicit CN
 $dn_connstr = "$common_connstr dbname=certdb_cn";
 
@@ -481,6 +490,9 @@ test_connect_ok(
 	"certificate authorization succeeds with CN mapping"
 );
 
+$log->like(
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"full authenticated DNs are logged even in CN mapping mode");
 
 
 TODO:
@@ -539,6 +551,8 @@ SKIP:
 		"certificate authorization fails because of file permissions");
 }
 
+$log->rotate;
+
 # client cert belonging to another user
 test_connect_fails(
 	$common_connstr,
@@ -547,6 +561,10 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+$log->like(
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"cert authentication succeeds even if authorization fails");
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
@@ -554,6 +572,10 @@ test_connect_fails(
 	qr/SSL error/,
 	"certificate authorization fails with revoked client cert");
 
+$log->unlike(
+	qr/connection authenticated:/,
+	"revoked certs do not authenticate users");
+
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
 # fails, iff username doesn't match Common Name.
@@ -581,6 +603,12 @@ test_connect_ok(
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+# None of the above connections to verifydb should have resulted in
+# authentication.
+$log->unlike(
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity");
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..81aaa0b02d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -12,6 +12,7 @@ use FindBin;
 use lib $FindBin::RealBin;
 
 use SSLServer;
+use LogCollector;
 
 if ($ENV{with_ssl} ne 'openssl')
 {
@@ -27,7 +28,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +103,18 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+my $log = LogCollector->new($node);
+
+# Certificate verification at the connection level should still work fine.
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+$log->like(
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity");
+
 # clean up
 unlink($client_tmp_key);
 
diff --git a/src/test/ssl/t/LogCollector.pm b/src/test/ssl/t/LogCollector.pm
new file mode 100644
index 0000000000..ce0c4063f2
--- /dev/null
+++ b/src/test/ssl/t/LogCollector.pm
@@ -0,0 +1,84 @@
+# This module defines a class to help match log entries to specific connections
+# or queries.
+
+package LogCollector;
+
+use strict;
+use warnings;
+use TestLib;
+use Test::More ();
+
+use Exporter 'import';
+our @EXPORT = qw(
+  start_log_capture
+);
+
+# Wrap a new collector around $node.
+#
+# The node's current logfile will be saved, then rotated, so that future log
+# entries are written to a new logfile. Whenever the rotate() method is called
+# on the collector, all entries from the new logfile will be appended to the old
+# logfile, and then the new logfile will be truncated. This gives a clean slate
+# to the next tests.
+sub new
+{
+	my ($class, $node) = @_;
+
+	my $self = {
+		_node     => $node,
+		_orig_log => $node->logfile,
+		_log      => $node->rotate_logfile,
+	};
+	bless $self, $class;
+
+	$node->restart; # start writing to the new log
+	return $self;
+}
+
+# Returns the contents of the current (new) logfile. These will be written to
+# the old logfile when rotate() is called.
+sub contents
+{
+	my $self = shift;
+
+	return slurp_file($self->{_log});
+}
+
+# Writes the current contents of the new logfile to the old logfile, then
+# truncates the new logfile.
+sub rotate
+{
+	my $self = shift;
+	my $log_contents = $self->contents;
+
+	append_to_file($self->{_orig_log}, $log_contents);
+	truncate $self->{_log}, 0;
+}
+
+# Calls Test::More::like on the current logfile contents, then rotates the logs
+# so that future calls to like() or unlike() will not match the contents that
+# have already been checked.
+#
+# To avoid the auto-rotation behavior (e.g. if you want to perform multiple
+# checks on the same log contents), use contents() instead and perform a manual
+# rotate() after the checks are complete.
+sub like
+{
+	my ($self, $expected, $name) = @_;
+
+	Test::More::like($self->contents, $expected, $name);
+
+	$self->rotate;
+}
+
+# The opposite of like(), above.
+sub unlike
+{
+	my ($self, $expected, $name) = @_;
+
+	Test::More::unlike($self->contents, $expected, $name);
+
+	$self->rotate;
+}
+
+1;
-- 
2.25.1

#59Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#57)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, Mar 30, 2021 at 05:06:51PM +0000, Jacob Champion wrote:

The key there is "if there is a failure" -- false positives need to be
debugged too. Tests I've worked with recently for the NSS work were
succeeding for the wrong reasons. Overly generic logfile matches ("SSL
error"), for example.

Indeed, so that's a test stability issue. It looks like a good idea
to make those tests more picky with the sub-errors they expect. I see
most "certificate verify failed" a lot, two "sslv3 alert certificate
revoked" and one "tlsv1 alert unknown ca" with 1.1.1, but it is not
something that this patch has to address IMO.

modules/ssl_passphrase_callback/t/001_testfunc.pl is where I pulled
this pattern from.

I see. For this case, I see no issue as the input caught is from
_PG_init() so that seems better than a wait on the logs generated.

Does unilateral log truncation play any nicer with parallel test runs?
I understand not wanting to make an existing problem worse, but it
doesn't seem like the existing tests were written for general
parallelism.

TAP tests running in parallel use their own isolated backend, wiht
dedicated paths and ports.

Would it be acceptable to adjust the tests for live rotation using the
logging collector, rather than a full restart? It would unfortunately
mean that we have to somehow wait for the rotation to complete, since
that's asynchronous.

(Speaking of asynchronous: how does the existing check-and-truncate
code make sure that the log entries it's looking for have been flushed
to disk? Shutting down the server guarantees it.)

stderr redirection looks to be working pretty well with
issues_sql_like().

I took a stab at this in v13: "Causes each attempted connection to the
server to be logged, as well as successful completion of both client
authentication (if necessary) and authorization." (IMO any further in-
depth explanation of authn/z and user mapping probably belongs in the
auth method documentation, and this patch doesn't change any authn/z
behavior.)

v13 also incorporates the latest SSL cert changes, so it's just a
single patch now. Tests now cover the CN and DN clientname modes. I
have not changed the log capture method yet; I'll take a look at it
next.

Thanks, I am looking into that and I am digging into the code now.
--
Michael

#60Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#58)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, Mar 30, 2021 at 11:15:48PM +0000, Jacob Champion wrote:

Rather than putting Postgres log data into the Perl logs, I rotate the
logs exactly once at the beginning -- so that there's an
old 001_ssltests_primary.log, and a new 001_ssltests_primary_1.log --
and then every time we truncate the logfile, I shuffle the bits from
the new logfile into the old one. So no one has to learn to find the
log entries in a new place, we don't get an explosion of rotated logs,
we don't lose the log data, we don't match incorrect portions of the
logs, and we only pay the restart price once. This is wrapped into a
small Perl module, LogCollector.

Hmm. I have dug today into that and I am really not convinced that
this is necessary, as a connection attempt combined with the output
sent to stderr gives you the stability needed. If we were to have
anything like that, perhaps a sub-class of PostgresNode would be
adapted instead, with an internal log integration.

After thinking about it, the new wording in config.sgml looks fine
as-is.

Anyway, I have not been able to convince myself that we need those
slowdowns and that many server restarts as there is no
reload-dependent timing here, and things have been stable on
everything I have tested (including a slow RPI). I have found a
couple of things that can be simplified in the tests:
- In src/test/authentication/, except for the trust method where there
is no auth ID, all the other tests wrap a like() if $res == 0, or
unlike() otherwise. I think that it is cleaner to make the matching
pattern an argument of test_role(), and adapt the tests to that.
- src/test/ldap/ can also embed a single logic within test_access().
- src/test/ssl/ is a different beast, but I think that there is more
refactoring possible here in parallel of the recent work I have sent
to have equivalents of test_connect_ok() and test_connect_fails() in
PostgresNode.pm. For now, I think that we should just live in this
set with a small routine able to check for pattern matches in the
logs.

Attached is an updated patch, with a couple of comments tweaks, the
reworked tests and an indentation done.
--
Michael

Attachments:

v15-0001-Log-authenticated-identity-from-all-auth-backend.patchtext/x-diff; charset=us-asciiDownload
From 0f308c892cb4bb120572e996b1eb612682134c03 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 31 Mar 2021 16:05:33 +0900
Subject: [PATCH v15] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

The Kerberos test suite has been modified to let tests match multiple
log lines.
---
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/backend/libpq/auth.c                  | 136 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/test/authentication/t/001_password.pl |  76 +++++++++---
 src/test/kerberos/t/001_auth.pl           |  47 ++++++--
 src/test/ldap/t/001_auth.pl               |  42 +++++--
 src/test/ssl/t/001_ssltests.pl            |  61 +++++++++-
 src/test/ssl/t/002_scram.pl               |  18 ++-
 doc/src/sgml/config.sgml                  |   3 +-
 10 files changed, 381 insertions(+), 40 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dee056b0d6 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
-
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3115,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..f75d555ae5 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 22;
 }
 
 
@@ -38,25 +38,50 @@ sub reset_pg_hba
 # Test access for a single role, useful to wrap all tests into one.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
-	my $status_string = 'failed';
+	my $node             = shift;
+	my $role             = shift;
+	my $method           = shift;
+	my $expected_res     = shift;
+	my $expected_log_msg = shift;
+	my $status_string    = 'failed';
 
 	$status_string = 'success' if ($expected_res eq 0);
 
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	is($res, $expected_res,
-		"authentication $status_string for method $method, role $role");
+	my $res =
+	  $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
+	my $testname =
+	  "authentication $status_string for method $method, role $role";
+	is($res, $expected_res, $testname);
+
+	# Check if any log generated by authentication matches or not.
+	if ($expected_log_msg ne '')
+	{
+		my $log_contents = slurp_file($node->logfile);
+		if ($res == 0)
+		{
+			like($log_contents, $expected_log_msg,
+				"$testname: logs matching");
+		}
+		else
+		{
+			unlike($log_contents, $expected_log_msg,
+				"$testname: logs not matching");
+		}
+
+		# Clean up any existing contents in the node's log file so as
+		# future tests don't step on each other's generated contents.
+		truncate $node->logfile, 0;
+	}
+
 	return;
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -74,21 +99,42 @@ reset_pg_hba($node, 'trust');
 test_role($node, 'scram_role', 'trust', 0);
 test_role($node, 'md5_role',   'trust', 0);
 
+# Particular case here, the logs should have generated nothing related
+# to authenticated users.
+my $log_contents = slurp_file($node->logfile);
+unlike(
+	$log_contents,
+	qr/connection authenticated:/,
+	"trust method does not authenticate users");
+truncate $node->logfile, 0;
+
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	qr/connection authenticated: identity="scram_role" method=password/);
+test_role($node, 'md5_role', 'password', 0,
+	qr/connection authenticated: identity="md5_role" method=password/);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role($node, 'scram_role', 'scram-sha-256', 0,
+	qr/connection authenticated: identity="scram_role" method=scram-sha-256/);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	qr/connection authenticated: identity="md5_role"/);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	qr/connection authenticated:/);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	qr/connection authenticated: identity="scram_role" method=md5/);
+test_role($node, 'md5_role', 'md5', 0,
+	qr/connection authenticated: identity="md5_role" method=md5/);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..d970cf3186 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 38;
 }
 else
 {
@@ -182,7 +182,8 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
+		@expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my ($res, $stdoutres, $stderrres) = $node->psql(
@@ -206,8 +207,8 @@ sub test_access
 		is($res, $expected_res, $test_name);
 	}
 
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
+	# Verify specified log messages are logged in the log file.
+	while (my $expect_log_msg = shift @expect_log_msgs)
 	{
 		my $first_logfile = slurp_file($node->logfile);
 
@@ -249,11 +250,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -265,6 +274,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -275,6 +285,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -284,6 +295,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -320,6 +332,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -329,6 +342,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
@@ -346,10 +360,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -357,6 +372,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -373,5 +389,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..e98c8fe965 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 25;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,12 +163,32 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, $expected_log_msg) = @_;
 
 	my $res =
 	  $node->psql('postgres', undef,
 				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
 	is($res, $expected_res, $test_name);
+
+	# Check if any log generated by authentication matches or not.
+	if ($expected_log_msg ne '')
+	{
+		my $log_contents = slurp_file($node->logfile);
+		if ($res == 0)
+		{
+			like($log_contents, $expected_log_msg,
+				"$test_name: logs matching");
+		}
+		else
+		{
+			unlike($log_contents, $expected_log_msg,
+				"$test_name: logs not matching");
+		}
+
+		# Clean up any existing contents in the node's log file so as
+		# future tests don't step on each other's generated contents.
+		truncate $node->logfile, 0;
+	}
 	return;
 }
 
@@ -180,12 +201,19 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
-test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
-test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+test_access(
+	$node, 'test0', 2,
+	'simple bind authentication fails if user not found in LDAP',
+	qr/connection authenticated:/);
+test_access(
+	$node, 'test1', 2,
+	'simple bind authentication fails with wrong password',
+	qr/connection authenticated:/);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access($node, 'test1', 0, 'simple bind authentication succeeds',
+	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
+);
 
 note "search+bind";
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index adaa1b4e9b..80be22fb95 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,33 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 108;
+}
+
+# Check for matching patterns in the logs of a node.  This should
+# be used after a connection attempt.  $res indicates if the pattern
+# should match or not.
+sub check_auth_logs
+{
+	my $node             = shift;
+	my $expected_log_msg = shift;
+	my $test_name        = shift;
+	my $res              = shift;
+
+	my $log_contents = slurp_file($node->logfile);
+
+	if ($res == 0)
+	{
+		like($log_contents, $expected_log_msg, $test_name);
+	}
+	else
+	{
+		unlike($log_contents, $expected_log_msg, $test_name);
+	}
+
+	# Clean up any existing contents in the node's log file so as
+	# future tests don't step on each other's generated contents.
+	truncate $node->logfile, 0;
 }
 
 #### Some configuration
@@ -463,6 +489,12 @@ test_connect_ok(
 	"certificate authorization succeeds with DN mapping"
 );
 
+check_auth_logs(
+	$node,
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"certificate authorization with DN mapping logged",
+	0);
+
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
 
@@ -481,6 +513,11 @@ test_connect_ok(
 	"certificate authorization succeeds with CN mapping"
 );
 
+check_auth_logs(
+	$node,
+	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
+	"certificate authorization with CN mapping logged with DN info",
+	0);
 
 
 TODO:
@@ -547,12 +584,23 @@ test_connect_fails(
 	"certificate authorization fails with client cert belonging to another user"
 );
 
+check_auth_logs(
+	$node,
+	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
+	"certificate authorization logged even on failure", 0);
+
 # revoked client cert
 test_connect_fails(
 	$common_connstr,
 	"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	qr/SSL error/,
-	"certificate authorization fails with revoked client cert");
+	"certificate authorization fails with revoked client cert",
+	0);
+
+check_auth_logs(
+	$node,
+	qr/connection authenticated:/,
+	"certificate authentication with revoked client cert not logged", 1);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -575,12 +623,21 @@ test_connect_fails(
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
+# Clean up once the logs to have a clean check state.
+truncate $node->logfile, 0;
 test_connect_ok(
 	$common_connstr,
 	"user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
 );
 
+# None of the above connections to verifydb should have resulted in
+# authentication.
+check_auth_logs(
+	$node,
+	qr/connection authenticated:/,
+	"trust auth method does not set authenticated identity", 1);
+
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 410b9e910d..0d0f4fe674 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,22 @@ test_connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+# Certificate verification at the connection level should still work fine.
+# Truncate once the logs, to ensure that we check what is generated for this
+# specific connection.
+truncate $node->logfile, 0;
+test_connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
+	"dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require");
+
+my $log_contents = slurp_file($node->logfile);
+like(
+	$log_contents,
+	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
+	"SCRAM with clientcert=verify-full sets the username as the authenticated identity"
+);
+
 # clean up
 unlink($client_tmp_key);
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ddc6d789d8..a322729098 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6678,7 +6678,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
-- 
2.31.0

#61Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#60)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, Mar 31, 2021 at 04:42:32PM +0900, Michael Paquier wrote:

Attached is an updated patch, with a couple of comments tweaks, the
reworked tests and an indentation done.

Jacob has mentioned me that v15 has some false positives in the SSL
tests, as we may catch in the backend logs patterns that come from
a previous test. We should really make that stuff more robust by
design, or it will bite hard with some bugs remaining undetected while
the tests pass. This stuff can take advantage of 0d1a3343, and I
think that we should make the kerberos, ldap, authentication and SSL
test suites just use connect_ok() and connect_fails() from
PostgresNode.pm. They just need to be extended a bit with a new
argument for the log pattern check. This has the advantage to
centralize in a single code path the log file truncation (or some log
file rotation if the logging collector is used).
--
Michael

#62Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#61)
4 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Thu, 2021-04-01 at 10:21 +0900, Michael Paquier wrote:

This stuff can take advantage of 0d1a3343, and I
think that we should make the kerberos, ldap, authentication and SSL
test suites just use connect_ok() and connect_fails() from
PostgresNode.pm. They just need to be extended a bit with a new
argument for the log pattern check.

v16, attached, migrates all tests in those suites to connect_ok/fails
(in the first two patches), and also adds the log pattern matching (in
the final feature patch).

A since-v15 diff is attached, but it should be viewed with suspicion
since I've rebased on top of the new SSL tests at the same time.

--Jacob

Attachments:

since-v15.diff.txttext/plain; name=since-v15.diff.txtDownload
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index f75d555ae5..f2cd7377d1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 22;
+	plan tests => 23;
 }
 
 
@@ -35,47 +35,28 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node             = shift;
-	my $role             = shift;
-	my $method           = shift;
-	my $expected_res     = shift;
-	my $expected_log_msg = shift;
-	my $status_string    = 'failed';
-
+	my ($node, $role, $method, $expected_res, %params) = @_;
+	my $status_string = 'failed';
 	$status_string = 'success' if ($expected_res eq 0);
 
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	my $res =
-	  $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	my $testname =
-	  "authentication $status_string for method $method, role $role";
-	is($res, $expected_res, $testname);
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for method $method, role $role";
 
-	# Check if any log generated by authentication matches or not.
-	if ($expected_log_msg ne '')
+	if ($expected_res eq 0)
 	{
-		my $log_contents = slurp_file($node->logfile);
-		if ($res == 0)
-		{
-			like($log_contents, $expected_log_msg,
-				"$testname: logs matching");
-		}
-		else
-		{
-			unlike($log_contents, $expected_log_msg,
-				"$testname: logs not matching");
-		}
-
-		# Clean up any existing contents in the node's log file so as
-		# future tests don't step on each other's generated contents.
-		truncate $node->logfile, 0;
+		$node->connect_ok($connstr, $testname, %params, extra_params => [ '-w' ]);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $testname, %params, extra_params => [ '-w' ]);
 	}
-
-	return;
 }
 
 # Initialize primary node
@@ -94,47 +75,41 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
-
-# Particular case here, the logs should have generated nothing related
-# to authenticated users.
-my $log_contents = slurp_file($node->logfile);
-unlike(
-	$log_contents,
-	qr/connection authenticated:/,
-	"trust method does not authenticate users");
-truncate $node->logfile, 0;
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_role($node, 'md5_role',   'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
 test_role($node, 'scram_role', 'password', 0,
-	qr/connection authenticated: identity="scram_role" method=password/);
+	log_like => [ qr/connection authenticated: identity="scram_role" method=password/ ]);
 test_role($node, 'md5_role', 'password', 0,
-	qr/connection authenticated: identity="md5_role" method=password/);
+	log_like => [ qr/connection authenticated: identity="md5_role" method=password/ ]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
 test_role($node, 'scram_role', 'scram-sha-256', 0,
-	qr/connection authenticated: identity="scram_role" method=scram-sha-256/);
+	log_like => [ qr/connection authenticated: identity="scram_role" method=scram-sha-256/ ]);
 test_role($node, 'md5_role', 'scram-sha-256', 2,
-	qr/connection authenticated: identity="md5_role"/);
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
 test_role($node, 'scram_role', 'scram-sha-256', 2,
-	qr/connection authenticated:/);
+	log_unlike => [ qr/connection authenticated:/ ]);
 $ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
 test_role($node, 'scram_role', 'md5', 0,
-	qr/connection authenticated: identity="scram_role" method=md5/);
+	log_like => [ qr/connection authenticated: identity="scram_role" method=md5/ ]);
 test_role($node, 'md5_role', 'md5', 0,
-	qr/connection authenticated: identity="md5_role" method=md5/);
+	log_like => [ qr/connection authenticated: identity="md5_role" method=md5/ ]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index 0aaab090ec..33e305e16c 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -41,12 +41,19 @@ sub test_login
 
 	$status_string = 'success' if ($expected_res eq 0);
 
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for role $role with password $password";
+
 	$ENV{"PGPASSWORD"} = $password;
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
-	is($res, $expected_res,
-		"authentication $status_string for role $role with password $password"
-	);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $testname);
+	}
 }
 
 # Initialize primary node. Force UTF-8 encoding, so that we can use non-ASCII
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index d970cf3186..5ca2eb0694 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 38;
+	plan tests => 46;
 }
 else
 {
@@ -186,40 +186,35 @@ sub test_access
 		@expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
-
-	# If we get a query result back, it should be true.
-	if ($res == $expected_res and $res eq 0)
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	my %params = (
+		query => $query,
+		extra_params => [ '-XAt' ],
+	);
+
+	if (@expect_log_msgs)
 	{
-		is($stdoutres, "t", $test_name);
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		$params{log_like} = \@regexes;
+	}
+
+	if ($expected_res eq 0)
+	{
+		my $stdoutres;
+		$params{stdout} = \$stdoutres;
+
+		$node->connect_ok($connstr, $test_name, %params);
+
+		is($stdoutres, "t", "$test_name: query result is true");
 	}
 	else
 	{
-		is($res, $expected_res, $test_name);
+		$node->connect_fails($connstr, undef, $test_name, %params);
 	}
-
-	# Verify specified log messages are logged in the log file.
-	while (my $expect_log_msg = shift @expect_log_msgs)
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -228,18 +223,18 @@ sub test_query
 	my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	my ($stdoutres, $stderrres);
+
+	$node->connect_ok($connstr, $test_name,
+		query => $query,
+		extra_params => [ '-XAt' ],
+		stdout => \$stdoutres,
+		stderr => \$stderrres,
+	);
 
-	is($res, 0, $test_name);
 	like($stdoutres, $expected, $test_name);
 	is($stderrres, "", $test_name);
 	return;
@@ -346,7 +341,7 @@ test_access(
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index e98c8fe965..bf3cf4a658 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -163,33 +163,20 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name, $expected_log_msg) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
+	my $connstr = "user=$role";
 
-	my $res =
-	  $node->psql('postgres', undef,
-				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
-	is($res, $expected_res, $test_name);
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	# Check if any log generated by authentication matches or not.
-	if ($expected_log_msg ne '')
+	if ($expected_res eq 0)
 	{
-		my $log_contents = slurp_file($node->logfile);
-		if ($res == 0)
-		{
-			like($log_contents, $expected_log_msg,
-				"$test_name: logs matching");
-		}
-		else
-		{
-			unlike($log_contents, $expected_log_msg,
-				"$test_name: logs not matching");
-		}
-
-		# Clean up any existing contents in the node's log file so as
-		# future tests don't step on each other's generated contents.
-		truncate $node->logfile, 0;
+		$node->connect_ok($connstr, $test_name, %params);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $test_name, %params);
 	}
-	return;
 }
 
 note "simple bind";
@@ -201,18 +188,16 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
-test_access(
-	$node, 'test0', 2,
+test_access($node, 'test0', 2,
 	'simple bind authentication fails if user not found in LDAP',
-	qr/connection authenticated:/);
-test_access(
-	$node, 'test1', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_access($node, 'test1', 2,
 	'simple bind authentication fails with wrong password',
-	qr/connection authenticated:/);
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 $ENV{"PGPASSWORD"} = 'secret1';
 test_access($node, 'test1', 0, 'simple bind authentication succeeds',
-	qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
 );
 
 note "search+bind";
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index d6e10544bb..2afe225267 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1860,47 +1860,157 @@ sub interactive_psql
 
 =pod
 
-=item $node->connect_ok($connstr, $test_name)
+=item $node->connect_ok($connstr, $test_name [, %params ])
 
 Attempt a connection with a custom connection string.  This is expected
 to succeed.
 
+=over
+
+=item query => 'SELECT some_query;'
+
+By default, a query based on the connection string is performed after the
+connection succeeds, to identify it in the server logs. If B<query> is set, it
+will be used instead. This can be used with the B<stdout> parameter (see
+C<psql()>) to capture a query result.
+
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+to match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must NOT match against the server log. They will be passed to
+C<Test::More::unlike()>.
+
+=back
+
+Any additional named parameters are passed to C<psql()> as-is.
+
 =cut
 
 sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $test_name) = @_;
+	my ($self, $connstr, $test_name, %params) = @_;
+
+	my $query = "SELECT \$\$connected with $connstr\$\$";
+	if (exists $params{query})
+	{
+		$query = delete $params{query};
+	}
+
+	my (@log_like, @log_unlike);
+	if (exists $params{log_like})
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (exists $params{log_unlike})
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret,  $stdout,  $stderr)    = $self->psql(
 		'postgres',
-		"SELECT \$\$connected with $connstr\$\$",
+		$query,
+		%params,
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
-	ok($ret == 0, $test_name);
+	is($ret, 0, $test_name);
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
 
-=item $node->connect_fails($connstr, $expected_stderr, $test_name)
+=item $node->connect_fails($connstr, $expected_stderr, $test_name [, %params ])
 
 Attempt a connection with a custom connection string.  This is expected
 to fail with a message that matches the regular expression
 $expected_stderr.
 
+=over
+
+=item log_like => [ qr/required message/ ]
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+See C<connect_ok()>, above.
+
+=back
+
+Any additional named parameters are passed to C<psql()>.
+
 =cut
 
 sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $expected_stderr, $test_name) = @_;
+	my ($self, $connstr, $expected_stderr, $test_name, %params) = @_;
+
+	my (@log_like, @log_unlike);
+	if (exists $params{log_like})
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (exists $params{log_unlike})
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		%params,
 		connstr => "$connstr");
 
-	ok($ret != 0, $test_name);
-	like($stderr, $expected_stderr, "$test_name: matches");
+	isnt($ret, 0, $test_name);
+
+	if (defined $expected_stderr)
+	{
+		like($stderr, $expected_stderr, "$test_name: stderr matches");
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index e20aff6b55..943703fc44 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,33 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 108;
-}
-
-# Check for matching patterns in the logs of a node.  This should
-# be used after a connection attempt.  $res indicates if the pattern
-# should match or not.
-sub check_auth_logs
-{
-	my $node             = shift;
-	my $expected_log_msg = shift;
-	my $test_name        = shift;
-	my $res              = shift;
-
-	my $log_contents = slurp_file($node->logfile);
-
-	if ($res == 0)
-	{
-		like($log_contents, $expected_log_msg, $test_name);
-	}
-	else
-	{
-		unlike($log_contents, $expected_log_msg, $test_name);
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -444,13 +418,8 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
- 
-check_auth_logs(
-	$node,
-	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
-	"certificate authorization with DN mapping logged",
-	0);
+	"certificate authorization succeeds with DN mapping",
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ]);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -464,13 +433,9 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
-
-check_auth_logs(
-	$node,
-	qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/,
-	"certificate authorization with CN mapping logged with DN info",
-	0);
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ]);
 
 
 
@@ -531,24 +496,19 @@ SKIP:
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	qr/certificate authentication failed for user "anotheruser"/,
-	"certificate authorization fails with client cert belonging to another user"
+	"certificate authorization fails with client cert belonging to another user",
+	# certificate authentication should be logged even on failure
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser" method=cert/ ],
 );
- 
-check_auth_logs(
-	$node,
-	qr/connection authenticated: identity="CN=ssltestuser" method=cert/,
-	"certificate authorization logged even on failure", 0);
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	qr/SSL error/,
-	"certificate authorization fails with revoked client cert");
-
-check_auth_logs(
-	$node,
-	qr/connection authenticated:/,
-	"certificate authentication with revoked client cert not logged", 1);
+	"certificate authorization fails with revoked client cert",
+	# revoked certificates should not authenticate the user
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -558,31 +518,28 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	qr/FATAL/,
-	"auth_option clientcert=verify-full fails with mismatching username and Common Name"
+	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
-# Clean up once the logs to have a clean check state.
-truncate $node->logfile, 0;
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
-# None of the above connections to verifydb should have resulted in
-# authentication.
-check_auth_logs(
-	$node,
-	qr/connection authenticated:/,
-	"trust auth method does not set authenticated identity", 1);
-
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
 $common_connstr =
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 9f1b080d45..d98f6319fa 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -97,20 +97,10 @@ $node->connect_fails(
 	"Cert authentication and channel_binding=require");
 
 # Certificate verification at the connection level should still work fine.
-# Truncate once the logs, to ensure that we check what is generated for this
-# specific connection.
-truncate $node->logfile, 0;
-test_connect_ok(
-	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR",
-	"dbname=verifydb user=ssltestuser channel_binding=require",
-	"SCRAM with clientcert=verify-full and channel_binding=require");
-
-my $log_contents = slurp_file($node->logfile);
-like(
-	$log_contents,
-	qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/,
-	"SCRAM with clientcert=verify-full sets the username as the authenticated identity"
-);
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [ qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]);
 
 # clean up
 unlink($client_tmp_key);
v16-0001-test-continue-migration-to-node-connect_ok-fails.patchtext/x-patch; name=v16-0001-test-continue-migration-to-node-connect_ok-fails.patchDownload
From f2fbbe4b034b9f0d20fe14356052197b1a067763 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 09:41:38 -0700
Subject: [PATCH v16 1/3] test: continue migration to $node->connect_ok/fails

...with the authentication and ldap suites.

- connect_ok() and connect_fails() now take optional named parameters to
  pass to the psql() call, so that the authentication suite can disable
  password prompts.
- They also use is() and isnt() rather than ok(), to make failure modes
  slightly less opaque.
- ldap's test_access() gets a Test::Builder::Level addition, to make it
  easier to debug the location of failures.
---
 src/test/authentication/t/001_password.pl | 16 +++++++++++----
 src/test/authentication/t/002_saslprep.pl | 17 +++++++++++-----
 src/test/ldap/t/001_auth.pl               | 19 ++++++++++++------
 src/test/perl/PostgresNode.pm             | 24 ++++++++++++++---------
 4 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..92d9b25332 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -48,10 +48,18 @@ sub test_role
 
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	is($res, $expected_res,
-		"authentication $status_string for method $method, role $role");
-	return;
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for method $method, role $role";
+
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname, extra_params => [ '-w' ]);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $testname, extra_params => [ '-w' ]);
+	}
 }
 
 # Initialize primary node
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index 0aaab090ec..33e305e16c 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -41,12 +41,19 @@ sub test_login
 
 	$status_string = 'success' if ($expected_res eq 0);
 
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for role $role with password $password";
+
 	$ENV{"PGPASSWORD"} = $password;
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
-	is($res, $expected_res,
-		"authentication $status_string for role $role with password $password"
-	);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $testname);
+	}
 }
 
 # Initialize primary node. Force UTF-8 encoding, so that we can use non-ASCII
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..b20e487db8 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -163,12 +163,19 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $expected_res, $test_name) = @_;
-
-	my $res =
-	  $node->psql('postgres', undef,
-				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
-	is($res, $expected_res, $test_name);
-	return;
+	my $connstr = "user=$role";
+
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $test_name);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, undef, $test_name);
+	}
 }
 
 note "simple bind";
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index d6e10544bb..137736cce6 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1860,47 +1860,53 @@ sub interactive_psql
 
 =pod
 
-=item $node->connect_ok($connstr, $test_name)
+=item $node->connect_ok($connstr, $test_name [, %params ])
 
 Attempt a connection with a custom connection string.  This is expected
-to succeed.
+to succeed. Any additional named parameters are passed to psql().
 
 =cut
 
 sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $test_name) = @_;
+	my ($self, $connstr, $test_name, %params) = @_;
 	my ($ret,  $stdout,  $stderr)    = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		%params,
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
-	ok($ret == 0, $test_name);
+	is($ret, 0, $test_name);
 }
 
 =pod
 
-=item $node->connect_fails($connstr, $expected_stderr, $test_name)
+=item $node->connect_fails($connstr, $expected_stderr, $test_name [, %params ])
 
 Attempt a connection with a custom connection string.  This is expected
 to fail with a message that matches the regular expression
-$expected_stderr.
+$expected_stderr. Any additional named parameters are passed to psql().
 
 =cut
 
 sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $expected_stderr, $test_name) = @_;
+	my ($self, $connstr, $expected_stderr, $test_name, %params) = @_;
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		%params,
 		connstr => "$connstr");
 
-	ok($ret != 0, $test_name);
-	like($stderr, $expected_stderr, "$test_name: matches");
+	isnt($ret, 0, $test_name);
+
+	if (defined $expected_stderr)
+	{
+		like($stderr, $expected_stderr, "$test_name: matches");
+	}
 }
 
 =pod
-- 
2.25.1

v16-0002-test-kerberos-migrate-to-node-connect_ok.patchtext/x-patch; name=v16-0002-test-kerberos-migrate-to-node-connect_ok.patchDownload
From 1fd091cb6968783128b1e6cec8da9fa3eb751f73 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 11:45:53 -0700
Subject: [PATCH v16 2/3] test/kerberos: migrate to $node->connect_ok()

connect_ok() has been enhanced to take an optional query to override the
default.
---
 src/test/kerberos/t/001_auth.pl | 57 +++++++++++++++++----------------
 src/test/perl/PostgresNode.pm   | 24 ++++++++++++--
 2 files changed, 51 insertions(+), 30 deletions(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..401f52f70a 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 34;
 }
 else
 {
@@ -185,25 +185,26 @@ sub test_access
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
-
-	# If we get a query result back, it should be true.
-	if ($res == $expected_res and $res eq 0)
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	my %params = (
+		query => $query,
+		extra_params => [ '-XAt' ],
+	);
+
+	if ($expected_res eq 0)
 	{
-		is($stdoutres, "t", $test_name);
+		my $stdoutres;
+		$params{stdout} = \$stdoutres;
+
+		$node->connect_ok($connstr, $test_name, %params);
+
+		is($stdoutres, "t", "$test_name: query result is true");
 	}
 	else
 	{
-		is($res, $expected_res, $test_name);
+		$node->connect_fails($connstr, undef, $test_name, %params);
 	}
 
 	# Verify specified log message is logged in the log file.
@@ -227,18 +228,18 @@ sub test_query
 	my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
-
-	is($res, 0, $test_name);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	my ($stdoutres, $stderrres);
+
+	$node->connect_ok($connstr, $test_name,
+		query => $query,
+		extra_params => [ '-XAt' ],
+		stdout => \$stdoutres,
+		stderr => \$stderrres,
+	);
+
 	like($stdoutres, $expected, $test_name);
 	is($stderrres, "", $test_name);
 	return;
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index 137736cce6..eea8c0bef5 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1863,7 +1863,20 @@ sub interactive_psql
 =item $node->connect_ok($connstr, $test_name [, %params ])
 
 Attempt a connection with a custom connection string.  This is expected
-to succeed. Any additional named parameters are passed to psql().
+to succeed.
+
+=over
+
+=item query => 'SELECT some_query;'
+
+By default, a query based on the connection string is performed after the
+connection succeeds, to identify it in the server logs. If B<query> is set, it
+will be used instead. This can be used with the B<stdout> parameter (see
+C<psql()>) to capture a query result.
+
+=back
+
+Any additional named parameters are passed to C<psql()> as-is.
 
 =cut
 
@@ -1871,9 +1884,16 @@ sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $test_name, %params) = @_;
+
+	my $query = "SELECT \$\$connected with $connstr\$\$";
+	if (exists $params{query})
+	{
+		$query = delete $params{query};
+	}
+
 	my ($ret,  $stdout,  $stderr)    = $self->psql(
 		'postgres',
-		"SELECT \$\$connected with $connstr\$\$",
+		$query,
 		%params,
 		connstr       => "$connstr",
 		on_error_stop => 0);
-- 
2.25.1

v16-0003-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v16-0003-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 7727491c1fbf893da3bf6faab0aa377fe6f2e096 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 16:01:27 -0700
Subject: [PATCH v16 3/3] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

PostgresNode::connect_ok/fails() have been modified to let tests check
the logfiles for required or prohibited patterns, using the respective
log_like and log_unlike parameters.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 136 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/test/authentication/t/001_password.pl |  49 +++++---
 src/test/kerberos/t/001_auth.pl           |  67 +++++++----
 src/test/ldap/t/001_auth.pl               |  20 ++--
 src/test/perl/PostgresNode.pm             |  88 +++++++++++++-
 src/test/ssl/t/001_ssltests.pl            |  30 +++--
 src/test/ssl/t/002_scram.pl               |   8 +-
 11 files changed, 375 insertions(+), 64 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index d1e2e8c4c3..38344ced5b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6694,7 +6694,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dee056b0d6 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3115,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 92d9b25332..f2cd7377d1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 23;
 }
 
 
@@ -35,15 +35,12 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
+	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
-
 	$status_string = 'success' if ($expected_res eq 0);
 
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
@@ -53,18 +50,19 @@ sub test_role
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $testname, extra_params => [ '-w' ]);
+		$node->connect_ok($connstr, $testname, %params, extra_params => [ '-w' ]);
 	}
 	else
 	{
 		# Currently, we don't check the error message, just the code.
-		$node->connect_fails($connstr, undef, $testname, extra_params => [ '-w' ]);
+		$node->connect_fails($connstr, undef, $testname, %params, extra_params => [ '-w' ]);
 	}
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -77,26 +75,41 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_role($node, 'md5_role',   'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=password/ ]);
+test_role($node, 'md5_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=password/ ]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role($node, 'scram_role', 'scram-sha-256', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=scram-sha-256/ ]);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=md5/ ]);
+test_role($node, 'md5_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=md5/ ]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 401f52f70a..5ca2eb0694 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 46;
 }
 else
 {
@@ -182,7 +182,8 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
+		@expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my $connstr = $node->connstr('postgres') .
@@ -193,6 +194,14 @@ sub test_access
 		extra_params => [ '-XAt' ],
 	);
 
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		$params{log_like} = \@regexes;
+	}
+
 	if ($expected_res eq 0)
 	{
 		my $stdoutres;
@@ -206,20 +215,6 @@ sub test_access
 	{
 		$node->connect_fails($connstr, undef, $test_name, %params);
 	}
-
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -250,11 +245,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -266,6 +269,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -276,6 +280,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -285,6 +290,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -321,6 +327,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -330,10 +337,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -347,10 +355,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -358,6 +367,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -374,5 +384,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index b20e487db8..bf3cf4a658 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 25;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,19 +163,19 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $test_name);
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
 		# Currently, we don't check the error message, just the code.
-		$node->connect_fails($connstr, undef, $test_name);
+		$node->connect_fails($connstr, undef, $test_name, %params);
 	}
 }
 
@@ -188,11 +189,16 @@ $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
 test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
+	'simple bind authentication fails if user not found in LDAP',
+	log_unlike => [ qr/connection authenticated:/ ]);
 test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+	'simple bind authentication fails with wrong password',
+	log_unlike => [ qr/connection authenticated:/ ]);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access($node, 'test1', 0, 'simple bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search+bind";
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index eea8c0bef5..2afe225267 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1874,6 +1874,17 @@ connection succeeds, to identify it in the server logs. If B<query> is set, it
 will be used instead. This can be used with the B<stdout> parameter (see
 C<psql()>) to capture a query result.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+to match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must NOT match against the server log. They will be passed to
+C<Test::More::unlike()>.
+
 =back
 
 Any additional named parameters are passed to C<psql()> as-is.
@@ -1891,6 +1902,22 @@ sub connect_ok
 		$query = delete $params{query};
 	}
 
+	my (@log_like, @log_unlike);
+	if (exists $params{log_like})
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (exists $params{log_unlike})
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret,  $stdout,  $stderr)    = $self->psql(
 		'postgres',
 		$query,
@@ -1899,6 +1926,20 @@ sub connect_ok
 		on_error_stop => 0);
 
 	is($ret, 0, $test_name);
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
@@ -1907,7 +1948,19 @@ sub connect_ok
 
 Attempt a connection with a custom connection string.  This is expected
 to fail with a message that matches the regular expression
-$expected_stderr. Any additional named parameters are passed to psql().
+$expected_stderr.
+
+=over
+
+=item log_like => [ qr/required message/ ]
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+See C<connect_ok()>, above.
+
+=back
+
+Any additional named parameters are passed to C<psql()>.
 
 =cut
 
@@ -1915,6 +1968,23 @@ sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $expected_stderr, $test_name, %params) = @_;
+
+	my (@log_like, @log_unlike);
+	if (exists $params{log_like})
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (exists $params{log_unlike})
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
@@ -1925,7 +1995,21 @@ sub connect_fails
 
 	if (defined $expected_stderr)
 	{
-		like($stderr, $expected_stderr, "$test_name: matches");
+		like($stderr, $expected_stderr, "$test_name: stderr matches");
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
 	}
 }
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1a63f279c..943703fc44 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -418,7 +418,8 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
+	"certificate authorization succeeds with DN mapping",
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ]);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -432,7 +433,9 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ]);
 
 
 
@@ -493,14 +496,19 @@ SKIP:
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	qr/certificate authentication failed for user "anotheruser"/,
-	"certificate authorization fails with client cert belonging to another user"
+	"certificate authorization fails with client cert belonging to another user",
+	# certificate authentication should be logged even on failure
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser" method=cert/ ],
 );
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	qr/SSL error/,
-	"certificate authorization fails with revoked client cert");
+	"certificate authorization fails with revoked client cert",
+	# revoked certificates should not authenticate the user
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -510,20 +518,26 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	qr/FATAL/,
-	"auth_option clientcert=verify-full fails with mismatching username and Common Name"
+	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index e31650b931..d98f6319fa 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -96,6 +96,12 @@ $node->connect_fails(
 	qr/channel binding required, but server authenticated client without channel binding/,
 	"Cert authentication and channel_binding=require");
 
+# Certificate verification at the connection level should still work fine.
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [ qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]);
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#63Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#62)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Apr 02, 2021 at 12:03:21AM +0000, Jacob Champion wrote:

On Thu, 2021-04-01 at 10:21 +0900, Michael Paquier wrote:

This stuff can take advantage of 0d1a3343, and I
think that we should make the kerberos, ldap, authentication and SSL
test suites just use connect_ok() and connect_fails() from
PostgresNode.pm. They just need to be extended a bit with a new
argument for the log pattern check.

v16, attached, migrates all tests in those suites to connect_ok/fails
(in the first two patches), and also adds the log pattern matching (in
the final feature patch).

Thanks. I have been looking at 0001 and 0002, and found the addition
of %params to connect_ok() and connect_fails() confusing first, as
this is only required for the 12th test of 001_password.pl (failure to
grab a password for md5_role not located in a pgpass file with
PGPASSWORD not set). Instead of falling into a trap where the tests
could remain stuck, I think that we could just pass down -w from
connect_ok() and connect_fails() to PostgresNode::psql.

This change made also the parameter handling of the kerberos tests
more confusing on two points:
- PostgresNode::psql uses a query as an argument, so there was a mix
between the query passed down within the set of parameters, but then
removed from the list.
- PostgresNode::psql uses already -XAt so there is no need to define
it again.

A since-v15 diff is attached, but it should be viewed with suspicion
since I've rebased on top of the new SSL tests at the same time.

That did not seem that suspicious to me ;)

Anyway, after looking at 0003, the main patch, it becomes quite clear
that the need to match logs depending on like() or unlike() is much
more elegant once we have use of parameters in connect_ok() and
connect_fails(), but I think that it is a mistake to pass down blindly
the parameters to psql and delete some of them on the way while
keeping the others. The existing code of HEAD only requires a SQL
query or some expected stderr or stdout output, so let's make all
that parameterized first.

Attached is what I have come up with as the first building piece,
which is basically a combination of 0001 and 0002, except that I
modified things so as the number of arguments remains minimal for all
the routines. This avoids the manipulation of the list of parameters
passed down to PostgresNode::psql. The arguments for the optional
query, the expected stdout and stderr are part of the parameter set
(0001 was not doing that). For the main patch, this will need to be
extended with two more parameters in each routine: log_like and
log_unlike to match for the log patterns, handled as arrays of
regexes. That's what 0003 is basically doing already.

As a whole, this is a consolidation of its own, so let's apply this
part first.
--
Michael

Attachments:

v18-0001-Plug-more-TAP-test-suites-with-new-PostgresNode-.patchtext/x-diff; charset=us-asciiDownload
From a184fe1e6488ca9a5f7c98ab02ae428e05a84449 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 2 Apr 2021 13:44:39 +0900
Subject: [PATCH v18] Plug more TAP test suites with new PostgresNode's new
 routines

This switches the kerberos, ldap and authentication to use connect_ok()
and connect_fails() recently introduced in PostgresNode.pm.  The SSL
tests need some extra juggling to accomodate with the changes.
---
 src/test/authentication/t/001_password.pl |  17 +++-
 src/test/authentication/t/002_saslprep.pl |  18 +++-
 src/test/kerberos/t/001_auth.pl           |  41 +++-----
 src/test/ldap/t/001_auth.pl               |  15 ++-
 src/test/perl/PostgresNode.pm             |  67 +++++++++---
 src/test/ssl/t/001_ssltests.pl            | 118 +++++++++++-----------
 src/test/ssl/t/002_scram.pl               |  16 +--
 7 files changed, 170 insertions(+), 122 deletions(-)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..4d5e304de1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -46,12 +46,19 @@ sub test_role
 
 	$status_string = 'success' if ($expected_res eq 0);
 
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for method $method, role $role";
 
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	is($res, $expected_res,
-		"authentication $status_string for method $method, role $role");
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code.
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index 0aaab090ec..530344a5d6 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -41,12 +41,20 @@ sub test_login
 
 	$status_string = 'success' if ($expected_res eq 0);
 
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for role $role with password $password";
+
 	$ENV{"PGPASSWORD"} = $password;
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
-	is($res, $expected_res,
-		"authentication $status_string for role $role with password $password"
-	);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node. Force UTF-8 encoding, so that we can use non-ASCII
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..31fdc49b86 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 30;
 }
 else
 {
@@ -185,25 +185,18 @@ sub test_access
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	# If we get a query result back, it should be true.
-	if ($res == $expected_res and $res eq 0)
+	if ($expected_res eq 0)
 	{
-		is($stdoutres, "t", $test_name);
+		# The result is assumed to match "true", or "t", here.
+		$node->connect_ok($connstr, $test_name, sql => $query,
+				  expected_stdout => qr/t/);
 	}
 	else
 	{
-		is($res, $expected_res, $test_name);
+		$node->connect_fails($connstr, $test_name);
 	}
 
 	# Verify specified log message is logged in the log file.
@@ -227,20 +220,12 @@ sub test_query
 	my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	is($res, 0, $test_name);
-	like($stdoutres, $expected, $test_name);
-	is($stderrres, "", $test_name);
+	my ($stdoutres, $stderrres);
+
+	$node->connect_ok($connstr, $test_name, $query, $expected);
 	return;
 }
 
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..b08ba6b281 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -163,12 +163,17 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $expected_res, $test_name) = @_;
+	my $connstr = "user=$role";
 
-	my $res =
-	  $node->psql('postgres', undef,
-				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
-	is($res, $expected_res, $test_name);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $test_name);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, $test_name);
+	}
 }
 
 note "simple bind";
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index d6e10544bb..ba1407e761 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1860,52 +1860,95 @@ sub interactive_psql
 
 =pod
 
-=item $node->connect_ok($connstr, $test_name)
+=item $node->connect_ok($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
 to succeed.
 
+=over
+
+=item sql => B<value>
+
+If this parameter is set, this query is used for the connection attempt
+instead of the default.
+
+=item expected_stdout => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=back
+
 =cut
 
 sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $test_name) = @_;
-	my ($ret,  $stdout,  $stderr)    = $self->psql(
+	my ($self, $connstr, $test_name, %params) = @_;
+
+	my $sql;
+	if (defined($params{sql}))
+	{
+		$sql = $params{sql};
+	}
+	else
+	{
+		$sql = "SELECT \$\$connected with $connstr\$\$";
+	}
+
+	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
-		"SELECT \$\$connected with $connstr\$\$",
+		$sql,
+		extra_params => [ '-w'],
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
-	ok($ret == 0, $test_name);
+	is($ret, 0, $test_name);
+
+	if (defined($params{expected_stdout}))
+	{
+		like($stdout, $params{expected_stdout}, "$test_name: matches");
+	}
 }
 
 =pod
 
-=item $node->connect_fails($connstr, $expected_stderr, $test_name)
+=item $node->connect_fails($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
-to fail with a message that matches the regular expression
-$expected_stderr.
+to fail.  The generated error message can be checked by specifying the
+regular expression $expected_stderr for a match.
+
+=over
+
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=back
 
 =cut
 
 sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $expected_stderr, $test_name) = @_;
+	my ($self, $connstr, $test_name, %params) = @_;
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		extra_params => [ '-w'],
 		connstr => "$connstr");
 
-	ok($ret != 0, $test_name);
-	like($stderr, $expected_stderr, "$test_name: matches");
+	isnt($ret, 0, $test_name);
+
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: matches");
+	}
 }
 
 =pod
 
-=item $node->poll_query_until($dbname, $query [, $expected ])
+=item $node->poll_query_until($dbname, $query [, $expected )
 
 Run B<$query> repeatedly, until it returns the B<$expected> result
 ('t', or SQL boolean true, by default).
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1a63f279c..21beef01aa 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -137,8 +137,8 @@ $common_connstr =
 # The server should not accept non-SSL connections.
 $node->connect_fails(
 	"$common_connstr sslmode=disable",
-	qr/\Qno pg_hba.conf entry\E/,
-	"server doesn't accept non-SSL connections");
+	"server doesn't accept non-SSL connections",
+	expected_stderr => qr/\Qno pg_hba.conf entry\E/);
 
 # Try without a root cert. In sslmode=require, this should work. In verify-ca
 # or verify-full mode it should fail.
@@ -147,34 +147,34 @@ $node->connect_ok(
 	"connect without server root cert sslmode=require");
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-ca",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-ca");
+	"connect without server root cert sslmode=verify-ca",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-full",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-full");
+	"connect without server root cert sslmode=verify-full",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 
 # Try with wrong root cert, should fail. (We're using the client CA as the
 # root, but the server's key is signed by the server CA.)
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=require",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=require");
+	"connect with wrong server root cert sslmode=require",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-ca",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=verify-ca");
+	"connect with wrong server root cert sslmode=verify-ca",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-full",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=verify-full");
+	"connect with wrong server root cert sslmode=verify-full",
+	expected_stderr => qr/SSL error/);
 
 # Try with just the server CA's cert. This fails because the root file
 # must contain the whole chain up to the root CA.
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/server_ca.crt sslmode=verify-ca",
-	qr/SSL error/,
-	"connect with server CA cert, without root CA");
+	"connect with server CA cert, without root CA",
+	expected_stderr => qr/SSL error/);
 
 # And finally, with the correct root cert.
 $node->connect_ok(
@@ -206,14 +206,14 @@ $node->connect_ok(
 # A CRL belonging to a different CA is not accepted, fails
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl",
-	qr/SSL error/,
-	"CRL belonging to a different CA");
+	"CRL belonging to a different CA",
+	expected_stderr => qr/SSL error/);
 
 # The same for CRL directory
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/client-crldir",
-	qr/SSL error/,
-	"directory CRL belonging to a different CA");
+	"directory CRL belonging to a different CA",
+	expected_stderr => qr/SSL error/);
 
 # With the correct CRL, succeeds (this cert is not revoked)
 $node->connect_ok(
@@ -237,8 +237,8 @@ $node->connect_ok(
 	"mismatch between host name and server certificate sslmode=verify-ca");
 $node->connect_fails(
 	"$common_connstr sslmode=verify-full host=wronghost.test",
-	qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/,
-	"mismatch between host name and server certificate sslmode=verify-full");
+	"mismatch between host name and server certificate sslmode=verify-full",
+	expected_stderr => qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/);
 
 # Test Subject Alternative Names.
 switch_server_cert($node, 'server-multiple-alt-names');
@@ -257,12 +257,12 @@ $node->connect_ok("$common_connstr host=foo.wildcard.pg-ssltest.test",
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names");
+	"host name not matching with X.509 Subject Alternative Names",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names wildcard");
+	"host name not matching with X.509 Subject Alternative Names wildcard",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/);
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
@@ -277,12 +277,12 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name");
+	"host name not matching with a single X.509 Subject Alternative Name",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name wildcard"
+	"host name not matching with a single X.509 Subject Alternative Name wildcard",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
@@ -298,8 +298,8 @@ $node->connect_ok("$common_connstr host=dns2.alt-name.pg-ssltest.test",
 	"certificate with both a CN and SANs 2");
 $node->connect_fails(
 	"$common_connstr host=common-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/,
-	"certificate with both a CN and SANs ignores CN");
+	"certificate with both a CN and SANs ignores CN",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/);
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
@@ -313,8 +313,8 @@ $node->connect_ok(
 $node->connect_fails(
 	$common_connstr . " "
 	  . "sslmode=verify-full host=common-name.pg-ssltest.test",
-	qr/could not get server's host name from server certificate/,
-	"server certificate without CN or SANs sslmode=verify-full");
+	"server certificate without CN or SANs sslmode=verify-full",
+	expected_stderr => qr/could not get server's host name from server certificate/);
 
 # Test that the CRL works
 switch_server_cert($node, 'server-revoked');
@@ -328,12 +328,12 @@ $node->connect_ok(
 	"connects without client-side CRL");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl",
-	qr/SSL error/,
-	"does not connect with client-side CRL file");
+	"does not connect with client-side CRL file",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/root+server-crldir",
-	qr/SSL error/,
-	"does not connect with client-side CRL directory");
+	"does not connect with client-side CRL directory",
+	expected_stderr => qr/SSL error/);
 
 # pg_stat_ssl
 command_like(
@@ -355,16 +355,16 @@ $node->connect_ok(
 	"connection success with correct range of TLS protocol versions");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.1",
-	qr/invalid SSL protocol version range/,
-	"connection failure with incorrect range of TLS protocol versions");
+	"connection failure with incorrect range of TLS protocol versions",
+	expected_stderr => qr/invalid SSL protocol version range/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=incorrect_tls",
-	qr/invalid ssl_min_protocol_version value/,
-	"connection failure with an incorrect SSL protocol minimum bound");
+	"connection failure with an incorrect SSL protocol minimum bound",
+	expected_stderr => qr/invalid ssl_min_protocol_version value/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_max_protocol_version=incorrect_tls",
-	qr/invalid ssl_max_protocol_version value/,
-	"connection failure with an incorrect SSL protocol maximum bound");
+	"connection failure with an incorrect SSL protocol maximum bound",
+	expected_stderr => qr/invalid ssl_max_protocol_version value/);
 
 ### Server-side tests.
 ###
@@ -378,8 +378,8 @@ $common_connstr =
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
-	qr/connection requires a valid client certificate/,
-	"certificate authorization fails without client cert");
+	"certificate authorization fails without client cert",
+	expected_stderr => qr/connection requires a valid client certificate/);
 
 # correct client cert in unencrypted PEM
 $node->connect_ok(
@@ -408,8 +408,8 @@ $node->connect_ok(
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong'",
-	qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!,
-	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format"
+	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
+	expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!
 );
 
 
@@ -446,14 +446,14 @@ TODO:
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
 		"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
 		"certificate authorization fails with correct client cert and no password in encrypted PEM format"
 	);
 
@@ -485,22 +485,22 @@ SKIP:
 
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_wrongperms_tmp.key",
-		qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!,
-		"certificate authorization fails because of file permissions");
+		"certificate authorization fails because of file permissions",
+		expected_stderr => qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!);
 }
 
 # client cert belonging to another user
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/certificate authentication failed for user "anotheruser"/,
-	"certificate authorization fails with client cert belonging to another user"
+	"certificate authorization fails with client cert belonging to another user",
+	expected_stderr => qr/certificate authentication failed for user "anotheruser"/
 );
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error/,
-	"certificate authorization fails with revoked client cert");
+	"certificate authorization fails with revoked client cert",
+	expected_stderr => qr/SSL error/);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -515,8 +515,8 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/FATAL/,
-	"auth_option clientcert=verify-full fails with mismatching username and Common Name"
+	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
+	expected_stderr => qr/FATAL/,
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
@@ -536,7 +536,7 @@ $node->connect_ok(
 	"intermediate client certificate is provided by client");
 $node->connect_fails(
 	$common_connstr . " " . "sslmode=require sslcert=ssl/client.crt",
-	qr/SSL error/, "intermediate client certificate is missing");
+	"intermediate client certificate is missing", expected_stderr => qr/SSL error/);
 
 # test server-side CRL directory
 switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
@@ -544,8 +544,8 @@ switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error/,
-	"certificate authorization fails with revoked client cert with server-side CRL directory");
+	"certificate authorization fails with revoked client cert with server-side CRL directory",
+	expected_stderr => qr/SSL error/);
 
 # clean up
 foreach my $key (@keys)
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index e31650b931..640b95099a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -60,8 +60,8 @@ $node->connect_ok(
 # Test channel_binding
 $node->connect_fails(
 	"$common_connstr user=ssltestuser channel_binding=invalid_value",
-	qr/invalid channel_binding value: "invalid_value"/,
-	"SCRAM with SSL and channel_binding=invalid_value");
+	"SCRAM with SSL and channel_binding=invalid_value",
+	expected_stderr => qr/invalid channel_binding value: "invalid_value"/);
 $node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable",
 	"SCRAM with SSL and channel_binding=disable");
 if ($supports_tls_server_end_point)
@@ -74,15 +74,15 @@ else
 {
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser channel_binding=require",
-		qr/channel binding is required, but server did not offer an authentication method that supports channel binding/,
-		"SCRAM with SSL and channel_binding=require");
+		"SCRAM with SSL and channel_binding=require",
+		expected_stderr => qr/channel binding is required, but server did not offer an authentication method that supports channel binding/);
 }
 
 # Now test when the user has an MD5-encrypted password; should fail
 $node->connect_fails(
 	"$common_connstr user=md5testuser channel_binding=require",
-	qr/channel binding required but not supported by server's authentication request/,
-	"MD5 with SSL and channel_binding=require");
+	"MD5 with SSL and channel_binding=require",
+	expected_stderr => qr/channel binding required but not supported by server's authentication request/);
 
 # Now test with auth method 'cert' by connecting to 'certdb'. Should fail,
 # because channel binding is not performed.  Note that ssl/client.key may
@@ -93,8 +93,8 @@ copy("ssl/client.key", $client_tmp_key);
 chmod 0600, $client_tmp_key;
 $node->connect_fails(
 	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=certdb user=ssltestuser channel_binding=require",
-	qr/channel binding required, but server authenticated client without channel binding/,
-	"Cert authentication and channel_binding=require");
+	"Cert authentication and channel_binding=require",
+	expected_stderr => qr/channel binding required, but server authenticated client without channel binding/);
 
 # clean up
 unlink($client_tmp_key);
-- 
2.31.0

#64Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#63)
3 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, 2021-04-02 at 13:45 +0900, Michael Paquier wrote:

Attached is what I have come up with as the first building piece,
which is basically a combination of 0001 and 0002, except that I
modified things so as the number of arguments remains minimal for all
the routines. This avoids the manipulation of the list of parameters
passed down to PostgresNode::psql. The arguments for the optional
query, the expected stdout and stderr are part of the parameter set
(0001 was not doing that).

I made a few changes, highlighted in the since-v18 diff:

+		# The result is assumed to match "true", or "t", here.
+		$node->connect_ok($connstr, $test_name, sql => $query,
+				  expected_stdout => qr/t/);

I've anchored this as qr/^t$/ so we don't accidentally match a stray
"t" in some larger string.

-	is($res, 0, $test_name);
-	like($stdoutres, $expected, $test_name);
-	is($stderrres, "", $test_name);
+	my ($stdoutres, $stderrres);
+
+	$node->connect_ok($connstr, $test_name, $query, $expected);

$query and $expected need to be given as named parameters. We also lost
the stderr check from the previous version of the test, so I added
expected_stderr to connect_ok().

@@ -446,14 +446,14 @@ TODO:
# correct client cert in encrypted PEM with empty password
$node->connect_fails(
"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
);

These tests don't run yet inside the TODO block, but I've put the
expected_stderr parameter at the end of the list for them.

For the main patch, this will need to be
extended with two more parameters in each routine: log_like and
log_unlike to match for the log patterns, handled as arrays of
regexes. That's what 0003 is basically doing already.

Rebased on top of your patch as v19, attached. (v17 disappeared into
the ether somewhere, I think. :D)

Now that it's easy to add log_like to existing tests, I fleshed out the
LDAP tests with a few more cases. They don't add code coverage, but
they pin the desired behavior for a few more types of LDAP auth.

--Jacob

Attachments:

since-v18.diff.txttext/plain; name=since-v18.diff.txtDownload
commit d80742fbe2124687591e76dc069d7cab6a2cecc8
Author: Jacob Champion <pchampion@vmware.com>
Date:   Fri Apr 2 10:31:40 2021 -0700

    squash! Plug more TAP test suites with new PostgresNode's new routines

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 31fdc49b86..2395664145 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 30;
+	plan tests => 34;
 }
 else
 {
@@ -192,7 +192,7 @@ sub test_access
 	{
 		# The result is assumed to match "true", or "t", here.
 		$node->connect_ok($connstr, $test_name, sql => $query,
-				  expected_stdout => qr/t/);
+				  expected_stdout => qr/^t$/);
 	}
 	else
 	{
@@ -223,9 +223,9 @@ sub test_query
 	my $connstr = $node->connstr('postgres') .
 		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	my ($stdoutres, $stderrres);
-
-	$node->connect_ok($connstr, $test_name, $query, $expected);
+	$node->connect_ok($connstr, $test_name, sql => $query,
+		expected_stdout => $expected,
+		expected_stderr => qr/^$/);
 	return;
 }
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index ba1407e761..f29c877c79 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1876,6 +1876,10 @@ instead of the default.
 
 If this regular expression is set, matches it with the output generated.
 
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the stderr generated.
+
 =back
 
 =cut
@@ -1906,7 +1910,11 @@ sub connect_ok
 
 	if (defined($params{expected_stdout}))
 	{
-		like($stdout, $params{expected_stdout}, "$test_name: matches");
+		like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
+	}
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
 }
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 21beef01aa..6df558fca7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -446,15 +446,15 @@ TODO:
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
-		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
+		"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key",
-		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and no password in encrypted PEM format"
+		"certificate authorization fails with correct client cert and no password in encrypted PEM format",
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 }
v19-0001-Plug-more-TAP-test-suites-with-new-PostgresNode-.patchtext/x-patch; name=v19-0001-Plug-more-TAP-test-suites-with-new-PostgresNode-.patchDownload
From 0bb565c41fdd9b1fcafc59775baa39cdd43eb4e7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 2 Apr 2021 13:44:39 +0900
Subject: [PATCH v19 1/2] Plug more TAP test suites with new PostgresNode's new
 routines

This switches the kerberos, ldap and authentication to use connect_ok()
and connect_fails() recently introduced in PostgresNode.pm.  The SSL
tests need some extra juggling to accomodate with the changes.
---
 src/test/authentication/t/001_password.pl |  19 ++--
 src/test/authentication/t/002_saslprep.pl |  18 +++-
 src/test/kerberos/t/001_auth.pl           |  45 +++-----
 src/test/ldap/t/001_auth.pl               |  17 +--
 src/test/perl/PostgresNode.pm             |  75 ++++++++++---
 src/test/ssl/t/001_ssltests.pl            | 122 +++++++++++-----------
 src/test/ssl/t/002_scram.pl               |  16 +--
 7 files changed, 184 insertions(+), 128 deletions(-)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..4d5e304de1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -46,12 +46,19 @@ sub test_role
 
 	$status_string = 'success' if ($expected_res eq 0);
 
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
-
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	is($res, $expected_res,
-		"authentication $status_string for method $method, role $role");
-	return;
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for method $method, role $role";
+
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code.
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index 0aaab090ec..530344a5d6 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -41,12 +41,20 @@ sub test_login
 
 	$status_string = 'success' if ($expected_res eq 0);
 
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for role $role with password $password";
+
 	$ENV{"PGPASSWORD"} = $password;
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
-	is($res, $expected_res,
-		"authentication $status_string for role $role with password $password"
-	);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node. Force UTF-8 encoding, so that we can use non-ASCII
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..2395664145 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 34;
 }
 else
 {
@@ -185,25 +185,18 @@ sub test_access
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
-
-	# If we get a query result back, it should be true.
-	if ($res == $expected_res and $res eq 0)
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	if ($expected_res eq 0)
 	{
-		is($stdoutres, "t", $test_name);
+		# The result is assumed to match "true", or "t", here.
+		$node->connect_ok($connstr, $test_name, sql => $query,
+				  expected_stdout => qr/^t$/);
 	}
 	else
 	{
-		is($res, $expected_res, $test_name);
+		$node->connect_fails($connstr, $test_name);
 	}
 
 	# Verify specified log message is logged in the log file.
@@ -227,20 +220,12 @@ sub test_query
 	my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
-
-	is($res, 0, $test_name);
-	like($stdoutres, $expected, $test_name);
-	is($stderrres, "", $test_name);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
+
+	$node->connect_ok($connstr, $test_name, sql => $query,
+		expected_stdout => $expected,
+		expected_stderr => qr/^$/);
 	return;
 }
 
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..b08ba6b281 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -163,12 +163,17 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $expected_res, $test_name) = @_;
-
-	my $res =
-	  $node->psql('postgres', undef,
-				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
-	is($res, $expected_res, $test_name);
-	return;
+	my $connstr = "user=$role";
+
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $test_name);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, $test_name);
+	}
 }
 
 note "simple bind";
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index d6e10544bb..f29c877c79 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1860,52 +1860,103 @@ sub interactive_psql
 
 =pod
 
-=item $node->connect_ok($connstr, $test_name)
+=item $node->connect_ok($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
 to succeed.
 
+=over
+
+=item sql => B<value>
+
+If this parameter is set, this query is used for the connection attempt
+instead of the default.
+
+=item expected_stdout => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the stderr generated.
+
+=back
+
 =cut
 
 sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $test_name) = @_;
-	my ($ret,  $stdout,  $stderr)    = $self->psql(
+	my ($self, $connstr, $test_name, %params) = @_;
+
+	my $sql;
+	if (defined($params{sql}))
+	{
+		$sql = $params{sql};
+	}
+	else
+	{
+		$sql = "SELECT \$\$connected with $connstr\$\$";
+	}
+
+	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
-		"SELECT \$\$connected with $connstr\$\$",
+		$sql,
+		extra_params => [ '-w'],
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
-	ok($ret == 0, $test_name);
+	is($ret, 0, $test_name);
+
+	if (defined($params{expected_stdout}))
+	{
+		like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
+	}
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
+	}
 }
 
 =pod
 
-=item $node->connect_fails($connstr, $expected_stderr, $test_name)
+=item $node->connect_fails($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
-to fail with a message that matches the regular expression
-$expected_stderr.
+to fail.  The generated error message can be checked by specifying the
+regular expression $expected_stderr for a match.
+
+=over
+
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=back
 
 =cut
 
 sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $expected_stderr, $test_name) = @_;
+	my ($self, $connstr, $test_name, %params) = @_;
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		extra_params => [ '-w'],
 		connstr => "$connstr");
 
-	ok($ret != 0, $test_name);
-	like($stderr, $expected_stderr, "$test_name: matches");
+	isnt($ret, 0, $test_name);
+
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: matches");
+	}
 }
 
 =pod
 
-=item $node->poll_query_until($dbname, $query [, $expected ])
+=item $node->poll_query_until($dbname, $query [, $expected )
 
 Run B<$query> repeatedly, until it returns the B<$expected> result
 ('t', or SQL boolean true, by default).
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1a63f279c..6df558fca7 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -137,8 +137,8 @@ $common_connstr =
 # The server should not accept non-SSL connections.
 $node->connect_fails(
 	"$common_connstr sslmode=disable",
-	qr/\Qno pg_hba.conf entry\E/,
-	"server doesn't accept non-SSL connections");
+	"server doesn't accept non-SSL connections",
+	expected_stderr => qr/\Qno pg_hba.conf entry\E/);
 
 # Try without a root cert. In sslmode=require, this should work. In verify-ca
 # or verify-full mode it should fail.
@@ -147,34 +147,34 @@ $node->connect_ok(
 	"connect without server root cert sslmode=require");
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-ca",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-ca");
+	"connect without server root cert sslmode=verify-ca",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-full",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-full");
+	"connect without server root cert sslmode=verify-full",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 
 # Try with wrong root cert, should fail. (We're using the client CA as the
 # root, but the server's key is signed by the server CA.)
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=require",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=require");
+	"connect with wrong server root cert sslmode=require",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-ca",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=verify-ca");
+	"connect with wrong server root cert sslmode=verify-ca",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-full",
-	qr/SSL error/,
-	"connect with wrong server root cert sslmode=verify-full");
+	"connect with wrong server root cert sslmode=verify-full",
+	expected_stderr => qr/SSL error/);
 
 # Try with just the server CA's cert. This fails because the root file
 # must contain the whole chain up to the root CA.
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/server_ca.crt sslmode=verify-ca",
-	qr/SSL error/,
-	"connect with server CA cert, without root CA");
+	"connect with server CA cert, without root CA",
+	expected_stderr => qr/SSL error/);
 
 # And finally, with the correct root cert.
 $node->connect_ok(
@@ -206,14 +206,14 @@ $node->connect_ok(
 # A CRL belonging to a different CA is not accepted, fails
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl",
-	qr/SSL error/,
-	"CRL belonging to a different CA");
+	"CRL belonging to a different CA",
+	expected_stderr => qr/SSL error/);
 
 # The same for CRL directory
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/client-crldir",
-	qr/SSL error/,
-	"directory CRL belonging to a different CA");
+	"directory CRL belonging to a different CA",
+	expected_stderr => qr/SSL error/);
 
 # With the correct CRL, succeeds (this cert is not revoked)
 $node->connect_ok(
@@ -237,8 +237,8 @@ $node->connect_ok(
 	"mismatch between host name and server certificate sslmode=verify-ca");
 $node->connect_fails(
 	"$common_connstr sslmode=verify-full host=wronghost.test",
-	qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/,
-	"mismatch between host name and server certificate sslmode=verify-full");
+	"mismatch between host name and server certificate sslmode=verify-full",
+	expected_stderr => qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/);
 
 # Test Subject Alternative Names.
 switch_server_cert($node, 'server-multiple-alt-names');
@@ -257,12 +257,12 @@ $node->connect_ok("$common_connstr host=foo.wildcard.pg-ssltest.test",
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names");
+	"host name not matching with X.509 Subject Alternative Names",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names wildcard");
+	"host name not matching with X.509 Subject Alternative Names wildcard",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/);
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
@@ -277,12 +277,12 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name");
+	"host name not matching with a single X.509 Subject Alternative Name",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name wildcard"
+	"host name not matching with a single X.509 Subject Alternative Name wildcard",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
@@ -298,8 +298,8 @@ $node->connect_ok("$common_connstr host=dns2.alt-name.pg-ssltest.test",
 	"certificate with both a CN and SANs 2");
 $node->connect_fails(
 	"$common_connstr host=common-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/,
-	"certificate with both a CN and SANs ignores CN");
+	"certificate with both a CN and SANs ignores CN",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/);
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
@@ -313,8 +313,8 @@ $node->connect_ok(
 $node->connect_fails(
 	$common_connstr . " "
 	  . "sslmode=verify-full host=common-name.pg-ssltest.test",
-	qr/could not get server's host name from server certificate/,
-	"server certificate without CN or SANs sslmode=verify-full");
+	"server certificate without CN or SANs sslmode=verify-full",
+	expected_stderr => qr/could not get server's host name from server certificate/);
 
 # Test that the CRL works
 switch_server_cert($node, 'server-revoked');
@@ -328,12 +328,12 @@ $node->connect_ok(
 	"connects without client-side CRL");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl",
-	qr/SSL error/,
-	"does not connect with client-side CRL file");
+	"does not connect with client-side CRL file",
+	expected_stderr => qr/SSL error/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/root+server-crldir",
-	qr/SSL error/,
-	"does not connect with client-side CRL directory");
+	"does not connect with client-side CRL directory",
+	expected_stderr => qr/SSL error/);
 
 # pg_stat_ssl
 command_like(
@@ -355,16 +355,16 @@ $node->connect_ok(
 	"connection success with correct range of TLS protocol versions");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.1",
-	qr/invalid SSL protocol version range/,
-	"connection failure with incorrect range of TLS protocol versions");
+	"connection failure with incorrect range of TLS protocol versions",
+	expected_stderr => qr/invalid SSL protocol version range/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=incorrect_tls",
-	qr/invalid ssl_min_protocol_version value/,
-	"connection failure with an incorrect SSL protocol minimum bound");
+	"connection failure with an incorrect SSL protocol minimum bound",
+	expected_stderr => qr/invalid ssl_min_protocol_version value/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_max_protocol_version=incorrect_tls",
-	qr/invalid ssl_max_protocol_version value/,
-	"connection failure with an incorrect SSL protocol maximum bound");
+	"connection failure with an incorrect SSL protocol maximum bound",
+	expected_stderr => qr/invalid ssl_max_protocol_version value/);
 
 ### Server-side tests.
 ###
@@ -378,8 +378,8 @@ $common_connstr =
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
-	qr/connection requires a valid client certificate/,
-	"certificate authorization fails without client cert");
+	"certificate authorization fails without client cert",
+	expected_stderr => qr/connection requires a valid client certificate/);
 
 # correct client cert in unencrypted PEM
 $node->connect_ok(
@@ -408,8 +408,8 @@ $node->connect_ok(
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong'",
-	qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!,
-	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format"
+	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
+	expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!
 );
 
 
@@ -446,15 +446,15 @@ TODO:
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
+		"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and no password in encrypted PEM format"
+		"certificate authorization fails with correct client cert and no password in encrypted PEM format",
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 }
@@ -485,22 +485,22 @@ SKIP:
 
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_wrongperms_tmp.key",
-		qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!,
-		"certificate authorization fails because of file permissions");
+		"certificate authorization fails because of file permissions",
+		expected_stderr => qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!);
 }
 
 # client cert belonging to another user
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/certificate authentication failed for user "anotheruser"/,
-	"certificate authorization fails with client cert belonging to another user"
+	"certificate authorization fails with client cert belonging to another user",
+	expected_stderr => qr/certificate authentication failed for user "anotheruser"/
 );
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error/,
-	"certificate authorization fails with revoked client cert");
+	"certificate authorization fails with revoked client cert",
+	expected_stderr => qr/SSL error/);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -515,8 +515,8 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/FATAL/,
-	"auth_option clientcert=verify-full fails with mismatching username and Common Name"
+	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
+	expected_stderr => qr/FATAL/,
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
@@ -536,7 +536,7 @@ $node->connect_ok(
 	"intermediate client certificate is provided by client");
 $node->connect_fails(
 	$common_connstr . " " . "sslmode=require sslcert=ssl/client.crt",
-	qr/SSL error/, "intermediate client certificate is missing");
+	"intermediate client certificate is missing", expected_stderr => qr/SSL error/);
 
 # test server-side CRL directory
 switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
@@ -544,8 +544,8 @@ switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error/,
-	"certificate authorization fails with revoked client cert with server-side CRL directory");
+	"certificate authorization fails with revoked client cert with server-side CRL directory",
+	expected_stderr => qr/SSL error/);
 
 # clean up
 foreach my $key (@keys)
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index e31650b931..640b95099a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -60,8 +60,8 @@ $node->connect_ok(
 # Test channel_binding
 $node->connect_fails(
 	"$common_connstr user=ssltestuser channel_binding=invalid_value",
-	qr/invalid channel_binding value: "invalid_value"/,
-	"SCRAM with SSL and channel_binding=invalid_value");
+	"SCRAM with SSL and channel_binding=invalid_value",
+	expected_stderr => qr/invalid channel_binding value: "invalid_value"/);
 $node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable",
 	"SCRAM with SSL and channel_binding=disable");
 if ($supports_tls_server_end_point)
@@ -74,15 +74,15 @@ else
 {
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser channel_binding=require",
-		qr/channel binding is required, but server did not offer an authentication method that supports channel binding/,
-		"SCRAM with SSL and channel_binding=require");
+		"SCRAM with SSL and channel_binding=require",
+		expected_stderr => qr/channel binding is required, but server did not offer an authentication method that supports channel binding/);
 }
 
 # Now test when the user has an MD5-encrypted password; should fail
 $node->connect_fails(
 	"$common_connstr user=md5testuser channel_binding=require",
-	qr/channel binding required but not supported by server's authentication request/,
-	"MD5 with SSL and channel_binding=require");
+	"MD5 with SSL and channel_binding=require",
+	expected_stderr => qr/channel binding required but not supported by server's authentication request/);
 
 # Now test with auth method 'cert' by connecting to 'certdb'. Should fail,
 # because channel binding is not performed.  Note that ssl/client.key may
@@ -93,8 +93,8 @@ copy("ssl/client.key", $client_tmp_key);
 chmod 0600, $client_tmp_key;
 $node->connect_fails(
 	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=certdb user=ssltestuser channel_binding=require",
-	qr/channel binding required, but server authenticated client without channel binding/,
-	"Cert authentication and channel_binding=require");
+	"Cert authentication and channel_binding=require",
+	expected_stderr => qr/channel binding required, but server authenticated client without channel binding/);
 
 # clean up
 unlink($client_tmp_key);
-- 
2.25.1

v19-0002-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v19-0002-Log-authenticated-identity-from-all-auth-backend.patchDownload
From dc1f3c590a27ff71c28ec47e7f303bffc917580e Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 16:01:27 -0700
Subject: [PATCH v19 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

PostgresNode::connect_ok/fails() have been modified to let tests check
the logfiles for required or prohibited patterns, using the respective
log_like and log_unlike parameters.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 136 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/test/authentication/t/001_password.pl |  49 +++++---
 src/test/kerberos/t/001_auth.pl           |  78 +++++++++----
 src/test/ldap/t/001_auth.pl               |  32 +++--
 src/test/perl/PostgresNode.pm             |  81 ++++++++++++-
 src/test/ssl/t/001_ssltests.pl            |  30 +++--
 src/test/ssl/t/002_scram.pl               |   8 +-
 11 files changed, 387 insertions(+), 68 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9d87b5097a..7379dd3650 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6714,7 +6714,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dee056b0d6 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3115,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 4d5e304de1..f1975bf5d7 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 23;
 }
 
 
@@ -35,15 +35,12 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
+	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
-
 	$status_string = 'success' if ($expected_res eq 0);
 
 	my $connstr = "user=$role";
@@ -51,19 +48,20 @@ sub test_role
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $testname);
+		$node->connect_ok($connstr, $testname, %params);
 	}
 	else
 	{
 		# No match pattern checks are done here on errors, only the
 		# status code.
-		$node->connect_fails($connstr, $testname);
+		$node->connect_fails($connstr, $testname, %params);
 	}
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -76,26 +74,41 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_role($node, 'md5_role',   'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=password/ ]);
+test_role($node, 'md5_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=password/ ]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role($node, 'scram_role', 'scram-sha-256', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=scram-sha-256/ ]);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=md5/ ]);
+test_role($node, 'md5_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=md5/ ]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 2395664145..ff4d7503a6 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 46;
 }
 else
 {
@@ -182,36 +182,36 @@ note "running tests";
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
-	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
+	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
+		@expect_log_msgs) = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my $connstr = $node->connstr('postgres') .
 		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
+	my %params = (
+		sql => $query,
+	);
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		$params{log_like} = \@regexes;
+	}
+
 	if ($expected_res eq 0)
 	{
 		# The result is assumed to match "true", or "t", here.
-		$node->connect_ok($connstr, $test_name, sql => $query,
-				  expected_stdout => qr/^t$/);
+		$params{expected_stdout} = qr/^t$/;
+
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
-
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -234,11 +234,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -250,6 +258,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -260,6 +269,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -269,6 +279,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -305,6 +316,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -314,10 +326,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -331,10 +344,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -342,6 +356,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -358,5 +373,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index b08ba6b281..a03e4ef0df 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 28;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,17 +163,17 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $test_name);
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
 		# Currently, we don't check the error message, just the code.
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
 }
 
@@ -186,11 +187,16 @@ $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
 test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
+	'simple bind authentication fails if user not found in LDAP',
+	log_unlike => [ qr/connection authenticated:/ ]);
 test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+	'simple bind authentication fails with wrong password',
+	log_unlike => [ qr/connection authenticated:/ ]);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access($node, 'test1', 0, 'simple bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search+bind";
 
@@ -206,7 +212,9 @@ test_access($node, 'test0', 2,
 test_access($node, 'test1', 2,
 	'search+bind authentication fails with wrong password');
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search+bind authentication succeeds');
+test_access($node, 'test1', 0, 'search+bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "multiple servers";
 
@@ -250,9 +258,13 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search filter finds by uid');
+test_access($node, 'test1', 0, 'search filter finds by uid',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 $ENV{"PGPASSWORD"} = 'secret2';
-test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail',
+	log_like => [ qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search filters in LDAP URLs";
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index f29c877c79..aca920fe16 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1880,6 +1880,19 @@ If this regular expression is set, matches it with the output generated.
 
 If this regular expression is set, matches it with the stderr generated.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must NOT match against the server log. They will be passed to
+C<Test::More::unlike()>.
+
+=back
+
 =back
 
 =cut
@@ -1899,6 +1912,22 @@ sub connect_ok
 		$sql = "SELECT \$\$connected with $connstr\$\$";
 	}
 
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		$sql,
@@ -1916,6 +1945,19 @@ sub connect_ok
 	{
 	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
@@ -1932,6 +1974,12 @@ regular expression $expected_stderr for a match.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+See C<connect_ok()>, above.
+
 =back
 
 =cut
@@ -1940,6 +1988,23 @@ sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $test_name, %params) = @_;
+
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
@@ -1950,7 +2015,21 @@ sub connect_fails
 
 	if (defined($params{expected_stderr}))
 	{
-	    like($stderr, $params{expected_stderr}, "$test_name: matches");
+	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
 	}
 }
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 6df558fca7..dacb395b37 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -418,7 +418,9 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
+	"certificate authorization succeeds with DN mapping",
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -432,7 +434,10 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 
 
@@ -493,14 +498,19 @@ SKIP:
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"certificate authorization fails with client cert belonging to another user",
-	expected_stderr => qr/certificate authentication failed for user "anotheruser"/
+	expected_stderr => qr/certificate authentication failed for user "anotheruser"/,
+	# certificate authentication should be logged even on failure
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser" method=cert/ ],
 );
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	"certificate authorization fails with revoked client cert",
-	expected_stderr => qr/SSL error/);
+	expected_stderr => qr/SSL error/,
+	# revoked certificates should not authenticate the user
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -510,20 +520,26 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
 	expected_stderr => qr/FATAL/,
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 640b95099a..3f77fdcd2d 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -96,6 +96,12 @@ $node->connect_fails(
 	"Cert authentication and channel_binding=require",
 	expected_stderr => qr/channel binding required, but server authenticated client without channel binding/);
 
+# Certificate verification at the connection level should still work fine.
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [ qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]);
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#65Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#63)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Apr 02, 2021 at 01:45:31PM +0900, Michael Paquier wrote:

As a whole, this is a consolidation of its own, so let's apply this
part first.

Slight rebase for this one to take care of the updates with the SSL
error messages.
--
Michael

Attachments:

v19-0001-Plug-more-TAP-test-suites-with-new-PostgresNode-.patchtext/x-diff; charset=us-asciiDownload
From 01e836535119dcd5a69ce54e1c86ae51bfba492c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 2 Apr 2021 13:44:39 +0900
Subject: [PATCH v19] Plug more TAP test suites with new PostgresNode's new
 routines

This switches the kerberos, ldap and authentication to use connect_ok()
and connect_fails() recently introduced in PostgresNode.pm.  The SSL
tests need some extra juggling to accomodate with the changes.
---
 src/test/authentication/t/001_password.pl |  17 +++-
 src/test/authentication/t/002_saslprep.pl |  18 +++-
 src/test/kerberos/t/001_auth.pl           |  41 +++-----
 src/test/ldap/t/001_auth.pl               |  15 ++-
 src/test/perl/PostgresNode.pm             |  67 +++++++++---
 src/test/ssl/t/001_ssltests.pl            | 118 +++++++++++-----------
 src/test/ssl/t/002_scram.pl               |  16 +--
 7 files changed, 170 insertions(+), 122 deletions(-)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 36a616d7c7..4d5e304de1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -46,12 +46,19 @@ sub test_role
 
 	$status_string = 'success' if ($expected_res eq 0);
 
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for method $method, role $role";
 
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role, '-w' ]);
-	is($res, $expected_res,
-		"authentication $status_string for method $method, role $role");
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code.
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index 0aaab090ec..530344a5d6 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -41,12 +41,20 @@ sub test_login
 
 	$status_string = 'success' if ($expected_res eq 0);
 
+	my $connstr = "user=$role";
+	my $testname = "authentication $status_string for role $role with password $password";
+
 	$ENV{"PGPASSWORD"} = $password;
-	my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
-	is($res, $expected_res,
-		"authentication $status_string for role $role with password $password"
-	);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $testname);
+	}
+	else
+	{
+		# No match pattern checks are done here on errors, only the
+		# status code
+		$node->connect_fails($connstr, $testname);
+	}
 }
 
 # Initialize primary node. Force UTF-8 encoding, so that we can use non-ASCII
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 38e9ef7b1f..31fdc49b86 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 26;
+	plan tests => 30;
 }
 else
 {
@@ -185,25 +185,18 @@ sub test_access
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name, $expect_log_msg) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	# If we get a query result back, it should be true.
-	if ($res == $expected_res and $res eq 0)
+	if ($expected_res eq 0)
 	{
-		is($stdoutres, "t", $test_name);
+		# The result is assumed to match "true", or "t", here.
+		$node->connect_ok($connstr, $test_name, sql => $query,
+				  expected_stdout => qr/t/);
 	}
 	else
 	{
-		is($res, $expected_res, $test_name);
+		$node->connect_fails($connstr, $test_name);
 	}
 
 	# Verify specified log message is logged in the log file.
@@ -227,20 +220,12 @@ sub test_query
 	my ($node, $role, $query, $expected, $gssencmode, $test_name) = @_;
 
 	# need to connect over TCP/IP for Kerberos
-	my ($res, $stdoutres, $stderrres) = $node->psql(
-		'postgres',
-		"$query",
-		extra_params => [
-			'-XAtd',
-			$node->connstr('postgres')
-			  . " host=$host hostaddr=$hostaddr $gssencmode",
-			'-U',
-			$role
-		]);
+	my $connstr = $node->connstr('postgres') .
+		" user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	is($res, 0, $test_name);
-	like($stdoutres, $expected, $test_name);
-	is($stderrres, "", $test_name);
+	my ($stdoutres, $stderrres);
+
+	$node->connect_ok($connstr, $test_name, $query, $expected);
 	return;
 }
 
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 3bc7672451..b08ba6b281 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -163,12 +163,17 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $expected_res, $test_name) = @_;
+	my $connstr = "user=$role";
 
-	my $res =
-	  $node->psql('postgres', undef,
-				  extra_params => [ '-U', $role, '-c', 'SELECT 1' ]);
-	is($res, $expected_res, $test_name);
-	return;
+	if ($expected_res eq 0)
+	{
+		$node->connect_ok($connstr, $test_name);
+	}
+	else
+	{
+		# Currently, we don't check the error message, just the code.
+		$node->connect_fails($connstr, $test_name);
+	}
 }
 
 note "simple bind";
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index d6e10544bb..ba1407e761 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1860,52 +1860,95 @@ sub interactive_psql
 
 =pod
 
-=item $node->connect_ok($connstr, $test_name)
+=item $node->connect_ok($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
 to succeed.
 
+=over
+
+=item sql => B<value>
+
+If this parameter is set, this query is used for the connection attempt
+instead of the default.
+
+=item expected_stdout => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=back
+
 =cut
 
 sub connect_ok
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $test_name) = @_;
-	my ($ret,  $stdout,  $stderr)    = $self->psql(
+	my ($self, $connstr, $test_name, %params) = @_;
+
+	my $sql;
+	if (defined($params{sql}))
+	{
+		$sql = $params{sql};
+	}
+	else
+	{
+		$sql = "SELECT \$\$connected with $connstr\$\$";
+	}
+
+	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
-		"SELECT \$\$connected with $connstr\$\$",
+		$sql,
+		extra_params => [ '-w'],
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
-	ok($ret == 0, $test_name);
+	is($ret, 0, $test_name);
+
+	if (defined($params{expected_stdout}))
+	{
+		like($stdout, $params{expected_stdout}, "$test_name: matches");
+	}
 }
 
 =pod
 
-=item $node->connect_fails($connstr, $expected_stderr, $test_name)
+=item $node->connect_fails($connstr, $test_name, %params)
 
 Attempt a connection with a custom connection string.  This is expected
-to fail with a message that matches the regular expression
-$expected_stderr.
+to fail.  The generated error message can be checked by specifying the
+regular expression $expected_stderr for a match.
+
+=over
+
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the output generated.
+
+=back
 
 =cut
 
 sub connect_fails
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my ($self, $connstr, $expected_stderr, $test_name) = @_;
+	my ($self, $connstr, $test_name, %params) = @_;
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		"SELECT \$\$connected with $connstr\$\$",
+		extra_params => [ '-w'],
 		connstr => "$connstr");
 
-	ok($ret != 0, $test_name);
-	like($stderr, $expected_stderr, "$test_name: matches");
+	isnt($ret, 0, $test_name);
+
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: matches");
+	}
 }
 
 =pod
 
-=item $node->poll_query_until($dbname, $query [, $expected ])
+=item $node->poll_query_until($dbname, $query [, $expected )
 
 Run B<$query> repeatedly, until it returns the B<$expected> result
 ('t', or SQL boolean true, by default).
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 49af9c9a07..f0beb6e123 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -137,8 +137,8 @@ $common_connstr =
 # The server should not accept non-SSL connections.
 $node->connect_fails(
 	"$common_connstr sslmode=disable",
-	qr/\Qno pg_hba.conf entry\E/,
-	"server doesn't accept non-SSL connections");
+	"server doesn't accept non-SSL connections",
+	expected_stderr => qr/\Qno pg_hba.conf entry\E/);
 
 # Try without a root cert. In sslmode=require, this should work. In verify-ca
 # or verify-full mode it should fail.
@@ -147,34 +147,34 @@ $node->connect_ok(
 	"connect without server root cert sslmode=require");
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-ca",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-ca");
+	"connect without server root cert sslmode=verify-ca",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=invalid sslmode=verify-full",
-	qr/root certificate file "invalid" does not exist/,
-	"connect without server root cert sslmode=verify-full");
+	"connect without server root cert sslmode=verify-full",
+	expected_stderr => qr/root certificate file "invalid" does not exist/);
 
 # Try with wrong root cert, should fail. (We're using the client CA as the
 # root, but the server's key is signed by the server CA.)
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=require",
-	qr/SSL error: certificate verify failed/,
-	"connect with wrong server root cert sslmode=require");
+	"connect with wrong server root cert sslmode=require",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-ca",
-	qr/SSL error: certificate verify failed/,
-	"connect with wrong server root cert sslmode=verify-ca");
+	"connect with wrong server root cert sslmode=verify-ca",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/client_ca.crt sslmode=verify-full",
-	qr/SSL error: certificate verify failed/,
-	"connect with wrong server root cert sslmode=verify-full");
+	"connect with wrong server root cert sslmode=verify-full",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 
 # Try with just the server CA's cert. This fails because the root file
 # must contain the whole chain up to the root CA.
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/server_ca.crt sslmode=verify-ca",
-	qr/SSL error: certificate verify failed/,
-	"connect with server CA cert, without root CA");
+	"connect with server CA cert, without root CA",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 
 # And finally, with the correct root cert.
 $node->connect_ok(
@@ -206,14 +206,14 @@ $node->connect_ok(
 # A CRL belonging to a different CA is not accepted, fails
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl",
-	qr/SSL error: certificate verify failed/,
-	"CRL belonging to a different CA");
+	"CRL belonging to a different CA",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 
 # The same for CRL directory
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/client-crldir",
-	qr/SSL error: certificate verify failed/,
-	"directory CRL belonging to a different CA");
+	"directory CRL belonging to a different CA",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 
 # With the correct CRL, succeeds (this cert is not revoked)
 $node->connect_ok(
@@ -237,8 +237,8 @@ $node->connect_ok(
 	"mismatch between host name and server certificate sslmode=verify-ca");
 $node->connect_fails(
 	"$common_connstr sslmode=verify-full host=wronghost.test",
-	qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/,
-	"mismatch between host name and server certificate sslmode=verify-full");
+	"mismatch between host name and server certificate sslmode=verify-full",
+	expected_stderr => qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/);
 
 # Test Subject Alternative Names.
 switch_server_cert($node, 'server-multiple-alt-names');
@@ -257,12 +257,12 @@ $node->connect_ok("$common_connstr host=foo.wildcard.pg-ssltest.test",
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names");
+	"host name not matching with X.509 Subject Alternative Names",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with X.509 Subject Alternative Names wildcard");
+	"host name not matching with X.509 Subject Alternative Names wildcard",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/);
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
@@ -277,12 +277,12 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr host=wronghost.alt-name.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name");
+	"host name not matching with a single X.509 Subject Alternative Name",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/);
 $node->connect_fails(
 	"$common_connstr host=deep.subdomain.wildcard.pg-ssltest.test",
-	qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/,
-	"host name not matching with a single X.509 Subject Alternative Name wildcard"
+	"host name not matching with a single X.509 Subject Alternative Name wildcard",
+	expected_stderr => qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
@@ -298,8 +298,8 @@ $node->connect_ok("$common_connstr host=dns2.alt-name.pg-ssltest.test",
 	"certificate with both a CN and SANs 2");
 $node->connect_fails(
 	"$common_connstr host=common-name.pg-ssltest.test",
-	qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/,
-	"certificate with both a CN and SANs ignores CN");
+	"certificate with both a CN and SANs ignores CN",
+	expected_stderr => qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/);
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
@@ -313,8 +313,8 @@ $node->connect_ok(
 $node->connect_fails(
 	$common_connstr . " "
 	  . "sslmode=verify-full host=common-name.pg-ssltest.test",
-	qr/could not get server's host name from server certificate/,
-	"server certificate without CN or SANs sslmode=verify-full");
+	"server certificate without CN or SANs sslmode=verify-full",
+	expected_stderr => qr/could not get server's host name from server certificate/);
 
 # Test that the CRL works
 switch_server_cert($node, 'server-revoked');
@@ -328,12 +328,12 @@ $node->connect_ok(
 	"connects without client-side CRL");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl",
-	qr/SSL error: certificate verify failed/,
-	"does not connect with client-side CRL file");
+	"does not connect with client-side CRL file",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrldir=ssl/root+server-crldir",
-	qr/SSL error: certificate verify failed/,
-	"does not connect with client-side CRL directory");
+	"does not connect with client-side CRL directory",
+	expected_stderr => qr/SSL error: certificate verify failed/);
 
 # pg_stat_ssl
 command_like(
@@ -355,16 +355,16 @@ $node->connect_ok(
 	"connection success with correct range of TLS protocol versions");
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.1",
-	qr/invalid SSL protocol version range/,
-	"connection failure with incorrect range of TLS protocol versions");
+	"connection failure with incorrect range of TLS protocol versions",
+	expected_stderr => qr/invalid SSL protocol version range/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=incorrect_tls",
-	qr/invalid ssl_min_protocol_version value/,
-	"connection failure with an incorrect SSL protocol minimum bound");
+	"connection failure with an incorrect SSL protocol minimum bound",
+	expected_stderr => qr/invalid ssl_min_protocol_version value/);
 $node->connect_fails(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_max_protocol_version=incorrect_tls",
-	qr/invalid ssl_max_protocol_version value/,
-	"connection failure with an incorrect SSL protocol maximum bound");
+	"connection failure with an incorrect SSL protocol maximum bound",
+	expected_stderr => qr/invalid ssl_max_protocol_version value/);
 
 ### Server-side tests.
 ###
@@ -378,8 +378,8 @@ $common_connstr =
 # no client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=invalid",
-	qr/connection requires a valid client certificate/,
-	"certificate authorization fails without client cert");
+	"certificate authorization fails without client cert",
+	expected_stderr => qr/connection requires a valid client certificate/);
 
 # correct client cert in unencrypted PEM
 $node->connect_ok(
@@ -408,8 +408,8 @@ $node->connect_ok(
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong'",
-	qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!,
-	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format"
+	"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
+	expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!
 );
 
 
@@ -446,14 +446,14 @@ TODO:
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
 		"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key",
-		qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
+		expected_stderr => qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
 		"certificate authorization fails with correct client cert and no password in encrypted PEM format"
 	);
 
@@ -485,22 +485,22 @@ SKIP:
 
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_wrongperms_tmp.key",
-		qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!,
-		"certificate authorization fails because of file permissions");
+		"certificate authorization fails because of file permissions",
+		expected_stderr => qr!\Qprivate key file "ssl/client_wrongperms_tmp.key" has group or world access\E!);
 }
 
 # client cert belonging to another user
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/certificate authentication failed for user "anotheruser"/,
-	"certificate authorization fails with client cert belonging to another user"
+	"certificate authorization fails with client cert belonging to another user",
+	expected_stderr => qr/certificate authentication failed for user "anotheruser"/
 );
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error: sslv3 alert certificate revoked/,
-	"certificate authorization fails with revoked client cert");
+	"certificate authorization fails with revoked client cert",
+	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -515,8 +515,8 @@ $node->connect_ok(
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
-	"auth_option clientcert=verify-full fails with mismatching username and Common Name"
+	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
+	expected_stderr => qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
 );
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
@@ -536,7 +536,7 @@ $node->connect_ok(
 	"intermediate client certificate is provided by client");
 $node->connect_fails(
 	$common_connstr . " " . "sslmode=require sslcert=ssl/client.crt",
-	qr/SSL error: tlsv1 alert unknown ca/, "intermediate client certificate is missing");
+	"intermediate client certificate is missing", expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
 
 # test server-side CRL directory
 switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
@@ -544,8 +544,8 @@ switch_server_cert($node, 'server-cn-only', undef, undef, 'root+client-crldir');
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
-	qr/SSL error: sslv3 alert certificate revoked/,
-	"certificate authorization fails with revoked client cert with server-side CRL directory");
+	"certificate authorization fails with revoked client cert with server-side CRL directory",
+	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
 
 # clean up
 foreach my $key (@keys)
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index e31650b931..640b95099a 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -60,8 +60,8 @@ $node->connect_ok(
 # Test channel_binding
 $node->connect_fails(
 	"$common_connstr user=ssltestuser channel_binding=invalid_value",
-	qr/invalid channel_binding value: "invalid_value"/,
-	"SCRAM with SSL and channel_binding=invalid_value");
+	"SCRAM with SSL and channel_binding=invalid_value",
+	expected_stderr => qr/invalid channel_binding value: "invalid_value"/);
 $node->connect_ok("$common_connstr user=ssltestuser channel_binding=disable",
 	"SCRAM with SSL and channel_binding=disable");
 if ($supports_tls_server_end_point)
@@ -74,15 +74,15 @@ else
 {
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser channel_binding=require",
-		qr/channel binding is required, but server did not offer an authentication method that supports channel binding/,
-		"SCRAM with SSL and channel_binding=require");
+		"SCRAM with SSL and channel_binding=require",
+		expected_stderr => qr/channel binding is required, but server did not offer an authentication method that supports channel binding/);
 }
 
 # Now test when the user has an MD5-encrypted password; should fail
 $node->connect_fails(
 	"$common_connstr user=md5testuser channel_binding=require",
-	qr/channel binding required but not supported by server's authentication request/,
-	"MD5 with SSL and channel_binding=require");
+	"MD5 with SSL and channel_binding=require",
+	expected_stderr => qr/channel binding required but not supported by server's authentication request/);
 
 # Now test with auth method 'cert' by connecting to 'certdb'. Should fail,
 # because channel binding is not performed.  Note that ssl/client.key may
@@ -93,8 +93,8 @@ copy("ssl/client.key", $client_tmp_key);
 chmod 0600, $client_tmp_key;
 $node->connect_fails(
 	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=certdb user=ssltestuser channel_binding=require",
-	qr/channel binding required, but server authenticated client without channel binding/,
-	"Cert authentication and channel_binding=require");
+	"Cert authentication and channel_binding=require",
+	expected_stderr => qr/channel binding required, but server authenticated client without channel binding/);
 
 # clean up
 unlink($client_tmp_key);
-- 
2.31.0

#66Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#65)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Sat, Apr 03, 2021 at 09:30:25PM +0900, Michael Paquier wrote:

Slight rebase for this one to take care of the updates with the SSL
error messages.

I have been looking again at that and applied it as c50624cd after
some slight modifications. Attached is the main, refactored, patch
that plugs on top of the existing infrastructure. connect_ok() and
connect_fails() gain two parameters each to match or to not match the
logs of the backend, with a truncation of the logs done before any
connection attempt.

I have spent more time reviewing the backend code while on it and
there was one thing that stood out:
+       ereport(FATAL,
+               (errmsg("connection was re-authenticated"),
+                errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+                              port->authn_id, id)));
This message would not actually trigger because auth_failed() is the
code path in charge of showing an error here, so this could just be
replaced by an assertion on authn_id being NULL?  The contents of this
log were a bit in contradiction with the comments a couple of lines
above anyway.  Jacob, what do you think?
--
Michael

Attachments:

v20-0001-Log-authenticated-identity-from-all-auth-backend.patchtext/x-diff; charset=us-asciiDownload
From d8df487fb85ddd1a6ea2f9d7d5f30b72462117ea Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 5 Apr 2021 14:46:21 +0900
Subject: [PATCH v20] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

PostgresNode::connect_ok/fails() have been modified to let tests check
the logfiles for required or prohibited patterns, using the respective
log_like and log_unlike parameters.
---
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/backend/libpq/auth.c                  | 122 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 +++++
 src/test/authentication/t/001_password.pl |  59 +++++++----
 src/test/kerberos/t/001_auth.pl           |  73 +++++++++----
 src/test/ldap/t/001_auth.pl               |  29 +++--
 src/test/perl/PostgresNode.pm             |  72 +++++++++++++
 src/test/ssl/t/001_ssltests.pl            |  36 +++++--
 src/test/ssl/t/002_scram.pl               |  10 +-
 doc/src/sgml/config.sgml                  |   3 +-
 11 files changed, 374 insertions(+), 68 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..94f37e6d87 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,37 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id && port->authn_id == NULL);
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +791,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +853,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1215,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1331,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1561,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1968,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2000,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2031,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
-
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2295,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2333,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2840,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2906,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3101,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 65303ca3f5..150b226c0e 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 23;
 }
 
 
@@ -35,15 +35,12 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
+	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
-
 	$status_string = 'success' if ($expected_res eq 0);
 
 	my $connstr = "user=$role";
@@ -52,18 +49,19 @@ sub test_role
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $testname);
+		$node->connect_ok($connstr, $testname, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $testname);
+		$node->connect_fails($connstr, $testname, %params);
 	}
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -76,26 +74,51 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [qr/connection authenticated:/]);
+test_role($node, 'md5_role', 'trust', 0,
+	log_unlike => [qr/connection authenticated:/]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	log_like =>
+	  [qr/connection authenticated: identity="scram_role" method=password/]);
+test_role($node, 'md5_role', 'password', 0,
+	log_like =>
+	  [qr/connection authenticated: identity="md5_role" method=password/]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role(
+	$node,
+	'scram_role',
+	'scram-sha-256',
+	0,
+	log_like => [
+		qr/connection authenticated: identity="scram_role" method=scram-sha-256/
+	]);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	log_unlike => [qr/connection authenticated:/]);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	log_unlike => [qr/connection authenticated:/]);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	log_like =>
+	  [qr/connection authenticated: identity="scram_role" method=md5/]);
+test_role($node, 'md5_role', 'md5', 0,
+	log_like =>
+	  [qr/connection authenticated: identity="md5_role" method=md5/]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index de52a66785..fd78815366 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 30;
+	plan tests => 42;
 }
 else
 {
@@ -183,39 +183,36 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
-		$expect_log_msg)
+		@expect_log_msgs)
 	  = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my $connstr = $node->connstr('postgres')
 	  . " user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
+	my @log_like = undef;
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		@log_like = \@regexes;
+	}
+
 	if ($expected_res eq 0)
 	{
 		# The result is assumed to match "true", or "t", here.
 		$node->connect_ok(
 			$connstr, $test_name,
 			sql             => $query,
-			expected_stdout => qr/t/);
+			expected_stdout => qr/t/,
+			log_like        => @log_like);
 	}
 	else
 	{
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, log_like => @log_like);
 	}
-
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -238,11 +235,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -254,6 +259,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -264,6 +270,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -273,6 +280,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -309,6 +317,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -318,10 +327,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -335,10 +345,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -346,6 +357,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -362,5 +374,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index ad54854a42..7842dcba0c 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 25;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,17 +163,17 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $test_name);
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
 }
 
@@ -185,12 +186,22 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
-test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
-test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+test_access(
+	$node, 'test0', 2,
+	'simple bind authentication fails if user not found in LDAP',
+	log_unlike => [qr/connection authenticated:/]);
+test_access(
+	$node, 'test1', 2,
+	'simple bind authentication fails with wrong password',
+	log_unlike => [qr/connection authenticated:/]);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access(
+	$node, 'test1', 0,
+	'simple bind authentication succeeds',
+	log_like => [
+		qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/
+	],);
 
 note "search+bind";
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index bbde34c929..d27d0a5295 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1876,6 +1876,17 @@ instead of the default.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular
+expressions to match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular
+expressions that must NOT match against the server log. They will be passed
+to C<Test::More::unlike()>.
+
 =back
 
 =cut
@@ -1895,6 +1906,10 @@ sub connect_ok
 		$sql = "SELECT \$\$connected with $connstr\$\$";
 	}
 
+	# Truncate the logs of the server before doing any optional
+	# matching with the backend logs.
+	truncate $self->logfile, 0;
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1910,6 +1925,27 @@ sub connect_ok
 	{
 		like($stdout, $params{expected_stdout}, "$test_name: matches");
 	}
+
+	if (defined($params{log_like}))
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @{ $params{log_like} })
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+
+	}
+
+	if (defined($params{log_unlike}))
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @{ $params{log_unlike} })
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
@@ -1925,6 +1961,17 @@ to fail.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular
+expressions to match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular
+expressions that must NOT match against the server log. They will be passed
+to C<Test::More::unlike()>.
+
 =back
 
 =cut
@@ -1934,6 +1981,10 @@ sub connect_fails
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $test_name, %params) = @_;
 
+	# Truncate the logs of the server before doing any optional
+	# matching with the backend logs.
+	truncate $self->logfile, 0;
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1948,6 +1999,27 @@ sub connect_fails
 	{
 		like($stderr, $params{expected_stderr}, "$test_name: matches");
 	}
+
+	if (defined($params{log_like}))
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @{ $params{log_like} })
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+	}
+
+	if (defined($params{log_unlike}))
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @{ $params{log_unlike} })
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+
+	}
 }
 
 =pod
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 21865ac30f..85b79180d5 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -431,7 +431,10 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
+	"certificate authorization succeeds with DN mapping",
+	log_like => [
+		qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
+	]);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -445,7 +448,11 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [
+		qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
+	]);
 
 
 
@@ -511,13 +518,18 @@ $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"certificate authorization fails with client cert belonging to another user",
 	expected_stderr =>
-	  qr/certificate authentication failed for user "anotheruser"/);
+	  qr/certificate authentication failed for user "anotheruser"/,
+	# Certificate authentication should be logged even on failure.
+	log_like =>
+	  [qr/connection authenticated: identity="CN=ssltestuser" method=cert/]);
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	"certificate authorization fails with revoked client cert",
-	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
+	# Revoked certificates should not authenticate the user.
+	log_unlike => [qr/connection authenticated:/]);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -527,21 +539,25 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
-);
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [qr/connection authenticated:/],);
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
 	expected_stderr =>
-	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,);
+	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
+	# verify-full does not provide authentication
+	log_unlike => [qr/connection authenticated:/],);
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
-);
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [qr/connection authenticated:/]);
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
 switch_server_cert($node, 'server-cn-only', 'root_ca');
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 583b62b3a1..3cb22ffced 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,14 @@ $node->connect_fails(
 	  qr/channel binding required, but server authenticated client without channel binding/
 );
 
+# Certificate verification at the connection level should still work fine.
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [
+		qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
+	]);
+
 # clean up
 unlink($client_tmp_key);
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0c9128a55d..d47b5eaf1d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6751,7 +6751,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
-- 
2.31.0

#67Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#66)
2 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, 2021-04-05 at 14:47 +0900, Michael Paquier wrote:

On Sat, Apr 03, 2021 at 09:30:25PM +0900, Michael Paquier wrote:

Slight rebase for this one to take care of the updates with the SSL
error messages.

I have been looking again at that and applied it as c50624cd after
some slight modifications.

This loses the test fixes I made in my v19 [1]/messages/by-id/8c08c6402051b5348d599c0e07bbd83f8614fa16.camel@vmware.com; some of the tests on
HEAD aren't testing anything anymore. I've put those fixups into 0001,
attached.

Attached is the main, refactored, patch
that plugs on top of the existing infrastructure. connect_ok() and
connect_fails() gain two parameters each to match or to not match the
logs of the backend, with a truncation of the logs done before any
connection attempt.

It looks like this is a reimplementation of v19, but it loses the
additional tests I wrote? Not sure. Maybe my v19 was sent to spam?

In any case I have attached my Friday patch as 0002.

I have spent more time reviewing the backend code while on it and
there was one thing that stood out:
+       ereport(FATAL,
+               (errmsg("connection was re-authenticated"),
+                errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+                              port->authn_id, id)));
This message would not actually trigger because auth_failed() is the
code path in charge of showing an error here

It triggers just fine for me (you can duplicate one of the
set_authn_id() calls to see):

FATAL: connection was re-authenticated
DETAIL: previous ID: "uid=test2,dc=example,dc=net"; new ID: "uid=test2,dc=example,dc=net"

so this could just be
replaced by an assertion on authn_id being NULL?

An assertion seems like the wrong way to go; in the event that a future
code path accidentally performs a duplicated authentication, the FATAL
will just kill off an attacker's connection, while an assertion will
DoS the server.

The contents of this
log were a bit in contradiction with the comments a couple of lines
above anyway.

What do you mean by this? I took another look at the comment and it
seems to match the implementation.

v21 attached, which is just a rebase of my original v19.

--Jacob

[1]: /messages/by-id/8c08c6402051b5348d599c0e07bbd83f8614fa16.camel@vmware.com

Attachments:

v21-0001-test-kerberos-fix-up-test_query.patchtext/x-patch; name=v21-0001-test-kerberos-fix-up-test_query.patchDownload
From 9a2e8eab5f2d338414652438cce1c759af451c11 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Mon, 5 Apr 2021 09:35:11 -0700
Subject: [PATCH v21 1/2] test/kerberos: fix up test_query

c50624cdd accidentally disabled test_query; add its functionality back
here. expected_stderr has been added to PostgresNode::connect_ok().

Also fix some TODO tests in test/ssl.
---
 src/test/kerberos/t/001_auth.pl | 12 +++++++-----
 src/test/perl/PostgresNode.pm   | 12 ++++++++++--
 src/test/ssl/t/001_ssltests.pl  |  8 ++++----
 3 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index de52a66785..8b8c7c4814 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 30;
+	plan tests => 34;
 }
 else
 {
@@ -196,7 +196,7 @@ sub test_access
 		$node->connect_ok(
 			$connstr, $test_name,
 			sql             => $query,
-			expected_stdout => qr/t/);
+			expected_stdout => qr/^t$/);
 	}
 	else
 	{
@@ -227,9 +227,11 @@ sub test_query
 	my $connstr = $node->connstr('postgres')
 	  . " user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
-	my ($stdoutres, $stderrres);
-
-	$node->connect_ok($connstr, $test_name, $query, $expected);
+	$node->connect_ok(
+		$connstr, $test_name,
+		sql             => $query,
+		expected_stdout => $expected,
+		expected_stderr => qr/^$/);
 	return;
 }
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index bbde34c929..34279e71a8 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1876,6 +1876,10 @@ instead of the default.
 
 If this regular expression is set, matches it with the output generated.
 
+=item expected_stderr => B<value>
+
+If this regular expression is set, matches it with the stderr generated.
+
 =back
 
 =cut
@@ -1908,7 +1912,11 @@ sub connect_ok
 
 	if (defined($params{expected_stdout}))
 	{
-		like($stdout, $params{expected_stdout}, "$test_name: matches");
+		like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
+	}
+	if (defined($params{expected_stderr}))
+	{
+	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
 }
 
@@ -1946,7 +1954,7 @@ sub connect_fails
 
 	if (defined($params{expected_stderr}))
 	{
-		like($stderr, $params{expected_stderr}, "$test_name: matches");
+		like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
 }
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 21865ac30f..0decbe7177 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -459,17 +459,17 @@ TODO:
 	# correct client cert in encrypted PEM with empty password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''",
+		"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
 		expected_stderr =>
-		  qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and empty password in encrypted PEM format"
+		  qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 	# correct client cert in encrypted PEM with no password
 	$node->connect_fails(
 		"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key",
+		"certificate authorization fails with correct client cert and no password in encrypted PEM format",
 		expected_stderr =>
-		  qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!,
-		"certificate authorization fails with correct client cert and no password in encrypted PEM format"
+		  qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!
 	);
 
 }
-- 
2.25.1

v21-0002-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v21-0002-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 8787f21149d5775eb18624f6e658a4590ceb958c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 16:01:27 -0700
Subject: [PATCH v21 2/2] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

PostgresNode::connect_ok/fails() have been modified to let tests check
the logfiles for required or prohibited patterns, using the respective
log_like and log_unlike parameters.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 136 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/test/authentication/t/001_password.pl |  49 +++++---
 src/test/kerberos/t/001_auth.pl           |  79 +++++++++----
 src/test/ldap/t/001_auth.pl               |  32 +++--
 src/test/perl/PostgresNode.pm             |  78 +++++++++++++
 src/test/ssl/t/001_ssltests.pl            |  34 ++++--
 src/test/ssl/t/002_scram.pl               |   8 +-
 11 files changed, 387 insertions(+), 70 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0c9128a55d..d47b5eaf1d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6751,7 +6751,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dee056b0d6 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3115,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 65303ca3f5..775face306 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 23;
 }
 
 
@@ -35,15 +35,12 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
+	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
-
 	$status_string = 'success' if ($expected_res eq 0);
 
 	my $connstr = "user=$role";
@@ -52,18 +49,19 @@ sub test_role
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $testname);
+		$node->connect_ok($connstr, $testname, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $testname);
+		$node->connect_fails($connstr, $testname, %params);
 	}
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -76,26 +74,41 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_role($node, 'md5_role',   'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=password/ ]);
+test_role($node, 'md5_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=password/ ]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role($node, 'scram_role', 'scram-sha-256', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=scram-sha-256/ ]);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=md5/ ]);
+test_role($node, 'md5_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=md5/ ]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 8b8c7c4814..75308eced8 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 34;
+	plan tests => 46;
 }
 else
 {
@@ -183,39 +183,36 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
-		$expect_log_msg)
+		@expect_log_msgs)
 	  = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my $connstr = $node->connstr('postgres')
 	  . " user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
+	my %params = (
+		sql => $query,
+	);
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		$params{log_like} = \@regexes;
+	}
+
 	if ($expected_res eq 0)
 	{
 		# The result is assumed to match "true", or "t", here.
-		$node->connect_ok(
-			$connstr, $test_name,
-			sql             => $query,
-			expected_stdout => qr/^t$/);
+		$params{expected_stdout} = qr/^t$/;
+
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
-
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -240,11 +237,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -256,6 +261,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -266,6 +272,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -275,6 +282,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -311,6 +319,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -320,10 +329,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -337,10 +347,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -348,6 +359,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -364,5 +376,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index ad54854a42..df881c295e 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 28;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,17 +163,17 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $test_name);
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
 }
 
@@ -186,11 +187,16 @@ $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
 test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
+	'simple bind authentication fails if user not found in LDAP',
+	log_unlike => [ qr/connection authenticated:/ ]);
 test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+	'simple bind authentication fails with wrong password',
+	log_unlike => [ qr/connection authenticated:/ ]);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access($node, 'test1', 0, 'simple bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search+bind";
 
@@ -206,7 +212,9 @@ test_access($node, 'test0', 2,
 test_access($node, 'test1', 2,
 	'search+bind authentication fails with wrong password');
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search+bind authentication succeeds');
+test_access($node, 'test1', 0, 'search+bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "multiple servers";
 
@@ -250,9 +258,13 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search filter finds by uid');
+test_access($node, 'test1', 0, 'search filter finds by uid',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 $ENV{"PGPASSWORD"} = 'secret2';
-test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail',
+	log_like => [ qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search filters in LDAP URLs";
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index 34279e71a8..a5d66c8052 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1880,6 +1880,19 @@ If this regular expression is set, matches it with the output generated.
 
 If this regular expression is set, matches it with the stderr generated.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must NOT match against the server log. They will be passed to
+C<Test::More::unlike()>.
+
+=back
+
 =back
 
 =cut
@@ -1899,6 +1912,22 @@ sub connect_ok
 		$sql = "SELECT \$\$connected with $connstr\$\$";
 	}
 
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1918,6 +1947,19 @@ sub connect_ok
 	{
 	    like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
@@ -1933,6 +1975,12 @@ to fail.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+See C<connect_ok()>, above.
+
 =back
 
 =cut
@@ -1942,6 +1990,22 @@ sub connect_fails
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $test_name, %params) = @_;
 
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ delete $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ delete $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1956,6 +2020,20 @@ sub connect_fails
 	{
 		like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
 	}
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 0decbe7177..b1949e2903 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -431,7 +431,9 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
+	"certificate authorization succeeds with DN mapping",
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -445,7 +447,10 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 
 
@@ -511,13 +516,19 @@ $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"certificate authorization fails with client cert belonging to another user",
 	expected_stderr =>
-	  qr/certificate authentication failed for user "anotheruser"/);
+	  qr/certificate authentication failed for user "anotheruser"/,
+	# certificate authentication should be logged even on failure
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser" method=cert/ ],
+);
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	"certificate authorization fails with revoked client cert",
-	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
+	# revoked certificates should not authenticate the user
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -527,20 +538,27 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
 	expected_stderr =>
-	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,);
+	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 583b62b3a1..0280d0a1ef 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,12 @@ $node->connect_fails(
 	  qr/channel binding required, but server authenticated client without channel binding/
 );
 
+# Certificate verification at the connection level should still work fine.
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [ qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]);
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#68Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#67)
Re: Proposal: Save user's original authenticated identity for logging

On Mon, Apr 05, 2021 at 04:40:41PM +0000, Jacob Champion wrote:

This loses the test fixes I made in my v19 [1]; some of the tests on
HEAD aren't testing anything anymore. I've put those fixups into 0001,
attached.

Argh, Thanks. The part about not checking after the error output when
the connection should pass is wanted to be more consistent with the
other test suites. So I have removed this part and applied the rest
of 0001.

It looks like this is a reimplementation of v19, but it loses the
additional tests I wrote? Not sure.

So, what you have here are three extra tests for ldap with
search+bind and search filters. This looks like a good idea.

Maybe my v19 was sent to spam?

Indeed. All those messages are finishing in my spam folder. I am
wondering why actually. That's a bit surprising.

It triggers just fine for me (you can duplicate one of the
set_authn_id() calls to see):

FATAL: connection was re-authenticated
DETAIL: previous ID: "uid=test2,dc=example,dc=net"; new ID: "uid=test2,dc=example,dc=net"

Hmm. It looks like I did something wrong here.

An assertion seems like the wrong way to go; in the event that a future
code path accidentally performs a duplicated authentication, the FATAL
will just kill off an attacker's connection, while an assertion will
DoS the server.

Hmm. You are making a good point here, but is that really the best
thing we can do? We lose the context of the authentication type being
done with this implementation, and the client would know that it did a
re-authentication even if the logdetail goes only to the backend's
logs. Wouldn't it be better, for instance, to generate a LOG message
in this code path, switch to STATUS_ERROR to let auth_failed()
generate the FATAL message? set_authn_id() could just return a
boolean to tell if it was OK with the change in authn_id or not.

v21 attached, which is just a rebase of my original v19.

This requires a perltidy run from what I can see, but that's no big
deal.

+   my (@log_like, @log_unlike);
+   if (defined($params{log_like}))
+   {
+       @log_like = @{ delete $params{log_like} };
+   }
+   if (defined($params{log_unlike}))
+   {
+       @log_unlike = @{ delete $params{log_unlike} };
+   }
There is no need for that?  This removal was done as %params was
passed down directly as-is to PostgresNode::psql, but that's not the
case anymore.
--
Michael
#69Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#68)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, 2021-04-06 at 14:15 +0900, Michael Paquier wrote:

On Mon, Apr 05, 2021 at 04:40:41PM +0000, Jacob Champion wrote:

This loses the test fixes I made in my v19 [1]; some of the tests on
HEAD aren't testing anything anymore. I've put those fixups into 0001,
attached.

Argh, Thanks. The part about not checking after the error output when
the connection should pass is wanted to be more consistent with the
other test suites. So I have removed this part and applied the rest
of 0001.

I assumed Tom added those checks to catch a particular failure mode for
the GSS encryption case. (I guess Tom would know for sure.)

An assertion seems like the wrong way to go; in the event that a future
code path accidentally performs a duplicated authentication, the FATAL
will just kill off an attacker's connection, while an assertion will
DoS the server.

Hmm. You are making a good point here, but is that really the best
thing we can do? We lose the context of the authentication type being
done with this implementation, and the client would know that it did a
re-authentication even if the logdetail goes only to the backend's
logs. Wouldn't it be better, for instance, to generate a LOG message
in this code path, switch to STATUS_ERROR to let auth_failed()
generate the FATAL message? set_authn_id() could just return a
boolean to tell if it was OK with the change in authn_id or not.

My concern there is that we already know the code is wrong in this
(hypothetical future) case, and then we'd be relying on that wrong code
to correctly bubble up an error status. I think that, once you hit this
code path, the program flow should be interrupted immediately -- do not
pass Go, collect $200, or let the bad implementation continue to do
more damage.

I agree that losing the context is not ideal. To avoid that, I thought
it might be nice to add errbacktrace() to the ereport() call -- but
since the functions we're interested in are static, the backtrace
doesn't help. (I should check to see whether libbacktrace is better in
this situation. Later.)

As for the client knowing: an active attacker is probably going to know
that they're triggering the reauthentication anyway. So the primary
disadvantage I see is that a more passive attacker could scan for some
vulnerability by looking for that error message.

If that's a major concern, we could call auth_failed() directly from
this code. But that means that the auth_failed() logic must not give
them more ammunition, in this hypothetical scenario where the authn
system is already messed up. Obscuring the failure mode helps buy
people time to update Postgres, which definitely has value, but it
won't prevent any actual exploit by the time we get to this check. A
tricky trade-off.

v21 attached, which is just a rebase of my original v19.

This requires a perltidy run from what I can see, but that's no big
deal.

Is that done per-patch? It looks like there's a large amount of
untidied code in src/test in general, and in the files being touched.

+   my (@log_like, @log_unlike);
+   if (defined($params{log_like}))
+   {
+       @log_like = @{ delete $params{log_like} };
+   }
+   if (defined($params{log_unlike}))
+   {
+       @log_unlike = @{ delete $params{log_unlike} };
+   }
There is no need for that?  This removal was done as %params was
passed down directly as-is to PostgresNode::psql, but that's not the
case anymore.

Fixed in v22, thanks.

--Jacob

Attachments:

v22-0001-Log-authenticated-identity-from-all-auth-backend.patchtext/x-patch; name=v22-0001-Log-authenticated-identity-from-all-auth-backend.patchDownload
From 731112be1a29e873b96f982311c7bc35b6d864ec Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Thu, 1 Apr 2021 16:01:27 -0700
Subject: [PATCH v22] Log authenticated identity from all auth backends

The "authenticated identity" is the string used by an auth method to
identify a particular user. In many common cases this is the same as the
Postgres username, but for some third-party auth methods, the identifier
in use may be shortened or otherwise translated (e.g. through pg_ident
mappings) before the server stores it.

To help DBAs see who has actually interacted with the system, store the
original identity when authentication succeeds, and expose it via the
log_connections setting. The log entries look something like this
example (where a local user named "pchampion" is connecting to the
database as the "admin" user):

    LOG:  connection received: host=[local]
    LOG:  connection authenticated: identity="pchampion" method=peer (/data/pg_hba.conf:88)
    LOG:  connection authorized: user=admin database=postgres application_name=psql

port->authn_id is set according to the auth method:

    bsd: the Postgres username (which is the local username)
    cert: the client's Subject DN
    gss: the user principal
    ident: the remote username
    ldap: the final bind DN
    pam: the Postgres username (which is the PAM username)
    password (and all pw-challenge methods): the Postgres username
    peer: the peer's pw_name
    radius: the Postgres username (which is the RADIUS username)
    sspi: either the down-level (SAM-compatible) logon name, if
          compat_realm=1, or the User Principal Name if compat_realm=0

The trust auth method does not set an authenticated identity. Neither
does using clientcert=verify-full (use the cert auth method instead).

PostgresNode::connect_ok/fails() have been modified to let tests check
the logfiles for required or prohibited patterns, using the respective
log_like and log_unlike parameters.
---
 doc/src/sgml/config.sgml                  |   3 +-
 src/backend/libpq/auth.c                  | 136 ++++++++++++++++++++--
 src/backend/libpq/hba.c                   |  24 ++++
 src/include/libpq/hba.h                   |   1 +
 src/include/libpq/libpq-be.h              |  13 +++
 src/test/authentication/t/001_password.pl |  49 +++++---
 src/test/kerberos/t/001_auth.pl           |  79 +++++++++----
 src/test/ldap/t/001_auth.pl               |  32 +++--
 src/test/perl/PostgresNode.pm             |  76 ++++++++++++
 src/test/ssl/t/001_ssltests.pl            |  34 ++++--
 src/test/ssl/t/002_scram.pl               |   8 +-
 11 files changed, 385 insertions(+), 70 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index effc60c07b..e51639d56c 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6755,7 +6755,8 @@ local0.*    /var/log/postgresql
       <listitem>
        <para>
         Causes each attempted connection to the server to be logged,
-        as well as successful completion of client authentication.
+        as well as successful completion of both client authentication (if
+        necessary) and authorization.
         Only superusers can change this parameter at session start,
         and it cannot be changed at all within a session.
         The default is <literal>off</literal>.
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 9dc28e19aa..dee056b0d6 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -34,8 +34,10 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "port/pg_bswap.h"
+#include "postmaster/postmaster.h"
 #include "replication/walsender.h"
 #include "storage/ipc.h"
+#include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/timestamp.h"
 
@@ -47,6 +49,7 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
+static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -337,6 +340,51 @@ auth_failed(Port *port, int status, char *logdetail)
 }
 
 
+/*
+ * Sets the authenticated identity for the current user.  The provided string
+ * will be copied into the TopMemoryContext.  The ID will be logged if
+ * log_connections is enabled.
+ *
+ * Auth methods should call this routine exactly once, as soon as the user is
+ * successfully authenticated, even if they have reasons to know that
+ * authorization will fail later.
+ *
+ * The provided string will be copied into TopMemoryContext, to match the
+ * lifetime of the Port, so it is safe to pass a string that is managed by an
+ * external library.
+ */
+static void
+set_authn_id(Port *port, const char *id)
+{
+	Assert(id);
+
+	if (port->authn_id)
+	{
+		/*
+		 * An existing authn_id should never be overwritten; that means two
+		 * authentication providers are fighting (or one is fighting itself).
+		 * Don't leak any authn details to the client, but don't let the
+		 * connection continue, either.
+		 */
+		ereport(FATAL,
+				(errmsg("connection was re-authenticated"),
+				 errdetail_log("previous ID: \"%s\"; new ID: \"%s\"",
+							   port->authn_id, id)));
+	}
+
+	port->authn_id = MemoryContextStrdup(TopMemoryContext, id);
+
+	if (Log_connections)
+	{
+		ereport(LOG,
+				errmsg("connection authenticated: identity=\"%s\" method=%s "
+					   "(%s:%d)",
+					   port->authn_id, hba_authname(port), HbaFileName,
+					   port->hba->linenumber));
+	}
+}
+
+
 /*
  * Client authentication starts here.  If there is an error, this
  * function does not return and the backend process is terminated.
@@ -757,6 +805,9 @@ CheckPasswordAuth(Port *port, char **logdetail)
 		pfree(shadow_pass);
 	pfree(passwd);
 
+	if (result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return result;
 }
 
@@ -816,6 +867,10 @@ CheckPWChallengeAuth(Port *port, char **logdetail)
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
 	}
+
+	if (auth_result == STATUS_OK)
+		set_authn_id(port, port->user_name);
+
 	return auth_result;
 }
 
@@ -1174,8 +1229,13 @@ pg_GSS_checkauth(Port *port)
 	/*
 	 * Copy the original name of the authenticated principal into our backend
 	 * memory for display later.
+	 *
+	 * This is also our authenticated identity.  Set it now, rather than
+	 * waiting for the usermap check below, because authentication has already
+	 * succeeded and we want the log file to reflect that.
 	 */
 	port->gss->princ = MemoryContextStrdup(TopMemoryContext, gbuf.value);
+	set_authn_id(port, gbuf.value);
 
 	/*
 	 * Split the username at the realm separator
@@ -1285,6 +1345,7 @@ pg_SSPI_recvauth(Port *port)
 	DWORD		domainnamesize = sizeof(domainname);
 	SID_NAME_USE accountnameuse;
 	HMODULE		secur32;
+	char	   *authn_id;
 
 	QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken;
 
@@ -1514,6 +1575,26 @@ pg_SSPI_recvauth(Port *port)
 			return status;
 	}
 
+	/*
+	 * We have all of the information necessary to construct the authenticated
+	 * identity.  Set it now, rather than waiting for check_usermap below,
+	 * because authentication has already succeeded and we want the log file
+	 * to reflect that.
+	 */
+	if (port->hba->compat_realm)
+	{
+		/* SAM-compatible format. */
+		authn_id = psprintf("%s\\%s", domainname, accountname);
+	}
+	else
+	{
+		/* Kerberos principal format. */
+		authn_id = psprintf("%s@%s", accountname, domainname);
+	}
+
+	set_authn_id(port, authn_id);
+	pfree(authn_id);
+
 	/*
 	 * Compare realm/domain if requested. In SSPI, always compare case
 	 * insensitive.
@@ -1901,8 +1982,15 @@ ident_inet_done:
 		pg_freeaddrinfo_all(local_addr.addr.ss_family, la);
 
 	if (ident_return)
-		/* Success! Check the usermap */
+	{
+		/*
+		 * Success!  Store the identity, then check the usermap. Note that
+		 * setting the authenticated identity is done before checking the
+		 * usermap, because at this point authentication has succeeded.
+		 */
+		set_authn_id(port, ident_user);
 		return check_usermap(port->hba->usermap, port->user_name, ident_user, false);
+	}
 	return STATUS_ERROR;
 }
 
@@ -1926,7 +2014,6 @@ auth_peer(hbaPort *port)
 	gid_t		gid;
 #ifndef WIN32
 	struct passwd *pw;
-	char	   *peer_user;
 	int			ret;
 #endif
 
@@ -1958,12 +2045,14 @@ auth_peer(hbaPort *port)
 		return STATUS_ERROR;
 	}
 
-	/* Make a copy of static getpw*() result area. */
-	peer_user = pstrdup(pw->pw_name);
-
-	ret = check_usermap(port->hba->usermap, port->user_name, peer_user, false);
+	/*
+	 * Make a copy of static getpw*() result area; this is our authenticated
+	 * identity.  Set it before calling check_usermap, because authentication
+	 * has already succeeded and we want the log file to reflect that.
+	 */
+	set_authn_id(port, pw->pw_name);
 
-	pfree(peer_user);
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false);
 
 	return ret;
 #else
@@ -2220,6 +2309,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 
 	pam_passwd = NULL;			/* Unset pam_passwd */
 
+	if (retval == PAM_SUCCESS)
+		set_authn_id(port, user);
+
 	return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR);
 }
 #endif							/* USE_PAM */
@@ -2255,6 +2347,7 @@ CheckBSDAuth(Port *port, char *user)
 	if (!retval)
 		return STATUS_ERROR;
 
+	set_authn_id(port, user);
 	return STATUS_OK;
 }
 #endif							/* USE_BSD_AUTH */
@@ -2761,6 +2854,9 @@ CheckLDAPAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	/* Save the original bind DN as the authenticated identity. */
+	set_authn_id(port, fulluser);
+
 	ldap_unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
@@ -2824,6 +2920,30 @@ CheckCertAuth(Port *port)
 		return STATUS_ERROR;
 	}
 
+	if (port->hba->auth_method == uaCert)
+	{
+		/*
+		 * For cert auth, the client's Subject DN is always our authenticated
+		 * identity, even if we're only using its CN for authorization.  Set
+		 * it now, rather than waiting for check_usermap() below, because
+		 * authentication has already succeeded and we want the log file to
+		 * reflect that.
+		 */
+		if (!port->peer_dn)
+		{
+			/*
+			 * This should not happen as both peer_dn and peer_cn should be
+			 * set in this context.
+			 */
+			ereport(LOG,
+					(errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN",
+							port->user_name)));
+			return STATUS_ERROR;
+		}
+
+		set_authn_id(port, port->peer_dn);
+	}
+
 	/* Just pass the certificate cn/dn to the usermap check */
 	status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false);
 	if (status_check_usermap != STATUS_OK)
@@ -2995,6 +3115,8 @@ CheckRADIUSAuth(Port *port)
 		 */
 		if (ret == STATUS_OK)
 		{
+			set_authn_id(port, port->user_name);
+
 			pfree(passwd);
 			return STATUS_OK;
 		}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index feb711a6ef..b720b03e9a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -3141,3 +3141,27 @@ hba_getauthmethod(hbaPort *port)
 {
 	check_hba(port);
 }
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(hbaPort *port)
+{
+	UserAuth	auth_method;
+
+	Assert(port->hba);
+	auth_method = port->hba->auth_method;
+
+	if (auth_method < 0 || USER_AUTH_LAST < auth_method)
+	{
+		/* Should never happen. */
+		elog(FATAL, "port has out-of-bounds UserAuth: %d", auth_method);
+	}
+
+	return UserAuthName[auth_method];
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 1ec8603da7..63f2962139 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -137,6 +137,7 @@ typedef struct Port hbaPort;
 
 extern bool load_hba(void);
 extern bool load_ident(void);
+extern const char *hba_authname(hbaPort *port);
 extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 713c34fedd..02015efe13 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -159,6 +159,19 @@ typedef struct Port
 	 */
 	HbaLine    *hba;
 
+	/*
+	 * Authenticated identity.  The meaning of this identifier is dependent on
+	 * hba->auth_method; it is the identity (if any) that the user presented
+	 * during the authentication cycle, before they were assigned a database
+	 * role.  (It is effectively the "SYSTEM-USERNAME" of a pg_ident usermap
+	 * -- though the exact string in use may be different, depending on pg_hba
+	 * options.)
+	 *
+	 * authn_id is NULL if the user has not actually been authenticated, for
+	 * example if the "trust" auth method is in use.
+	 */
+	const char *authn_id;
+
 	/*
 	 * TCP keepalive and user timeout settings.
 	 *
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 65303ca3f5..775face306 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -17,7 +17,7 @@ if (!$use_unix_sockets)
 }
 else
 {
-	plan tests => 13;
+	plan tests => 23;
 }
 
 
@@ -35,15 +35,12 @@ sub reset_pg_hba
 	return;
 }
 
-# Test access for a single role, useful to wrap all tests into one.
+# Test access for a single role, useful to wrap all tests into one.  Extra
+# named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
-	my $node          = shift;
-	my $role          = shift;
-	my $method        = shift;
-	my $expected_res  = shift;
+	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
-
 	$status_string = 'success' if ($expected_res eq 0);
 
 	my $connstr = "user=$role";
@@ -52,18 +49,19 @@ sub test_role
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $testname);
+		$node->connect_ok($connstr, $testname, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $testname);
+		$node->connect_fails($connstr, $testname, %params);
 	}
 }
 
 # Initialize primary node
 my $node = get_new_node('primary');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 # Create 3 roles with different password methods for each one. The same
@@ -76,26 +74,41 @@ $node->safe_psql('postgres',
 );
 $ENV{"PGPASSWORD"} = 'pass';
 
-# For "trust" method, all users should be able to connect.
+# For "trust" method, all users should be able to connect. These users are not
+# considered to be authenticated.
 reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0);
-test_role($node, 'md5_role',   'trust', 0);
+test_role($node, 'scram_role', 'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
+test_role($node, 'md5_role',   'trust', 0,
+	log_unlike => [ qr/connection authenticated:/ ]);
 
 # For plain "password" method, all users should also be able to connect.
 reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0);
-test_role($node, 'md5_role',   'password', 0);
+test_role($node, 'scram_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=password/ ]);
+test_role($node, 'md5_role', 'password', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=password/ ]);
 
 # For "scram-sha-256" method, user "scram_role" should be able to connect.
 reset_pg_hba($node, 'scram-sha-256');
-test_role($node, 'scram_role', 'scram-sha-256', 0);
-test_role($node, 'md5_role',   'scram-sha-256', 2);
+test_role($node, 'scram_role', 'scram-sha-256', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=scram-sha-256/ ]);
+test_role($node, 'md5_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+
+# Test that bad passwords are rejected.
+$ENV{"PGPASSWORD"} = 'badpass';
+test_role($node, 'scram_role', 'scram-sha-256', 2,
+	log_unlike => [ qr/connection authenticated:/ ]);
+$ENV{"PGPASSWORD"} = 'pass';
 
 # For "md5" method, all users should be able to connect (SCRAM
 # authentication will be performed for the user with a SCRAM secret.)
 reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0);
-test_role($node, 'md5_role',   'md5', 0);
+test_role($node, 'scram_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="scram_role" method=md5/ ]);
+test_role($node, 'md5_role', 'md5', 0,
+	log_like => [ qr/connection authenticated: identity="md5_role" method=md5/ ]);
 
 # Tests for channel binding without SSL.
 # Using the password authentication method; channel binding can't work
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index 8db1829460..26c2c7077b 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -20,7 +20,7 @@ use Time::HiRes qw(usleep);
 
 if ($ENV{with_gssapi} eq 'yes')
 {
-	plan tests => 32;
+	plan tests => 44;
 }
 else
 {
@@ -183,39 +183,36 @@ note "running tests";
 sub test_access
 {
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
-		$expect_log_msg)
+		@expect_log_msgs)
 	  = @_;
 
 	# need to connect over TCP/IP for Kerberos
 	my $connstr = $node->connstr('postgres')
 	  . " user=$role host=$host hostaddr=$hostaddr $gssencmode";
 
+	my %params = (
+		sql => $query,
+	);
+
+	if (@expect_log_msgs)
+	{
+		# Match every message literally.
+		my @regexes = map { qr/\Q$_\E/ } @expect_log_msgs;
+
+		$params{log_like} = \@regexes;
+	}
+
 	if ($expected_res eq 0)
 	{
 		# The result is assumed to match "true", or "t", here.
-		$node->connect_ok(
-			$connstr, $test_name,
-			sql             => $query,
-			expected_stdout => qr/^t$/);
+		$params{expected_stdout} = qr/^t$/;
+
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
-
-	# Verify specified log message is logged in the log file.
-	if ($expect_log_msg ne '')
-	{
-		my $first_logfile = slurp_file($node->logfile);
-
-		like($first_logfile, qr/\Q$expect_log_msg\E/,
-			 'found expected log file content');
-	}
-
-	# Clean up any existing contents in the node's log file so as
-	# future tests don't step on each other's generated contents.
-	truncate $node->logfile, 0;
-	return;
 }
 
 # As above, but test for an arbitrary query result.
@@ -239,11 +236,19 @@ $node->append_conf('pg_hba.conf',
 	qq{host all all $hostaddr/32 gss map=mymap});
 $node->restart;
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket', '');
+test_access($node, 'test1', 'SELECT true', 2, '', 'fails without ticket');
 
 run_log [ $kinit, 'test1' ], \$test1_password or BAIL_OUT($?);
 
-test_access($node, 'test1', 'SELECT true', 2, '', 'fails without mapping', '');
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails without mapping',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
+	"no match in usermap \"mymap\" for user \"test1\"");
 
 $node->append_conf('pg_ident.conf', qq{mymap  /^(.*)\@$realm\$  \\1});
 $node->restart;
@@ -255,6 +260,7 @@ test_access(
 	0,
 	'',
 	'succeeds with mapping with default gssencmode and host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -265,6 +271,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -274,6 +281,7 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required with host hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 
@@ -310,6 +318,7 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access(
@@ -319,10 +328,11 @@ test_access(
 	0,
 	'gssencmode=require',
 	'succeeds with GSS-encrypted access required and hostgssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable',
-	'fails with GSS encryption disabled and hostgssenc hba', '');
+	'fails with GSS encryption disabled and hostgssenc hba');
 
 unlink($node->data_dir . '/pg_hba.conf');
 $node->append_conf('pg_hba.conf',
@@ -336,10 +346,11 @@ test_access(
 	0,
 	'gssencmode=prefer',
 	'succeeds with GSS-encrypted access preferred and hostnogssenc hba, but no encryption',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=require',
-	'fails with GSS-encrypted access required and hostnogssenc hba', '');
+	'fails with GSS-encrypted access required and hostnogssenc hba');
 test_access(
 	$node,
 	'test1',
@@ -347,6 +358,7 @@ test_access(
 	0,
 	'gssencmode=disable',
 	'succeeds with GSS encryption disabled and hostnogssenc hba',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=no, principal=test1\@$realm)"
 );
 
@@ -363,5 +375,22 @@ test_access(
 	0,
 	'',
 	'succeeds with include_realm=0 and defaults',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss",
 	"connection authorized: user=$username database=$dbname application_name=$application GSS (authenticated=yes, encrypted=yes, principal=test1\@$realm)"
 );
+
+# Reset pg_hba.conf, and cause a usermap failure with an authentication
+# that has passed.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{host all all $hostaddr/32 gss include_realm=0 krb_realm=EXAMPLE.ORG});
+$node->restart;
+
+test_access(
+	$node,
+	'test1',
+	'SELECT true',
+	2,
+	'',
+	'fails with wrong krb_realm, but still authenticates',
+	"connection authenticated: identity=\"test1\@$realm\" method=gss");
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index ad54854a42..df881c295e 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -6,7 +6,7 @@ use Test::More;
 
 if ($ENV{with_ldap} eq 'yes')
 {
-	plan tests => 22;
+	plan tests => 28;
 }
 else
 {
@@ -152,6 +152,7 @@ note "setting up PostgreSQL instance";
 
 my $node = get_new_node('node');
 $node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
 $node->start;
 
 $node->safe_psql('postgres', 'CREATE USER test0;');
@@ -162,17 +163,17 @@ note "running tests";
 
 sub test_access
 {
-	my ($node, $role, $expected_res, $test_name) = @_;
+	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
 	if ($expected_res eq 0)
 	{
-		$node->connect_ok($connstr, $test_name);
+		$node->connect_ok($connstr, $test_name, %params);
 	}
 	else
 	{
 		# No checks of the error message, only the status code.
-		$node->connect_fails($connstr, $test_name);
+		$node->connect_fails($connstr, $test_name, %params);
 	}
 }
 
@@ -186,11 +187,16 @@ $node->restart;
 
 $ENV{"PGPASSWORD"} = 'wrong';
 test_access($node, 'test0', 2,
-	'simple bind authentication fails if user not found in LDAP');
+	'simple bind authentication fails if user not found in LDAP',
+	log_unlike => [ qr/connection authenticated:/ ]);
 test_access($node, 'test1', 2,
-	'simple bind authentication fails with wrong password');
+	'simple bind authentication fails with wrong password',
+	log_unlike => [ qr/connection authenticated:/ ]);
+
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'simple bind authentication succeeds');
+test_access($node, 'test1', 0, 'simple bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search+bind";
 
@@ -206,7 +212,9 @@ test_access($node, 'test0', 2,
 test_access($node, 'test1', 2,
 	'search+bind authentication fails with wrong password');
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search+bind authentication succeeds');
+test_access($node, 'test1', 0, 'search+bind authentication succeeds',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 
 note "multiple servers";
 
@@ -250,9 +258,13 @@ $node->append_conf('pg_hba.conf',
 $node->restart;
 
 $ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 0, 'search filter finds by uid');
+test_access($node, 'test1', 0, 'search filter finds by uid',
+	log_like => [ qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],
+);
 $ENV{"PGPASSWORD"} = 'secret2';
-test_access($node, 'test2@example.net', 0, 'search filter finds by mail');
+test_access($node, 'test2@example.net', 0, 'search filter finds by mail',
+	log_like => [ qr/connection authenticated: identity="uid=test2,dc=example,dc=net" method=ldap/ ],
+);
 
 note "search filters in LDAP URLs";
 
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index ec202f1b6e..7f93c080d2 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1876,6 +1876,17 @@ instead of the default.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must match against the server log, using C<Test::More::like()>.
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+If given, it must be an array reference containing a list of regular expressions
+that must NOT match against the server log. They will be passed to
+C<Test::More::unlike()>.
+
 =back
 
 =cut
@@ -1895,6 +1906,22 @@ sub connect_ok
 		$sql = "SELECT \$\$connected with $connstr\$\$";
 	}
 
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1910,6 +1937,19 @@ sub connect_ok
 	{
 		like($stdout, $params{expected_stdout}, "$test_name: matches");
 	}
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
@@ -1925,6 +1965,12 @@ to fail.
 
 If this regular expression is set, matches it with the output generated.
 
+=item log_like => [ qr/required message/ ]
+
+=item log_unlike => [ qr/prohibited message/ ]
+
+See C<connect_ok()>, above.
+
 =back
 
 =cut
@@ -1934,6 +1980,22 @@ sub connect_fails
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my ($self, $connstr, $test_name, %params) = @_;
 
+	my (@log_like, @log_unlike);
+	if (defined($params{log_like}))
+	{
+		@log_like = @{ $params{log_like} };
+	}
+	if (defined($params{log_unlike}))
+	{
+		@log_unlike = @{ $params{log_unlike} };
+	}
+
+	if (@log_like or @log_unlike)
+	{
+		# Don't let previous log entries match for this connection.
+		truncate $self->logfile, 0;
+	}
+
 	# Never prompt for a password, any callers of this routine should
 	# have set up things properly, and this should not block.
 	my ($ret, $stdout, $stderr) = $self->psql(
@@ -1948,6 +2010,20 @@ sub connect_fails
 	{
 		like($stderr, $params{expected_stderr}, "$test_name: matches");
 	}
+
+	if (@log_like or @log_unlike)
+	{
+		my $log_contents = TestLib::slurp_file($self->logfile);
+
+		while (my $regex = shift @log_like)
+		{
+			like($log_contents, $regex, "$test_name: log matches");
+		}
+		while (my $regex = shift @log_unlike)
+		{
+			unlike($log_contents, $regex, "$test_name: log does not match");
+		}
+	}
 }
 
 =pod
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 0decbe7177..b1949e2903 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -17,7 +17,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 103;
+	plan tests => 110;
 }
 
 #### Some configuration
@@ -431,7 +431,9 @@ my $dn_connstr = "$common_connstr dbname=certdb_dn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with DN mapping");
+	"certificate authorization succeeds with DN mapping",
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 # same thing but with a regex
 $dn_connstr = "$common_connstr dbname=certdb_dn_re";
@@ -445,7 +447,10 @@ $dn_connstr = "$common_connstr dbname=certdb_cn";
 
 $node->connect_ok(
 	"$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=ssl/client-dn_tmp.key",
-	"certificate authorization succeeds with CN mapping");
+	"certificate authorization succeeds with CN mapping",
+	# the full DN should still be used as the authenticated identity
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/ ],
+);
 
 
 
@@ -511,13 +516,19 @@ $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"certificate authorization fails with client cert belonging to another user",
 	expected_stderr =>
-	  qr/certificate authentication failed for user "anotheruser"/);
+	  qr/certificate authentication failed for user "anotheruser"/,
+	# certificate authentication should be logged even on failure
+	log_like => [ qr/connection authenticated: identity="CN=ssltestuser" method=cert/ ],
+);
 
 # revoked client cert
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key",
 	"certificate authorization fails with revoked client cert",
-	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+	expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
+	# revoked certificates should not authenticate the user
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-option verify-full in pg_hba:
 # works, iff username matches Common Name
@@ -527,20 +538,27 @@ $common_connstr =
 
 $node->connect_ok(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-full succeeds with matching username and Common Name"
+	"auth_option clientcert=verify-full succeeds with matching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 $node->connect_fails(
 	"$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
 	"auth_option clientcert=verify-full fails with mismatching username and Common Name",
 	expected_stderr =>
-	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,);
+	  qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
+);
 
 # Check that connecting with auth-optionverify-ca in pg_hba :
 # works, when username doesn't match Common Name
 $node->connect_ok(
 	"$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key",
-	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name"
+	"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
+	# verify-full does not provide authentication
+	log_unlike => [ qr/connection authenticated:/ ],
 );
 
 # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 583b62b3a1..0280d0a1ef 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -27,7 +27,7 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
 my $supports_tls_server_end_point =
   check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
 
-my $number_of_tests = $supports_tls_server_end_point ? 9 : 10;
+my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
 
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
@@ -102,6 +102,12 @@ $node->connect_fails(
 	  qr/channel binding required, but server authenticated client without channel binding/
 );
 
+# Certificate verification at the connection level should still work fine.
+$node->connect_ok(
+	"sslcert=ssl/client.crt sslkey=$client_tmp_key sslrootcert=invalid hostaddr=$SERVERHOSTADDR dbname=verifydb user=ssltestuser channel_binding=require",
+	"SCRAM with clientcert=verify-full and channel_binding=require",
+	log_like => [ qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]);
+
 # clean up
 unlink($client_tmp_key);
 
-- 
2.25.1

#70Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#69)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, Apr 06, 2021 at 06:31:16PM +0000, Jacob Champion wrote:

On Tue, 2021-04-06 at 14:15 +0900, Michael Paquier wrote:

Hmm. You are making a good point here, but is that really the best
thing we can do? We lose the context of the authentication type being
done with this implementation, and the client would know that it did a
re-authentication even if the logdetail goes only to the backend's
logs. Wouldn't it be better, for instance, to generate a LOG message
in this code path, switch to STATUS_ERROR to let auth_failed()
generate the FATAL message? set_authn_id() could just return a
boolean to tell if it was OK with the change in authn_id or not.

My concern there is that we already know the code is wrong in this
(hypothetical future) case, and then we'd be relying on that wrong code
to correctly bubble up an error status. I think that, once you hit this
code path, the program flow should be interrupted immediately -- do not
pass Go, collect $200, or let the bad implementation continue to do
more damage.

Sounds fair to me.

I agree that losing the context is not ideal. To avoid that, I thought
it might be nice to add errbacktrace() to the ereport() call -- but
since the functions we're interested in are static, the backtrace
doesn't help. (I should check to see whether libbacktrace is better in
this situation. Later.)

Perhaps, but that does not seem strongly necessary to me either here.

If that's a major concern, we could call auth_failed() directly from
this code. But that means that the auth_failed() logic must not give
them more ammunition, in this hypothetical scenario where the authn
system is already messed up. Obscuring the failure mode helps buy
people time to update Postgres, which definitely has value, but it
won't prevent any actual exploit by the time we get to this check. A
tricky trade-off.

Nah. I don't like much a solution that involves calling auth_failed()
in more code paths than now.

This requires a perltidy run from what I can see, but that's no big
deal.

Is that done per-patch? It looks like there's a large amount of
untidied code in src/test in general, and in the files being touched.

Committers take care of that usually, but if you can do it that
helps :)

From what I can see, most of the indent diffs are coming from the
tests added with the addition of the log_(un)like parameters. See
pgindent's README for all the details related to the version of
perltidy, for example. The trick is that some previous patches may
not have been indented, causing the apparitions of extra diffs
unrelated to a patch. Usually that's easy enough to fix on a
file-basis.

Anyway, using a FATAL in this code path is fine by me at the end, so I
have applied the patch. Let's see now what the buildfarm thinks about
it.
--
Michael

#71Jacob Champion
pchampion@vmware.com
In reply to: Michael Paquier (#70)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, 2021-04-07 at 10:20 +0900, Michael Paquier wrote:

Anyway, using a FATAL in this code path is fine by me at the end, so I
have applied the patch. Let's see now what the buildfarm thinks about
it.

Looks like the farm has gone green, after some test fixups. Thanks for
all the reviews!

--Jacob

#72Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#71)
Re: Proposal: Save user's original authenticated identity for logging

On Tue, Apr 13, 2021 at 03:47:21PM +0000, Jacob Champion wrote:

Looks like the farm has gone green, after some test fixups. Thanks for
all the reviews!

You may want to follow this thread as well, as the topic is related to
what has been discussed on this thread as there is an impact in a
different code path for the TAP tests, and not only the connection
tests:
/messages/by-id/YHajnhcMAI3++pJL@paquier.xyz
--
Michael

#73Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Michael Paquier (#65)
1 attachment(s)
Re: Proposal: Save user's original authenticated identity for logging

On 03.04.21 14:30, Michael Paquier wrote:

On Fri, Apr 02, 2021 at 01:45:31PM +0900, Michael Paquier wrote:

As a whole, this is a consolidation of its own, so let's apply this
part first.

Slight rebase for this one to take care of the updates with the SSL
error messages.

I noticed this patch eliminated one $Test::Builder::Level assignment.
Was there a reason for this?

I think we should add it back, and also add a few missing ones in
similar places. See attached patch.

Attachments:

0001-Add-missing-Test-Builder-Level-settings.patchtext/plain; charset=UTF-8; name=0001-Add-missing-Test-Builder-Level-settings.patch; x-mac-creator=0; x-mac-type=0Download
From e9ce373a0482799dc37a695058fc2ca4370f33d1 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 22 Sep 2021 08:58:07 +0200
Subject: [PATCH] Add missing $Test::Builder::Level settings

---
 src/test/authentication/t/001_password.pl | 2 ++
 src/test/authentication/t/002_saslprep.pl | 2 ++
 src/test/kerberos/t/001_auth.pl           | 2 ++
 src/test/ldap/t/001_auth.pl               | 2 ++
 4 files changed, 8 insertions(+)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 427a360198..1296c5307a 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -42,6 +42,8 @@ sub reset_pg_hba
 # named parameters are passed to connect_ok/fails as-is.
 sub test_role
 {
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
 	my ($node, $role, $method, $expected_res, %params) = @_;
 	my $status_string = 'failed';
 	$status_string = 'success' if ($expected_res eq 0);
diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl
index f080a0ccba..5b8b4b28e7 100644
--- a/src/test/authentication/t/002_saslprep.pl
+++ b/src/test/authentication/t/002_saslprep.pl
@@ -36,6 +36,8 @@ sub reset_pg_hba
 # Test access for a single role, useful to wrap all tests into one.
 sub test_login
 {
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
 	my $node          = shift;
 	my $role          = shift;
 	my $password      = shift;
diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl
index b5594924ca..27c93abe78 100644
--- a/src/test/kerberos/t/001_auth.pl
+++ b/src/test/kerberos/t/001_auth.pl
@@ -185,6 +185,8 @@ END
 # Test connection success or failure, and if success, that query returns true.
 sub test_access
 {
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
 	my ($node, $role, $query, $expected_res, $gssencmode, $test_name,
 		@expect_log_msgs)
 	  = @_;
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 0ae14e4c85..0dfc1f9ee1 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -166,6 +166,8 @@ END
 
 sub test_access
 {
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
 	my ($node, $role, $expected_res, $test_name, %params) = @_;
 	my $connstr = "user=$role";
 
-- 
2.33.0

#74Michael Paquier
michael@paquier.xyz
In reply to: Peter Eisentraut (#73)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, Sep 22, 2021 at 08:59:38AM +0200, Peter Eisentraut wrote:

I noticed this patch eliminated one $Test::Builder::Level assignment. Was
there a reason for this?

I think we should add it back, and also add a few missing ones in similar
places. See attached patch.

[...]

{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+

So you are referring to this one removed in c50624c. In what does
this addition change things compared to what has been added in
connect_ok() and connect_fails()? I am pretty sure that I have
removed this one because this logic got refactored in
PostgresNode.pm.
--
Michael

#75Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Michael Paquier (#74)
Re: Proposal: Save user's original authenticated identity for logging

On 22.09.21 09:39, Michael Paquier wrote:

On Wed, Sep 22, 2021 at 08:59:38AM +0200, Peter Eisentraut wrote:

I noticed this patch eliminated one $Test::Builder::Level assignment. Was
there a reason for this?

I think we should add it back, and also add a few missing ones in similar
places. See attached patch.

[...]

{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+

So you are referring to this one removed in c50624c. In what does
this addition change things compared to what has been added in
connect_ok() and connect_fails()? I am pretty sure that I have
removed this one because this logic got refactored in
PostgresNode.pm.

This should be added to each level of a function call that represents a
test. This ensures that when a test fails, the line number points to
the top-level location of the test_role() call. Otherwise it would
point to the connect_ok() call inside test_role().

#76Jacob Champion
pchampion@vmware.com
In reply to: Peter Eisentraut (#75)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, 2021-09-22 at 10:20 +0200, Peter Eisentraut wrote:

This should be added to each level of a function call that represents a
test. This ensures that when a test fails, the line number points to
the top-level location of the test_role() call. Otherwise it would
point to the connect_ok() call inside test_role().

Patch LGTM, sorry about that. Thanks!

--Jacob

#77Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#76)
Re: Proposal: Save user's original authenticated identity for logging

On Wed, Sep 22, 2021 at 03:18:43PM +0000, Jacob Champion wrote:

On Wed, 2021-09-22 at 10:20 +0200, Peter Eisentraut wrote:

This should be added to each level of a function call that represents a
test. This ensures that when a test fails, the line number points to
the top-level location of the test_role() call. Otherwise it would
point to the connect_ok() call inside test_role().

Patch LGTM, sorry about that. Thanks!

For the places of the patch, that seems fine then. Thanks!

Do we need to care about that in other places? We have tests in
src/bin/ using subroutines that call things from PostgresNode.pm or
TestLib.pm, like pg_checksums, pg_ctl or pg_verifybackup, just to name
three.
--
Michael

#78Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Michael Paquier (#77)
Re: Proposal: Save user's original authenticated identity for logging

On 23.09.21 12:34, Michael Paquier wrote:

On Wed, Sep 22, 2021 at 03:18:43PM +0000, Jacob Champion wrote:

On Wed, 2021-09-22 at 10:20 +0200, Peter Eisentraut wrote:

This should be added to each level of a function call that represents a
test. This ensures that when a test fails, the line number points to
the top-level location of the test_role() call. Otherwise it would
point to the connect_ok() call inside test_role().

Patch LGTM, sorry about that. Thanks!

For the places of the patch, that seems fine then. Thanks!

committed

Do we need to care about that in other places? We have tests in
src/bin/ using subroutines that call things from PostgresNode.pm or
TestLib.pm, like pg_checksums, pg_ctl or pg_verifybackup, just to name
three.

Yeah, at first glance, there is probably more that could be done. Here,
I was just looking at a place where it was already and was accidentally
removed.

#79Andrew Dunstan
andrew@dunslane.net
In reply to: Peter Eisentraut (#78)
Re: Proposal: Save user's original authenticated identity for logging

On 9/23/21 5:20 PM, Peter Eisentraut wrote:

On 23.09.21 12:34, Michael Paquier wrote:

On Wed, Sep 22, 2021 at 03:18:43PM +0000, Jacob Champion wrote:

On Wed, 2021-09-22 at 10:20 +0200, Peter Eisentraut wrote:

This should be added to each level of a function call that
represents a
test.� This ensures that when a test fails, the line number points to
the top-level location of the test_role() call.� Otherwise it would
point to the connect_ok() call inside test_role().

Patch LGTM, sorry about that. Thanks!

For the places of the patch, that seems fine then.� Thanks!

committed

Do we need to care about that in other places?� We have tests in
src/bin/ using subroutines that call things from PostgresNode.pm or
TestLib.pm, like pg_checksums, pg_ctl or pg_verifybackup, just to name
three.

Yeah, at first glance, there is probably more that could be done.�
Here, I was just looking at a place where it was already and was
accidentally removed.

It probably wouldn't be a bad thing to have something somewhere
(src/test/perl/README ?) that explains when and why we need to bump
$Test::Builder::Level.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#80Michael Paquier
michael@paquier.xyz
In reply to: Andrew Dunstan (#79)
Re: Proposal: Save user's original authenticated identity for logging

On Fri, Sep 24, 2021 at 05:37:48PM -0400, Andrew Dunstan wrote:

It probably wouldn't be a bad thing to have something somewhere
(src/test/perl/README ?) that explains when and why we need to bump
$Test::Builder::Level.

I have some ideas about that. So I propose to move the discussion to
a new thread.
--
Michael