You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/test/tap/tests/pgsql-servers_ssl_params-t.cpp

707 lines
25 KiB

/**
* @file pgsql-servers_ssl_params-t.cpp
* @brief TAP integration tests for pgsql_servers_ssl_params.
*
* Part 1: Admin CRUD operations via PgSQL admin port (6132).
* Part 2: End-to-end backend SSL connections verifying per-server
* SSL params are applied (valid cert succeeds, bogus cert fails,
* per-server overrides globals, fallback works, monitor SSL).
*/
#include <unistd.h>
#include <string>
#include <sstream>
#include <chrono>
#include <thread>
#include <vector>
#include <cstdlib>
#include "libpq-fe.h"
#include "mysql.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"
CommandLine cl;
using PGConnPtr = std::unique_ptr<PGconn, decltype(&PQfinish)>;
// ============================================================================
// Helpers
// ============================================================================
enum ConnType { ADMIN, BACKEND };
PGConnPtr createNewConnection(ConnType conn_type, bool with_ssl = false) {
const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_host;
int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port;
const char* username = (conn_type == BACKEND) ? cl.pgsql_root_username : cl.admin_username;
const char* password = (conn_type == BACKEND) ? cl.pgsql_root_password : cl.admin_password;
std::stringstream ss;
ss << "host=" << host << " port=" << port;
ss << " user=" << username << " password=" << password;
ss << (with_ssl ? " sslmode=require" : " sslmode=disable");
PGconn* conn = PQconnectdb(ss.str().c_str());
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "Connection failed to '%s': %s\n",
(conn_type == BACKEND ? "Backend" : "Admin"), PQerrorMessage(conn));
PQfinish(conn);
return PGConnPtr(nullptr, &PQfinish);
}
return PGConnPtr(conn, &PQfinish);
}
bool exec_ok(PGconn* conn, const char* query) {
PGresult* res = PQexec(conn, query);
bool ok_result = (PQresultStatus(res) == PGRES_COMMAND_OK ||
PQresultStatus(res) == PGRES_TUPLES_OK);
if (!ok_result) {
fprintf(stderr, "Query failed: %s\nError: %s\n", query, PQerrorMessage(conn));
}
PQclear(res);
return ok_result;
}
int exec_count(PGconn* conn, const char* query) {
PGresult* res = PQexec(conn, query);
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
PQclear(res);
return -1;
}
int count = PQntuples(res);
PQclear(res);
return count;
}
std::string exec_scalar(PGconn* conn, const char* query) {
PGresult* res = PQexec(conn, query);
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return "";
}
std::string val = PQgetvalue(res, 0, 0);
PQclear(res);
return val;
}
bool exec_queries(PGconn* conn, const std::vector<const char*>& queries) {
for (const auto& q : queries) {
if (!exec_ok(conn, q)) return false;
}
return true;
}
void cleanup_ssl_params(PGconn* admin) {
exec_queries(admin, {
"DELETE FROM pgsql_servers_ssl_params",
"LOAD PGSQL SERVERS TO RUNTIME",
"SAVE PGSQL SERVERS TO DISK"
});
}
/**
* @brief Create a bogus cert file that exists but has invalid content.
*
* libpq will attempt to parse this file and fail with "no start line",
* which proves that per-server SSL params are actually being applied.
* A nonexistent file path is simply ignored by libpq when the backend
* doesn't require client certificates.
*/
static const char* BOGUS_CERT_PATH = "/tmp/proxysql_test_bogus_cert.pem";
static void create_bogus_cert_file() {
FILE* f = fopen(BOGUS_CERT_PATH, "w");
if (f) {
fprintf(f, "this is not a valid certificate\n");
fclose(f);
}
}
static void remove_bogus_cert_file() {
unlink(BOGUS_CERT_PATH);
}
static long getMonitorValue(PGconn* admin, const char* varname) {
std::stringstream q;
q << "SELECT Variable_Value FROM stats_pgsql_global "
"WHERE Variable_Name='" << varname << "';";
PGresult* res = PQexec(admin, q.str().c_str());
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return -1;
}
long v = atol(PQgetvalue(res, 0, 0));
PQclear(res);
return v;
}
static long getConnectInterval(PGconn* admin) {
PGresult* res = PQexec(admin,
"SELECT Variable_Value FROM global_variables WHERE Variable_Name='pgsql-monitor_connect_interval';"
);
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return 60000;
}
long v = atol(PQgetvalue(res, 0, 0));
PQclear(res);
return v;
}
static bool setConnectInterval(PGconn* admin, int value) {
std::stringstream q;
q << "SET pgsql-monitor_connect_interval=" << value << ";";
bool ok1 = exec_ok(admin, q.str().c_str());
bool ok2 = exec_ok(admin, "LOAD PGSQL VARIABLES TO RUNTIME");
usleep(10000);
return ok1 && ok2;
}
// ============================================================================
// Part 1: Admin CRUD Operations
// ============================================================================
static void test_table_exists(PGconn* admin) {
ok(exec_ok(admin, "SELECT * FROM pgsql_servers_ssl_params"),
"pgsql_servers_ssl_params table exists and is queryable");
ok(exec_ok(admin, "SELECT * FROM runtime_pgsql_servers_ssl_params"),
"runtime_pgsql_servers_ssl_params table exists and is queryable");
}
static void test_insert_and_select(PGconn* admin) {
cleanup_ssl_params(admin);
ok(exec_ok(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca, ssl_cert, ssl_key, "
"ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment) "
"VALUES ('testhost', 5432, 'testuser', '/ca.crt', '/cert.crt', '/key.pem', "
"'/crl.pem', '/crlpath', 'TLSv1.2-TLSv1.3', 'test row')"),
"INSERT into pgsql_servers_ssl_params succeeds");
int count = exec_count(admin, "SELECT * FROM pgsql_servers_ssl_params");
ok(count == 1, "SELECT returns 1 row after INSERT");
std::string val = exec_scalar(admin,
"SELECT ssl_ca FROM pgsql_servers_ssl_params WHERE hostname='testhost'");
ok(val == "/ca.crt", "ssl_ca column round-trips correctly");
val = exec_scalar(admin,
"SELECT ssl_protocol_version_range FROM pgsql_servers_ssl_params WHERE hostname='testhost'");
ok(val == "TLSv1.2-TLSv1.3", "ssl_protocol_version_range round-trips correctly");
}
static void test_primary_key_constraint(PGconn* admin) {
PGresult* res = PQexec(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca) "
"VALUES ('testhost', 5432, 'testuser', '/other_ca.crt')");
bool failed = (PQresultStatus(res) != PGRES_COMMAND_OK);
PQclear(res);
ok(failed, "Duplicate primary key INSERT is rejected");
}
static void test_load_to_runtime(PGconn* admin) {
ok(exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"),
"LOAD PGSQL SERVERS TO RUNTIME succeeds");
int count = exec_count(admin, "SELECT * FROM runtime_pgsql_servers_ssl_params");
ok(count == 1, "runtime table has 1 row after LOAD TO RUNTIME");
std::string val = exec_scalar(admin,
"SELECT ssl_ca FROM runtime_pgsql_servers_ssl_params WHERE hostname='testhost'");
ok(val == "/ca.crt", "runtime ssl_ca matches admin table");
}
static void test_multiple_rows(PGconn* admin) {
exec_ok(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca) VALUES ('host2', 5433, 'user2', '/ca2.crt')");
exec_ok(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca) VALUES ('host3', 5434, '', '/ca3.crt')");
ok(exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"),
"LOAD with multiple rows succeeds");
int count = exec_count(admin, "SELECT * FROM runtime_pgsql_servers_ssl_params");
ok(count == 3, "runtime table has 3 rows");
}
static void test_save_and_load_disk(PGconn* admin) {
ok(exec_ok(admin, "SAVE PGSQL SERVERS TO DISK"), "SAVE TO DISK succeeds");
exec_ok(admin, "DELETE FROM pgsql_servers_ssl_params");
int count = exec_count(admin, "SELECT * FROM pgsql_servers_ssl_params");
ok(count == 0, "admin table empty after DELETE");
ok(exec_ok(admin, "LOAD PGSQL SERVERS FROM DISK"), "LOAD FROM DISK succeeds");
count = exec_count(admin, "SELECT * FROM pgsql_servers_ssl_params");
ok(count == 3, "admin table has 3 rows after LOAD FROM DISK");
}
static void test_delete_and_reload(PGconn* admin) {
exec_ok(admin, "DELETE FROM pgsql_servers_ssl_params");
ok(exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"),
"LOAD TO RUNTIME after DELETE succeeds");
int count = exec_count(admin, "SELECT * FROM runtime_pgsql_servers_ssl_params");
ok(count == 0, "runtime table empty after DELETE + LOAD");
}
static void test_update_and_reload(PGconn* admin) {
exec_ok(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca) VALUES ('updatehost', 5432, '', '/old_ca.crt')");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
exec_ok(admin,
"UPDATE pgsql_servers_ssl_params SET ssl_ca='/new_ca.crt' WHERE hostname='updatehost'");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
std::string val = exec_scalar(admin,
"SELECT ssl_ca FROM runtime_pgsql_servers_ssl_params WHERE hostname='updatehost'");
ok(val == "/new_ca.crt", "UPDATE reflected in runtime after LOAD");
}
static void test_default_port(PGconn* admin) {
cleanup_ssl_params(admin);
exec_ok(admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, username, ssl_ca) VALUES ('defaultport', '', '/ca.crt')");
std::string val = exec_scalar(admin,
"SELECT port FROM pgsql_servers_ssl_params WHERE hostname='defaultport'");
ok(val == "5432", "Default port is 5432");
}
// ============================================================================
// Part 2: End-to-End Backend SSL Connections
// ============================================================================
static bool get_backend_server(PGconn* admin, std::string& hostname, int& port) {
PGresult* res = PQexec(admin,
"SELECT hostname, port FROM pgsql_servers LIMIT 1");
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return false;
}
hostname = PQgetvalue(res, 0, 0);
port = atoi(PQgetvalue(res, 0, 1));
PQclear(res);
return true;
}
static void test_backend_ssl_baseline(PGconn* admin) {
cleanup_ssl_params(admin);
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
usleep(500000);
auto backend = createNewConnection(BACKEND);
ok(backend != nullptr, "Baseline: backend connection through ProxySQL succeeds with use_ssl=1");
if (backend) {
PGresult* res = PQexec(backend.get(), "SELECT 1");
ok(PQresultStatus(res) == PGRES_TUPLES_OK, "Baseline: SELECT 1 succeeds");
PQclear(res);
} else {
ok(0, "Baseline: SELECT 1 succeeds (connection failed)");
}
}
static void test_per_server_params_promoted_to_runtime(PGconn* admin) {
std::string hostname;
int port;
if (!get_backend_server(admin, hostname, port)) {
ok(0, "Per-server runtime: no backend server found");
return;
}
cleanup_ssl_params(admin);
// Insert per-server SSL params for the actual backend
std::stringstream q;
q << "INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_protocol_version_range) VALUES ('"
<< hostname << "', " << port << ", '', '/test/ca.crt', '/test/cert.crt', '/test/key.pem', 'TLSv1.2-TLSv1.3')";
exec_ok(admin, q.str().c_str());
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
// Verify the params appear in the runtime table for the correct server
std::stringstream verify;
verify << "SELECT ssl_ca, ssl_cert, ssl_key, ssl_protocol_version_range "
"FROM runtime_pgsql_servers_ssl_params WHERE hostname='" << hostname << "' AND port=" << port;
PGresult* res = PQexec(admin, verify.str().c_str());
ok(PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) == 1,
"Per-server params for actual backend appear in runtime");
if (PQntuples(res) == 1) {
ok(std::string(PQgetvalue(res, 0, 0)) == "/test/ca.crt",
"Runtime ssl_ca matches inserted value");
ok(std::string(PQgetvalue(res, 0, 3)) == "TLSv1.2-TLSv1.3",
"Runtime ssl_protocol_version_range matches inserted value");
} else {
ok(0, "Runtime ssl_ca matches inserted value");
ok(0, "Runtime ssl_protocol_version_range matches inserted value");
}
PQclear(res);
}
static void test_tls_version_pin_causes_failure(PGconn* admin) {
std::string hostname;
int port;
if (!get_backend_server(admin, hostname, port)) {
ok(0, "TLS pin: no backend server found");
return;
}
cleanup_ssl_params(admin);
// Pin to TLSv1 (disabled in modern PgSQL) — connection must fail
std::stringstream q;
q << "INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_protocol_version_range) VALUES ('"
<< hostname << "', " << port << ", '', 'TLSv1')";
exec_ok(admin, q.str().c_str());
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
usleep(500000);
// Use create_new_connection=1 annotation to force a new backend connection
// that will use the per-server SSL params with TLSv1 restriction
auto backend = createNewConnection(BACKEND);
ok(backend != nullptr, "TLS pin: client connection to ProxySQL established");
bool query_failed = true;
if (backend) {
PGresult* res = PQexec(backend.get(), "/* create_new_connection=1 */ SELECT 1");
query_failed = (PQresultStatus(res) != PGRES_TUPLES_OK);
if (!query_failed) {
diag("TLS pin: query unexpectedly succeeded");
}
PQclear(res);
}
ok(query_failed, "TLS pin: query fails when per-server ssl_protocol_version_range=TLSv1");
}
static void test_per_server_overrides_global(PGconn* admin) {
std::string hostname;
int port;
if (!get_backend_server(admin, hostname, port)) {
ok(0, "Override: no backend server found");
return;
}
cleanup_ssl_params(admin);
// Per-server TLSv1 restriction should override working global config
std::stringstream q;
q << "INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_protocol_version_range) VALUES ('"
<< hostname << "', " << port << ", '', 'TLSv1')";
exec_ok(admin, q.str().c_str());
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
usleep(500000);
auto backend = createNewConnection(BACKEND);
bool query_failed = true;
if (backend) {
PGresult* res = PQexec(backend.get(), "/* create_new_connection=1 */ SELECT 1");
query_failed = (PQresultStatus(res) != PGRES_TUPLES_OK);
PQclear(res);
}
ok(query_failed,
"Override: per-server TLSv1 restriction causes failure despite no global TLS restriction");
}
static void test_remove_per_server_fallback_to_global(PGconn* admin) {
cleanup_ssl_params(admin);
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
usleep(500000);
auto backend = createNewConnection(BACKEND);
ok(backend != nullptr,
"Fallback to global: after removing per-server params, connection succeeds");
if (backend) {
PGresult* res = PQexec(backend.get(), "SELECT 1");
ok(PQresultStatus(res) == PGRES_TUPLES_OK,
"Fallback to global: SELECT 1 succeeds");
PQclear(res);
} else {
ok(0, "Fallback to global: SELECT 1 succeeds (connection failed)");
}
}
static void test_monitor_ssl_with_per_server_params(PGconn* admin) {
cleanup_ssl_params(admin);
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
long initial_ssl = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
diag("Initial PgSQL_Monitor_ssl_connections_OK: %ld", initial_ssl);
// Wait for at least 2 monitor cycles (interval was set to 2000ms in main)
usleep(5000000); // 5 seconds
long after_ssl = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
diag("After PgSQL_Monitor_ssl_connections_OK: %ld", after_ssl);
ok(after_ssl > initial_ssl,
"Monitor SSL counter increased with use_ssl=1 and no per-server row");
}
/**
* @brief Verify the monitor path actually consults pgsql_servers_ssl_params.
*
* Inserts a per-server row matching the actual backend with username=''
* (so the monitor's empty-username fallback in get_Server_SSL_Params hits
* it) and an ssl_protocol_version_range pinned to TLSv1 — disabled in
* modern PostgreSQL builds. If the monitor honors per-server params and
* propagates tls_version into its libpq conninfo, monitor SSL connections
* must fail and the OK counter must NOT advance over the wait window.
* If the monitor were ignoring per-server params (or dropping tls_version),
* the counter would keep climbing, which is the regression we want to catch.
*/
static void test_monitor_uses_per_server_row(PGconn* admin) {
std::string hostname;
int port;
if (!get_backend_server(admin, hostname, port)) {
ok(0, "Monitor per-server: no backend server found");
ok(0, "Monitor per-server: cleanup restores monitor SSL OK");
return;
}
cleanup_ssl_params(admin);
exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
// Insert a per-server row matching the backend, with TLSv1 pin so the
// monitor's connection attempt must fail if tls_version flows through.
std::stringstream q;
q << "INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_protocol_version_range) VALUES ('"
<< hostname << "', " << port << ", '', 'TLSv1')";
exec_ok(admin, q.str().c_str());
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
// Drain any monitor connect cycle scheduled with the pre-LOAD config.
// Why: the monitor thread may already be mid-cycle when LOAD runs; that
// in-flight connect was scheduled with the OLD SSL params (no TLSv1 pin)
// and will succeed and bump the counter AFTER we sample ok_before. Wait
// until two consecutive 1s samples are equal — meaning the new config
// is in effect and the counter has plateaued.
long ok_before = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
for (int i = 0; i < 10; i++) {
usleep(1000000); // 1s — longer than the test's monitor connect interval
long sample = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
if (sample == ok_before) break;
ok_before = sample;
}
diag("With TLSv1 per-server pin, ssl OK before wait (post-drain): %ld", ok_before);
usleep(5000000); // 5 seconds — multiple monitor cycles
long ok_after = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
diag("With TLSv1 per-server pin, ssl OK after wait: %ld (delta=%ld)",
ok_after, ok_after - ok_before);
ok(ok_after == ok_before,
"Monitor per-server: SSL OK counter does NOT advance when "
"per-server row pins ssl_protocol_version_range to TLSv1");
// Phase 2: remove the row and confirm the monitor recovers, proving
// the previous failure was caused by the per-server row and not by
// some unrelated breakage.
cleanup_ssl_params(admin);
exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME");
long recover_before = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
usleep(5000000);
long recover_after = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
diag("After cleanup, ssl OK recovered from %ld to %ld",
recover_before, recover_after);
ok(recover_after > recover_before,
"Monitor per-server: SSL OK counter resumes advancing after "
"removing the per-server row");
}
// ============================================================================
// Part 3: Cluster Query Support
// ============================================================================
/**
* @brief Test that the PROXY_SELECT cluster query for pgsql_servers_ssl_params
* returns correct data via the MySQL admin port.
*
* Cluster sync uses MySQL-protocol admin connections. The PROXY_SELECT query
* is intercepted by Admin_Handler and returns data from
* get_current_pgsql_table() or dump_table_pgsql().
*/
static void test_cluster_query_ssl_params(PGconn* pgsql_admin) {
cleanup_ssl_params(pgsql_admin);
// Insert test data via PgSQL admin and load to runtime
exec_ok(pgsql_admin,
"INSERT INTO pgsql_servers_ssl_params "
"(hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_protocol_version_range, comment) "
"VALUES ('cluster-host', 5432, 'clusteruser', '/certs/ca.crt', '/certs/cert.crt', "
"'/certs/key.pem', 'TLSv1.2-TLSv1.3', 'cluster test')");
exec_ok(pgsql_admin, "LOAD PGSQL SERVERS TO RUNTIME");
// Connect to MySQL admin port and execute the cluster query
MYSQL* mysql_admin = mysql_init(NULL);
if (!mysql_real_connect(mysql_admin, cl.admin_host, cl.admin_username,
cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
ok(0, "Cluster query: MySQL admin connection failed: %s", mysql_error(mysql_admin));
ok(0, "Cluster query: skipping result check");
ok(0, "Cluster query: skipping field check");
mysql_close(mysql_admin);
cleanup_ssl_params(pgsql_admin);
return;
}
ok(1, "Cluster query: MySQL admin connection established");
// Execute the PROXY_SELECT cluster query
const char* cluster_query =
"PROXY_SELECT hostname, port, username, ssl_ca, ssl_cert, ssl_key, "
"ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment "
"FROM runtime_pgsql_servers_ssl_params ORDER BY hostname, port, username";
int rc = mysql_query(mysql_admin, cluster_query);
if (rc != 0) {
ok(0, "Cluster query: PROXY_SELECT failed: %s", mysql_error(mysql_admin));
ok(0, "Cluster query: skipping field check");
mysql_close(mysql_admin);
cleanup_ssl_params(pgsql_admin);
return;
}
MYSQL_RES* result = mysql_store_result(mysql_admin);
ok(result != NULL && mysql_num_rows(result) == 1,
"Cluster query: PROXY_SELECT returns 1 row");
if (result && mysql_num_rows(result) == 1) {
MYSQL_ROW row = mysql_fetch_row(result);
bool hostname_ok = (row[0] && strcmp(row[0], "cluster-host") == 0);
bool ssl_ca_ok = (row[3] && strcmp(row[3], "/certs/ca.crt") == 0);
bool tls_range_ok = (row[8] && strcmp(row[8], "TLSv1.2-TLSv1.3") == 0);
ok(hostname_ok && ssl_ca_ok && tls_range_ok,
"Cluster query: returned row has correct hostname, ssl_ca, and ssl_protocol_version_range");
} else {
ok(0, "Cluster query: skipping field check (no rows)");
}
if (result) mysql_free_result(result);
mysql_close(mysql_admin);
cleanup_ssl_params(pgsql_admin);
}
// ============================================================================
// main
// ============================================================================
int main(int argc, char** argv) {
plan(37);
if (cl.getEnv()) {
BAIL_OUT("Failed to get environment variables");
return exit_status();
}
auto admin = createNewConnection(ADMIN);
ok(admin != nullptr, "Admin connection created");
if (!admin) {
BAIL_OUT("Cannot proceed without admin connection");
return exit_status();
}
PGconn* a = admin.get();
// Part 1: Admin CRUD
diag("---- Part 1: Admin CRUD Operations ----");
test_table_exists(a);
test_insert_and_select(a);
test_primary_key_constraint(a);
test_load_to_runtime(a);
test_multiple_rows(a);
test_save_and_load_disk(a);
test_delete_and_reload(a);
test_update_and_reload(a);
test_default_port(a);
// Part 2: End-to-End SSL
diag("---- Part 2: End-to-End Backend SSL ----");
test_backend_ssl_baseline(a);
test_per_server_params_promoted_to_runtime(a);
test_tls_version_pin_causes_failure(a);
test_per_server_overrides_global(a);
test_remove_per_server_fallback_to_global(a);
// Configure monitor: use 'postgres' user which is accepted by the
// backend, set a tight connect_interval so cycles run within the test
// wait window. Restore original values afterwards.
//
// We deliberately do NOT change pgsql-monitor_username /
// pgsql-monitor_password. An earlier version of this test set them
// to 'postgres'/'postgres' on the assumption that the backend had a
// postgres user with password 'postgres', but the actual CI infra
// (docker-pgsql16-single) randomizes POSTGRES_PASSWORD per container
// startup (e.g. "05e792e51d"), so the hardcoded credentials never
// authenticated and every monitor connect failed with
// FATAL: password authentication failed for user "postgres"
// The default monitor/monitor user works against the infra's
// pg_hba.conf and is what the initial (pre-test) connect check
// already uses successfully.
long original_connect_interval = getConnectInterval(a);
// Read current monitor username for diagnostic logging only.
std::string monitor_username = exec_scalar(a,
"SELECT Variable_Value FROM global_variables WHERE Variable_Name='pgsql-monitor_username'");
diag("Current monitor: user=%s interval=%ld ms",
monitor_username.c_str(), original_connect_interval);
setConnectInterval(a, 2000);
exec_ok(a, "UPDATE pgsql_servers SET use_ssl=1");
exec_ok(a, "LOAD PGSQL SERVERS TO RUNTIME");
usleep(3000000); // let the monitor pick up the new settings
test_monitor_ssl_with_per_server_params(a);
test_monitor_uses_per_server_row(a);
// Restore original connect interval. Monitor username/password are
// no longer touched by this test - see comment above - so there's
// nothing to restore there.
setConnectInterval(a, (int)original_connect_interval);
// Part 3: Cluster query support
diag("---- Part 3: Cluster Query Support ----");
test_cluster_query_ssl_params(a);
// Cleanup
remove_bogus_cert_file();
cleanup_ssl_params(a);
exec_ok(a, "UPDATE pgsql_servers SET use_ssl=0");
exec_ok(a, "LOAD PGSQL SERVERS TO RUNTIME");
return exit_status();
}