Add some information about authenticated identity via log_connections

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

To help administrators see who has actually interacted with the system,
this commit adds the capability to store the original identity when
authentication succeeds within the backend's Port, and generates a log
entry when log_connections is enabled.  The log entries generated look
something like this (where a local user named "foouser" is connecting to
the database as the database user called "admin"):

  LOG:  connection received: host=[local]
  LOG:  connection authenticated: identity="foouser" 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 authentication method:

  bsd: the PostgreSQL username (aka the local username)
  cert: the client's Subject DN
  gss: the user principal
  ident: the remote username
  ldap: the final bind DN
  pam: the PostgreSQL username (aka PAM username)
  password (and all pw-challenge methods): the PostgreSQL username
  peer: the peer's pw_name
  radius: the PostgreSQL username (aka 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 clientcert=verify-full.

Port->authn_id could be used for other purposes, like a superuser-only
extra column in pg_stat_activity, but this is left as future work.

PostgresNode::connect_{ok,fails}() have been modified to let tests check
the backend log files for required or prohibited patterns, using the
new log_like and log_unlike parameters.  This uses a method based on a
truncation of the existing server log file, like issues_sql_like().
Tests are added to the ldap, kerberos, authentication and SSL test
suites.

Author: Jacob Champion
Reviewed-by: Stephen Frost, Magnus Hagander, Tom Lane, Michael Paquier
Discussion: https://postgr.es/m/c55788dd1773c521c862e8e0dddb367df51222be.camel@vmware.com
This commit is contained in:
Michael Paquier 2021-04-07 10:16:39 +09:00
parent 8ee9b662da
commit 9afffcb833
11 changed files with 416 additions and 74 deletions

View File

@ -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>.

View File

@ -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;
}

View File

@ -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];
}

View File

@ -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,

View File

@ -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.
*

View File

@ -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

View File

@ -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");

View File

@ -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);
}
}
@ -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";
@ -206,7 +217,12 @@ 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 +266,21 @@ $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";

View File

@ -1876,6 +1876,18 @@ 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 +1907,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 +1938,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 +1966,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 +1981,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 +2011,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

View File

@ -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');

View File

@ -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);