Skip to content

add validation for LOAD ... FROM CONFIG #5036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: v3.0
Choose a base branch
from
Open
43 changes: 35 additions & 8 deletions lib/Admin_Handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1294,12 +1294,23 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query
int rows=0;
if (query_no_space[5] == 'P' || query_no_space[5] == 'p') {
rows=SPA->proxysql_config().Read_PgSQL_Users_from_configfile();
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql users from CONFIG\n");
if (rows < 0) {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load pgsql users from CONFIG due to validation errors\n");
SPA->send_error_msg_to_client(sess, "Configuration validation failed - check error log for details");
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql users from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
}
} else {
rows=SPA->proxysql_config().Read_MySQL_Users_from_configfile();
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql users from CONFIG\n");
if (rows < 0) {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load mysql users from CONFIG due to validation errors\n");
SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details");
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql users from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
}
}
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
GloVars.confFile->CloseFile();
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file);
Expand Down Expand Up @@ -1695,12 +1706,23 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query
int rows=0;
if (is_pgsql) {
rows=SPA->proxysql_config().Read_PgSQL_Servers_from_configfile();
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql servers from CONFIG\n");
if (rows < 0) {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load pgsql servers from CONFIG due to validation errors\n");
SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details");
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded pgsql servers from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
}
} else {
rows=SPA->proxysql_config().Read_MySQL_Servers_from_configfile();
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql servers from CONFIG\n");
if (rows < 0) {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load mysql servers from CONFIG due to validation errors\n");
SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details");
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql servers from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
}
}
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
GloVars.confFile->CloseFile();
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file);
Expand Down Expand Up @@ -1816,8 +1838,13 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query
ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa;
int rows=0;
rows=SPA->proxysql_config().Read_ProxySQL_Servers_from_configfile();
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded ProxySQL servers from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
if (rows < 0) {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Failed to load ProxySQL servers from CONFIG due to validation errors\n");
SPA->send_error_msg_to_client(sess, (char *)"Configuration validation failed - check error log for details");
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded ProxySQL servers from CONFIG\n");
SPA->send_ok_msg_to_client(sess, NULL, rows, query_no_space);
}
GloVars.confFile->CloseFile();
} else {
proxy_debug(PROXY_DEBUG_ADMIN, 4, "Unable to open or parse config file %s\n", GloVars.config_file);
Expand Down
213 changes: 211 additions & 2 deletions lib/ProxySQL_Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "cpp.h"

#include <sstream>
#include <set>
#include <tuple>

const char* config_header = "########################################################################################\n"
"# This config file is parsed using libconfig , and its grammar is described in:\n"
Expand Down Expand Up @@ -144,13 +146,94 @@ int ProxySQL_Config::Write_MySQL_Users_to_configfile(std::string& data) {
return 0;
}

/**
* Generic template function for validating config entries to prevent duplicates and validate mandatory fields
*
* @tparam PKTuple - Type of the primary key tuple (e.g., std::tuple<std::string, int>)
* @tparam ExtractorFunc - Function type that extracts primary key from a config Setting
* @param config_array - The libconfig Setting array to validate
* @param section_name - Name of the config section for error messages
* @param extractor - Function that extracts primary key tuple and validates mandatory fields
* @param admindb - Database connection for cleanup operations
* @param use_mysql_pragma - Whether to use MySQL-style PRAGMA commands
* @return 0 on success, -1 on validation failure
*/
template<typename PKTuple, typename ExtractorFunc>
int validate_config_entries(const Setting& config_array, const char* section_name,
ExtractorFunc extractor, SQLite3DB* admindb, bool use_mysql_pragma = true) {
int count = config_array.getLength();
std::set<PKTuple> pk_set;

// PRE-VALIDATION: Check for duplicates and mandatory fields
for (int i = 0; i < count; i++) {
const Setting& entry = config_array[i];
PKTuple pk_tuple;
std::string error_msg;

// Use the extractor function to get primary key and validate mandatory fields
if (!extractor(entry, pk_tuple, error_msg)) {
proxy_error("Admin: %s\n", error_msg.c_str());
if (use_mysql_pragma) {
admindb->execute("PRAGMA foreign_keys = OFF");
admindb->execute("PRAGMA foreign_keys = ON");
} else {
admindb->execute("PRAGMA foreign_keys = ON");
}
return -1;
}

// Check for duplicates within config file
if (pk_set.find(pk_tuple) != pk_set.end()) {
// Call extractor again to get detailed error message for duplicate
std::string duplicate_error;
extractor(entry, pk_tuple, duplicate_error); // This populates pk_tuple again
proxy_error("Admin: duplicate entry found in %s config section\n", section_name);
if (use_mysql_pragma) {
admindb->execute("PRAGMA foreign_keys = OFF");
admindb->execute("PRAGMA foreign_keys = ON");
} else {
admindb->execute("PRAGMA foreign_keys = ON");
}
return -1;
}
pk_set.insert(pk_tuple);
}

return 0; // Validation passed
}

int ProxySQL_Config::Read_MySQL_Users_from_configfile() {
const Setting& root = GloVars.confFile->cfg.getRoot();
if (root.exists("mysql_users")==false) return 0;
const Setting &mysql_users = root["mysql_users"];
int count = mysql_users.getLength();

// Define extractor function for MySQL users validation
auto mysql_users_extractor = [](const Setting& user, std::tuple<std::string, int>& pk_tuple, std::string& error_msg) -> bool {
std::string username;
int backend = 1; // default backend value

// Validate mandatory fields
if (user.lookupValue("username", username) == false) {
error_msg = "detected a mysql_users in config file without a mandatory username";
return false;
}
user.lookupValue("backend", backend); // Check if backend is specified

pk_tuple = std::make_tuple(username, backend);
return true;
};

// Use template validation function
int validation_result = validate_config_entries<std::tuple<std::string, int>>(
mysql_users, "mysql_users", mysql_users_extractor, admindb, true);

if (validation_result != 0) {
return validation_result;
}

// If validation passed, proceed with existing INSERT OR REPLACE logic
//fprintf(stderr, "Found %d users\n",count);
int i;
int rows=0;
admindb->execute("PRAGMA foreign_keys = OFF");
char *q=(char *)"INSERT OR REPLACE INTO mysql_users (username, password, active, use_ssl, default_hostgroup, default_schema, schema_locked, transaction_persistent, fast_forward, max_connections, attributes, comment) VALUES ('%s', '%s', %d, %d, %d, '%s', %d, %d, %d, %d, '%s','%s')";
Expand Down Expand Up @@ -1022,6 +1105,41 @@ int ProxySQL_Config::Read_MySQL_Servers_from_configfile() {
if (root.exists("mysql_servers")==true) {
const Setting &mysql_servers = root["mysql_servers"];
int count = mysql_servers.getLength();

// Define extractor function for MySQL servers validation
auto mysql_servers_extractor = [](const Setting& server, std::tuple<int, std::string, int>& pk_tuple, std::string& error_msg) -> bool {
std::string address;
int port = 3306;
int hostgroup;

// Validate mandatory fields
if (server.lookupValue("address", address) == false) {
if (server.lookupValue("hostname", address) == false) {
error_msg = "detected a mysql_servers in config file without a mandatory hostname";
return false;
}
}
server.lookupValue("port", port);
if (server.lookupValue("hostgroup", hostgroup) == false) {
if (server.lookupValue("hostgroup_id", hostgroup) == false) {
error_msg = "detected a mysql_servers in config file without a mandatory hostgroup_id";
return false;
}
}

pk_tuple = std::make_tuple(hostgroup, address, port);
return true;
};

// Use template validation function
int validation_result = validate_config_entries<std::tuple<int, std::string, int>>(
mysql_servers, "mysql_servers", mysql_servers_extractor, admindb, false);

if (validation_result != 0) {
return validation_result;
}

// If validation passed, proceed with existing INSERT OR REPLACE logic
//fprintf(stderr, "Found %d servers\n",count);
char *q=(char *)"INSERT OR REPLACE INTO mysql_servers (hostname, port, gtid_port, hostgroup_id, compression, weight, status, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) VALUES (\"%s\", %d, %d, %d, %d, %d, \"%s\", %d, %d, %d, %d, '%s')";
for (i=0; i< count; i++) {
Expand Down Expand Up @@ -1465,6 +1583,37 @@ int ProxySQL_Config::Read_ProxySQL_Servers_from_configfile() {
if (root.exists("proxysql_servers")==true) {
const Setting & proxysql_servers = root["proxysql_servers"];
int count = proxysql_servers.getLength();

// Define extractor function for ProxySQL servers validation
auto proxysql_servers_extractor = [](const Setting& server, std::tuple<std::string, int>& pk_tuple, std::string& error_msg) -> bool {
std::string address;
int port;

// Validate mandatory fields
if (server.lookupValue("address", address) == false) {
if (server.lookupValue("hostname", address) == false) {
error_msg = "detected a proxysql_servers in config file without a mandatory hostname";
return false;
}
}
if (server.lookupValue("port", port) == false) {
error_msg = "detected a proxysql_servers in config file without a mandatory port";
return false;
}

pk_tuple = std::make_tuple(address, port);
return true;
};

// Use template validation function
int validation_result = validate_config_entries<std::tuple<std::string, int>>(
proxysql_servers, "proxysql_servers", proxysql_servers_extractor, admindb, false);

if (validation_result != 0) {
return validation_result;
}

// If validation passed, proceed with existing INSERT OR REPLACE logic
//fprintf(stderr, "Found %d servers\n",count);
char *q=(char *)"INSERT OR REPLACE INTO proxysql_servers (hostname, port, weight, comment) VALUES (\"%s\", %d, %d, '%s')";
for (i=0; i< count; i++) {
Expand Down Expand Up @@ -1629,6 +1778,41 @@ int ProxySQL_Config::Read_PgSQL_Servers_from_configfile() {
if (root.exists("pgsql_servers") == true) {
const Setting& pgsql_servers = root["pgsql_servers"];
int count = pgsql_servers.getLength();

// Define extractor function for PostgreSQL servers validation
auto pgsql_servers_extractor = [](const Setting& server, std::tuple<int, std::string, int>& pk_tuple, std::string& error_msg) -> bool {
std::string address;
int port = 5432;
int hostgroup;

// Validate mandatory fields
if (server.lookupValue("address", address) == false) {
if (server.lookupValue("hostname", address) == false) {
error_msg = "detected a pgsql_servers in config file without a mandatory hostname";
return false;
}
}
server.lookupValue("port", port);
if (server.lookupValue("hostgroup", hostgroup) == false) {
if (server.lookupValue("hostgroup_id", hostgroup) == false) {
error_msg = "detected a pgsql_servers in config file without a mandatory hostgroup_id";
return false;
}
}

pk_tuple = std::make_tuple(hostgroup, address, port);
return true;
};

// Use template validation function
int validation_result = validate_config_entries<std::tuple<int, std::string, int>>(
pgsql_servers, "pgsql_servers", pgsql_servers_extractor, admindb, false);

if (validation_result != 0) {
return validation_result;
}

// If validation passed, proceed with existing INSERT OR REPLACE logic
//fprintf(stderr, "Found %d servers\n",count);
char* q = (char*)"INSERT OR REPLACE INTO pgsql_servers (hostname, port, hostgroup_id, compression, weight, status, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) VALUES (\"%s\", %d, %d, %d, %d, \"%s\", %d, %d, %d, %d, '%s')";
for (i = 0; i < count; i++) {
Expand Down Expand Up @@ -1782,8 +1966,33 @@ int ProxySQL_Config::Read_PgSQL_Users_from_configfile() {
if (root.exists("pgsql_users") == false) return 0;
const Setting& pgsql_users = root["pgsql_users"];
int count = pgsql_users.getLength();

// Define extractor function for PostgreSQL users validation
auto pgsql_users_extractor = [](const Setting& user, std::tuple<std::string, int>& pk_tuple, std::string& error_msg) -> bool {
std::string username;
int backend = 1; // default backend value

// Validate mandatory fields
if (user.lookupValue("username", username) == false) {
error_msg = "detected a pgsql_users in config file without a mandatory username";
return false;
}
user.lookupValue("backend", backend); // Check if backend is specified

pk_tuple = std::make_tuple(username, backend);
return true;
};

// Use template validation function
int validation_result = validate_config_entries<std::tuple<std::string, int>>(
pgsql_users, "pgsql_users", pgsql_users_extractor, admindb, true);

if (validation_result != 0) {
return validation_result;
}

// If validation passed, proceed with existing INSERT OR REPLACE logic
//fprintf(stderr, "Found %d users\n",count);
int i;
int rows = 0;
admindb->execute("PRAGMA foreign_keys = OFF");
char* q = (char*)"INSERT OR REPLACE INTO pgsql_users (username, password, active, use_ssl, default_hostgroup, transaction_persistent, fast_forward, max_connections, attributes, comment) VALUES ('%s', '%s', %d, %d, %d, %d, %d, %d, '%s','%s')";
Expand Down
Loading