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/test_sqlite3_pass_exts-t.cpp

566 lines
16 KiB

/**
* @file test_sqlite3_pass_exts-t.cpp
* @brief Tests the SQLite3 extensions in the Admin interface and it's MySQL compatibility.
* @details The test perform the following operations:
* 1. Create MySQL users with random pass and check pass reproduction for:
* - 'mysql_native_password'
* - 'caching_sha2_password'
* 2. Stress password creation, ensure that start and length matches expected.
* 3. End-to-end password generation testing:
* 1. Create passwords in both MySQL and ProxySQL using buit-in Admin function for hash generation.
* 2. Connect to ProxySQL and force a new backend connection using these passwords.
*/
#include <cassert>
#include <ctime>
#include <string>
#include <vector>
#include <utility>
#include "mysql.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"
// Additional env variables
uint32_t TAP_MYSQL8_BACKEND_HG = 30;
using std::string;
using std::vector;
using std::pair;
struct user_def_t {
string name;
string auth;
string pass;
string hash;
string salt;
};
struct user_creds_t {
string name;
string auth;
string pass;
};
#define MYSQL_QUERY_T_(mysql, query) \
do { \
if (mysql_query_t(mysql, query)) { \
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql)); \
return { EXIT_FAILURE, user_def_t {} }; \
} \
} while(0)
int create_mysql_user(MYSQL* mysql, const user_creds_t& creds) {
const string CREATE_USER {
"CREATE USER '" + creds.name + "'@'%' IDENTIFIED WITH"
" '" + creds.auth + "' BY '" + creds.pass + "'"
};
const string GRANT_USER_PRIVS { "GRANT ALL on *.* to '" + creds.name + "'@'%'" };
const string DROP_USER { "DROP USER IF EXISTS '" + creds.name + "'"};
MYSQL_QUERY_T(mysql, DROP_USER.c_str());
MYSQL_QUERY_T(mysql, CREATE_USER.c_str());
MYSQL_QUERY_T(mysql, GRANT_USER_PRIVS.c_str());
return EXIT_SUCCESS;
}
int config_proxysql_user(MYSQL* admin, const user_creds_t& creds) {
const string DEF_HG { std::to_string(TAP_MYSQL8_BACKEND_HG) };
MYSQL_QUERY_T(admin, ("DELETE FROM mysql_users WHERE username='" + creds.name + "'").c_str());
// Ensure cleanup of previously cached clear_text 'caching_sha2' passwords
MYSQL_QUERY_T(admin, "LOAD MYSQL USERS TO RUNTIME");
MYSQL_QUERY_T(admin, "LOAD MYSQL USERS TO RUNTIME");
const string insert_query {
"INSERT INTO mysql_users (username,password,default_hostgroup) "
"VALUES ('" + creds.name + "'," + creds.auth + "('" + creds.pass + "')," + DEF_HG + ")"
};
MYSQL_QUERY_T(admin, insert_query.c_str());
MYSQL_QUERY_T(admin, "LOAD MYSQL USERS TO RUNTIME");
return EXIT_SUCCESS;
}
pair<int,user_def_t> create_mysql_user_rnd_creds(MYSQL* mysql, const string& name, const string& auth) {
diag("Creating user with random pass user:'%s'", name.c_str());
const string CREATE_USER {
"CREATE USER '" + name + "'@'%' IDENTIFIED WITH '" + auth + "' BY RANDOM PASSWORD"
};
const string EXT_NATIVE_AUTH {
"SELECT authentication_string FROM mysql.user WHERE user='" + name + "'"
};
const string EXT_SHA2_AUTH {
"SELECT HEX(authentication_string), HEX(SUBSTR(authentication_string, 8, 20)) AS salt"
" FROM mysql.user WHERE user='" + name + "'"
};
const string DROP_USER { "DROP USER IF EXISTS '" + name + "'"};
string pass {};
string hash {};
string salt {};
// DROP/CREATE and extract new password
{
MYSQL_QUERY_T_(mysql, DROP_USER.c_str());
MYSQL_QUERY_T_(mysql, CREATE_USER.c_str());
MYSQL_RES* myres = mysql_store_result(mysql);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (myrow && myrow[2]) {
pass = string { myrow[2] };
}
mysql_free_result(myres);
}
// Extract 'authentication_string' and 'salt'
if (auth == "mysql_native_password") {
MYSQL_QUERY_T_(mysql, EXT_NATIVE_AUTH.c_str());
MYSQL_RES* myres = mysql_store_result(mysql);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (myrow && myrow[0]) {
hash = string { myrow[0] };
} else {
assert(!"Received malformed result");
}
mysql_free_result(myres);
} else if (auth == "caching_sha2_password") {
MYSQL_QUERY_T_(mysql, EXT_SHA2_AUTH.c_str());
MYSQL_RES* myres = mysql_store_result(mysql);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (myrow && myrow[0] && myrow[1]) {
hash = string { myrow[0] };
salt = string { myrow[1] };
} else {
assert(!"Received malformed result");
}
mysql_free_result(myres);
} else {
assert(!"Invalid auth method");
}
diag(
"Created user user:'%s', pass:'%s', hash: '%s', salt:'%s'",
name.c_str(), pass.c_str(), hash.c_str(), salt.c_str()
);
return { EXIT_SUCCESS, user_def_t { name, auth, pass, hash, salt } };
}
int test_pass_match(MYSQL* admin, const user_def_t& def) {
diag(
"Test MySQL/Admin pass match user:'%s', auth:'%s', pass:'%s', hash: '%s', salt:'%s'",
def.name.c_str(), def.auth.c_str(), def.pass.c_str(), def.hash.c_str(), def.salt.c_str()
);
if (def.auth == "mysql_native_password") {
const string GEN_NATIVE_PASS {
"SELECT MYSQL_NATIVE_PASSWORD('" + def.pass + "')"
};
MYSQL_QUERY_T(admin, GEN_NATIVE_PASS.c_str());
MYSQL_RES* myres = mysql_store_result(admin);
MYSQL_ROW myrow = mysql_fetch_row(myres);
string admin_hash { myrow[0] };
ok(
def.hash == admin_hash,
"MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'",
def.hash.c_str(), admin_hash.c_str()
);
mysql_free_result(myres);
} else if (def.auth == "caching_sha2_password") {
const string GEN_SHA2_PASS {
"SELECT HEX(CACHING_SHA2_PASSWORD('" + def.pass + "', UNHEX('" + def.salt + "')))"
};
MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str());
MYSQL_RES* myres = mysql_store_result(admin);
MYSQL_ROW myrow = mysql_fetch_row(myres);
string admin_hash { myrow[0] };
ok(
def.hash == admin_hash,
"MySQL hash should match ProxySQL generated mysql:'%s', admin:'%s'",
def.hash.c_str(), admin_hash.c_str()
);
mysql_free_result(myres);
}
return EXIT_SUCCESS;
}
int test_pass_gen(MYSQL* admin, const string& auth, const string& pass, const string& salt) {
diag("Test Admin pass hash gen auth:'%s',pass:'%s'", auth.c_str(), pass.c_str());
if (auth == "mysql_native_password") {
const string GEN_NATIVE_PASS { "SELECT MYSQL_NATIVE_PASSWORD('" + pass + "')" };
MYSQL_QUERY_T(admin, GEN_NATIVE_PASS.c_str());
MYSQL_RES* myres = mysql_store_result(admin);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (pass.size() > 0) {
const string admin_hash { myrow[0] };
bool valid_hash = admin_hash.size() == 41 && admin_hash[0] == '*';
ok(
valid_hash,
"Gen hash should be wellformed size:'%lu', hash:'%s'",
admin_hash.size(), admin_hash.c_str()
);
} else {
const string act_msg { myrow[0] };
const string exp_msg { "Invalid argument size" };
ok(
exp_msg == act_msg,
"Args verf should have failed exp_msg:'%s', act_msg:'%s'",
exp_msg.c_str(), act_msg.c_str()
);
}
mysql_free_result(myres);
} else if (auth == "caching_sha2_password") {
const string GEN_SHA2_PASS { "SELECT CACHING_SHA2_PASSWORD('" + pass + "', '" + salt + "')" };
MYSQL_QUERY_T(admin, GEN_SHA2_PASS.c_str());
MYSQL_RES* myres = mysql_store_result(admin);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (pass.size() > 0 && salt.size() > 0 && salt.size() <= 20) {
const string admin_hash { myrow[0] };
const string hash_start { "$A$005$" + salt };
bool valid_hash =
(admin_hash.size() == 50 + salt.size()) &&
admin_hash.rfind(hash_start, 0) == 0;
ok(
valid_hash,
"Gen hash should be wellformed size:'%lu', salt_size:'%lu', hash:'%s'",
admin_hash.size(), salt.size(), admin_hash.c_str()
);
} else {
const string act_msg { myrow[0] };
const string exp_msg { "Invalid argument size" };
ok(
exp_msg == act_msg,
"Args verf should have failed exp_msg:'%s', act_msg:'%s'",
exp_msg.c_str(), act_msg.c_str()
);
}
mysql_free_result(myres);
}
return EXIT_SUCCESS;
}
pair<int,string> get_query_res(MYSQL* mysql, const string& query) {
string res {};
int rc = mysql_query_t(mysql, query.c_str());
if (rc) {
return { rc, mysql_error(mysql) };
} else {
MYSQL_RES* myres = mysql_store_result(mysql);
MYSQL_ROW myrow = mysql_fetch_row(myres);
if (myrow && myrow[0]) {
res = myrow[0];
} else {
rc = 1;
res = "Invalid resultset received";
}
mysql_free_result(myres);
}
return { rc, res };
}
struct inv_input_t {
string query;
int err;
string msg;
};
// NOTE: If modified, set even numbers
const uint32_t USER_GEN_COUNT = 100;
const uint32_t PASS_GEN_COUNT = 1000;
const uint32_t RAND_USERS_GEN = 100;
const vector<inv_input_t> INV_INPUTS {
{
"SELECT MYSQL_NATIVE_PASSWORD()", 1,
"ProxySQL Admin Error: wrong number of arguments to function MYSQL_NATIVE_PASSWORD()"
},
{
"SELECT MYSQL_NATIVE_PASSWORD('00', '00')", 1,
"ProxySQL Admin Error: wrong number of arguments to function MYSQL_NATIVE_PASSWORD()"
},
{ "SELECT MYSQL_NATIVE_PASSWORD('')", 0, "Invalid argument size" },
{ "SELECT MYSQL_NATIVE_PASSWORD(2)", 0, "Invalid argument type" },
{
"SELECT CACHING_SHA2_PASSWORD()", 1,
"ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()"
},
{
"SELECT CACHING_SHA2_PASSWORD('00', '00', '00')", 1,
"ProxySQL Admin Error: wrong number of arguments to function CACHING_SHA2_PASSWORD()"
},
{ "SELECT CACHING_SHA2_PASSWORD('', '')", 0, "Invalid argument size" },
{ "SELECT CACHING_SHA2_PASSWORD('', '000000000000000000000')", 0, "Invalid argument size" },
{ "SELECT CACHING_SHA2_PASSWORD(2, '00')", 0, "Invalid argument type" },
{ "SELECT CACHING_SHA2_PASSWORD('00', 2)", 0, "Invalid argument type" },
};
int main(int argc, char** argv) {
CommandLine cl;
plan(
INV_INPUTS.size() +
USER_GEN_COUNT +
PASS_GEN_COUNT * 2 +
2 + // EXTRA: Two extra correctness tests; forcing randomness
RAND_USERS_GEN +
1 // EXTRA: Conn count after 'RAND_USERS_GEN'; consistency check
);
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return EXIT_FAILURE;
}
TAP_MYSQL8_BACKEND_HG = get_env_int("TAP_MYSQL8_BACKEND_HG", 30);
MYSQL* mysql = mysql_init(NULL);
if (!mysql_real_connect(mysql, cl.host, cl.mysql_username, cl.mysql_password, NULL, cl.mysql_port, NULL, 0)) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(mysql));
return EXIT_FAILURE;
}
MYSQL* admin = mysql_init(NULL);
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
return EXIT_FAILURE;
}
// Tests functions input verification
{
for (const inv_input_t& inv_input : INV_INPUTS) {
const pair<int,string> res { get_query_res(admin, inv_input.query) };
if (res.first && inv_input.err == 0) {
diag("Query on Admin unexpectedly failed rc:'%d', err:'%s'", res.first, res.second.c_str());
goto cleanup;
} else {
ok(
inv_input.msg == res.second,
"Expected failure should match actual exp:'%s', act:'%s'",
inv_input.msg.c_str(), res.second.c_str()
);
}
}
}
// Tests MySQL/Admin hashes compatibility
{
vector<user_def_t> users {};
for (size_t i = 0; i < USER_GEN_COUNT/2; i++) {
const string name { "rndextuser" + std::to_string(i) };
pair<int,user_def_t> user_def {
create_mysql_user_rnd_creds(mysql, name, "mysql_native_password")
};
if (user_def.first) {
diag("User creation failed user:'%s'", name.c_str());
goto cleanup;
} else {
users.push_back(user_def.second);
}
}
for (size_t i = 50; i < USER_GEN_COUNT; i++) {
const string name { "rndextuser" + std::to_string(i) };
pair<int,user_def_t> user_def {
create_mysql_user_rnd_creds(mysql, name, "caching_sha2_password")
};
if (user_def.first) {
diag("User creation failed user:'%s'", name.c_str());
goto cleanup;
} else {
users.push_back(user_def.second);
}
}
for (const user_def_t& def : users) {
test_pass_match(admin, def);
}
}
// Tests correctness of randomly generated hashes
{
std::srand(static_cast<unsigned int>(std::time(nullptr)));
// EXTRA: Two extra correctness tests; forcing randomness
test_pass_gen(admin, "mysql_native_password", "randpass0", "");
test_pass_gen(admin, "caching_sha2_password", "randpass0", "00000000000000000000");
for (size_t i = 0; i < PASS_GEN_COUNT; i++) {
const uint32_t pass_len = rand() % 150;
const string pass { random_string(pass_len) };
test_pass_gen(admin, "mysql_native_password", pass, "");
}
for (size_t i = 0; i < PASS_GEN_COUNT; i++) {
const uint32_t pass_len = rand() % 150;
const uint32_t salt_len = rand() % 20;
const string pass { random_string(pass_len) };
const string salt { random_string(pass_len) };
test_pass_gen(admin, "caching_sha2_password", pass, salt);
}
}
// Tests end-to-end connection with MySQL using same gen passwords
{
const string TAP_MYSQL8_BACKEND_HG_S { std::to_string(TAP_MYSQL8_BACKEND_HG) };
const string RAND_USERS_GEN_S { std::to_string(RAND_USERS_GEN) };
diag("Cleaning up previous backend connections...");
MYSQL_QUERY(admin,
("UPDATE mysql_servers SET max_connections=0 "
"WHERE hostgroup_id=" + TAP_MYSQL8_BACKEND_HG_S).c_str()
);
MYSQL_QUERY(admin, "LOAD MYSQL SERVERS TO RUNTIME");
const string COND_CONN_CLEANUP {
"SELECT IIF((SELECT SUM(ConnUsed + ConnFree) FROM stats.stats_mysql_connection_pool"
" WHERE hostgroup=" + TAP_MYSQL8_BACKEND_HG_S + ")=0, 'TRUE', 'FALSE')"
};
int w_res = wait_for_cond(admin, COND_CONN_CLEANUP, 10);
if (w_res) {
diag("Waiting for backend connections failed res:'%d'", w_res);
goto cleanup;
}
// Just in case a low connection limit is set by default
diag("Setup new connection limit max_connections='2000'");
MYSQL_QUERY(admin,
("UPDATE mysql_servers SET max_connections=2000 "
"WHERE hostgroup_id=" + TAP_MYSQL8_BACKEND_HG_S).c_str()
);
MYSQL_QUERY(admin, "LOAD MYSQL SERVERS TO RUNTIME");
for (uint32_t i = 0; i < RAND_USERS_GEN; i++) {
const string name { "username_" + std::to_string(i) };
const string pass { random_string(20) };
const string auth { i < 50 ? "MYSQL_NATIVE_PASSWORD" : "CACHING_SHA2_PASSWORD" };
const user_creds_t user_creds { name, auth, pass };
// TODO: Current 'auth_switch' limitation. Set 'caching_sha2_password' as default-auth method.
if (i < 50) {
MYSQL_QUERY(admin, "SET mysql-default_authentication_plugin='mysql_native_password'");
MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
} else {
MYSQL_QUERY(admin, "SET mysql-default_authentication_plugin='caching_sha2_password'");
MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
}
diag(
"Testing Client-to-MySQL with SQLite3 created hashes user:'%s', pass:'%s', auth:'%s'",
name.c_str(), pass.c_str(), auth.c_str()
);
if (create_mysql_user(mysql, user_creds)) {
diag("Failed to create MySQL user. Aborting further testing");
goto cleanup;
}
if (config_proxysql_user(admin, user_creds)) {
diag("Failed to create ProxySQL user. Aborting further testing");
goto cleanup;
}
diag(
"Creating connection to ProxySQL user:'%s', pass:'%s', auth:'%s'",
name.c_str(), pass.c_str(), auth.c_str()
);
MYSQL* proxy = mysql_init(NULL);
mysql_ssl_set(proxy, NULL, NULL, NULL, NULL, NULL);
if (
!mysql_real_connect(
proxy, cl.host, name.c_str(), pass.c_str(), NULL, cl.port, NULL, CLIENT_SSL
)
) {
diag("Failed to connect to ProxySQL error:'%s'", mysql_error(proxy));
goto cleanup;
}
int rc = mysql_query(proxy, "/* create_new_connection=1 */ DO 1");
ok(rc == 0, "End-to-end connection should succeed rc:'%d', err:'%s'", rc, mysql_error(proxy));
mysql_close(proxy);
}
const string SEL_POOL_CONNS {
"SELECT SUM(ConnUsed + ConnFree) FROM stats.stats_mysql_connection_pool"
" WHERE hostgroup=" + TAP_MYSQL8_BACKEND_HG_S
};
const string COND_CONN_CREATION {
"SELECT IIF((" + SEL_POOL_CONNS + ")=" + RAND_USERS_GEN_S + ", 'TRUE', 'FALSE')"
};
wait_for_cond(admin, COND_CONN_CREATION, 10);
ext_val_t<uint64_t> cur_conns { mysql_query_ext_val(admin, SEL_POOL_CONNS, uint64_t(0)) };
if (cur_conns.err) {
const string err { get_ext_val_err(admin, cur_conns) };
diag("Fetching conn count from pool failed err:'%s'", err.c_str());
}
ok(
RAND_USERS_GEN == cur_conns.val,
"Number of backend conns created should match conn attempts exp:'%u', act:'%lu'",
RAND_USERS_GEN, cur_conns.val
);
}
cleanup:
mysql_close(mysql);
mysql_close(admin);
return exit_status();
}