diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/libpq/auth.c | 136 | ||||
-rw-r--r-- | src/backend/libpq/hba.c | 24 | ||||
-rw-r--r-- | src/include/libpq/hba.h | 1 | ||||
-rw-r--r-- | src/include/libpq/libpq-be.h | 13 | ||||
-rw-r--r-- | src/test/authentication/t/001_password.pl | 59 | ||||
-rw-r--r-- | src/test/kerberos/t/001_auth.pl | 79 | ||||
-rw-r--r-- | src/test/ldap/t/001_auth.pl | 52 | ||||
-rw-r--r-- | src/test/perl/PostgresNode.pm | 77 | ||||
-rw-r--r-- | src/test/ssl/t/001_ssltests.pl | 36 | ||||
-rw-r--r-- | src/test/ssl/t/002_scram.pl | 10 |
10 files changed, 414 insertions, 73 deletions
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 9dc28e19aaf..dee056b0d65 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); /*---------------------------------------------------------------- @@ -338,6 +341,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; @@ -1515,6 +1576,26 @@ pg_SSPI_recvauth(Port *port) } /* + * 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 feb711a6ef7..b720b03e9a5 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 1ec8603da75..63f2962139f 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 713c34fedd7..02015efe13c 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -160,6 +160,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. * * default values are 0 if AF_UNIX or not yet known; current values are 0 diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index 65303ca3f5c..150b226c0e8 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 8db18294608..26c2c7077b3 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 ad54854a422..ec4721234bc 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); } } @@ -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"; diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm index ec202f1b6e5..598906ad649 100644 --- a/src/test/perl/PostgresNode.pm +++ b/src/test/perl/PostgresNode.pm @@ -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 diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 0decbe71774..cc797a5c98f 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 583b62b3a18..3cb22ffced1 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); |