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

594 lines
20 KiB

/**
* @file auth_unit-t.cpp
* @brief Unit tests for MySQL_Authentication and PgSQL_Authentication.
*
* Tests the authentication subsystem in isolation without a running
* ProxySQL instance. Covers:
* - Core CRUD: add, lookup, del, exists
* - Credential management: SHA1, clear-text passwords
* - Connection counting and max_connections enforcement
* - Bulk operations: set_all_inactive, remove_inactives, reset
* - Runtime checksums
* - Memory tracking
* - Frontend vs backend separation
* - PgSQL Authentication parity
*
* @see Phase 2.2 of the Unit Testing Framework (GitHub issue #5474)
*/
#include "tap.h"
#include "test_globals.h"
#include "test_init.h"
#include "proxysql.h"
#include "MySQL_Authentication.hpp"
#include "PgSQL_Authentication.h"
#include <cstring>
#include <openssl/sha.h>
// Extern declarations for Glo* pointers (defined in test_globals.cpp)
extern MySQL_Authentication *GloMyAuth;
extern PgSQL_Authentication *GloPgAuth;
// ============================================================================
// Helper: add a MySQL frontend user with common defaults
// ============================================================================
static bool mysql_add_frontend(MySQL_Authentication *auth,
const char *user, const char *pass,
int default_hg = 0, int max_conn = 100)
{
return auth->add(
(char *)user, (char *)pass, USERNAME_FRONTEND,
false, // use_ssl
default_hg, // default_hostgroup
(char *)"", // default_schema
false, // schema_locked
false, // transaction_persistent
false, // fast_forward
max_conn, // max_connections
(char *)"", // attributes
(char *)"" // comment
);
}
// ============================================================================
// Helper: add a MySQL backend user
// ============================================================================
static bool mysql_add_backend(MySQL_Authentication *auth,
const char *user, const char *pass,
int default_hg = 0, int max_conn = 100)
{
return auth->add(
(char *)user, (char *)pass, USERNAME_BACKEND,
false, default_hg, (char *)"", false, false, false,
max_conn, (char *)"", (char *)""
);
}
// ============================================================================
// Helper: add a PgSQL frontend user
// ============================================================================
static bool pgsql_add_frontend(PgSQL_Authentication *auth,
const char *user, const char *pass,
int default_hg = 0, int max_conn = 100)
{
return auth->add(
(char *)user, (char *)pass, USERNAME_FRONTEND,
false, // use_ssl
default_hg, // default_hostgroup
false, // transaction_persistent
false, // fast_forward
max_conn, // max_connections
(char *)"", // attributes
(char *)"" // comment
);
}
// ============================================================================
// 1. MySQL_Authentication: Core CRUD
// ============================================================================
/**
* @brief Test basic add + exists + lookup cycle.
*/
static void test_mysql_add_exists_lookup() {
mysql_add_frontend(GloMyAuth, "alice", "pass123", 1, 50);
ok(GloMyAuth->exists((char *)"alice") == true,
"MySQL: exists() returns true for added frontend user");
ok(GloMyAuth->exists((char *)"unknown") == false,
"MySQL: exists() returns false for nonexistent user");
// Lookup with dup options
dup_account_details_t dup = {true, true, true};
account_details_t ad = GloMyAuth->lookup(
(char *)"alice", USERNAME_FRONTEND, dup);
ok(ad.password != nullptr && strcmp(ad.password, "pass123") == 0,
"MySQL: lookup() returns correct password");
ok(ad.default_hostgroup == 1,
"MySQL: lookup() returns correct default_hostgroup");
ok(ad.max_connections == 50,
"MySQL: lookup() returns correct max_connections");
free_account_details(ad);
}
/**
* @brief Test that exists() only checks frontends, not backends.
*/
static void test_mysql_exists_frontend_only() {
mysql_add_backend(GloMyAuth, "backend_only", "secret");
ok(GloMyAuth->exists((char *)"backend_only") == false,
"MySQL: exists() returns false for backend-only user");
// But lookup with USERNAME_BACKEND should find it
dup_account_details_t dup = {false, false, false};
account_details_t ad = GloMyAuth->lookup(
(char *)"backend_only", USERNAME_BACKEND, dup);
ok(ad.password != nullptr && strcmp(ad.password, "secret") == 0,
"MySQL: lookup(BACKEND) finds backend user");
free_account_details(ad);
}
/**
* @brief Test that add() overwrites on duplicate username.
*/
static void test_mysql_add_overwrites() {
mysql_add_frontend(GloMyAuth, "bob", "old_pass", 1, 10);
mysql_add_frontend(GloMyAuth, "bob", "new_pass", 2, 20);
dup_account_details_t dup = {true, false, false};
account_details_t ad = GloMyAuth->lookup(
(char *)"bob", USERNAME_FRONTEND, dup);
ok(ad.password != nullptr && strcmp(ad.password, "new_pass") == 0,
"MySQL: add() overwrites password on duplicate");
ok(ad.default_hostgroup == 2,
"MySQL: add() overwrites default_hostgroup on duplicate");
ok(ad.max_connections == 20,
"MySQL: add() overwrites max_connections on duplicate");
free_account_details(ad);
}
/**
* @brief Test del() removes a user.
*/
static void test_mysql_del() {
mysql_add_frontend(GloMyAuth, "charlie", "pass");
ok(GloMyAuth->exists((char *)"charlie") == true,
"MySQL: user exists before del()");
bool deleted = GloMyAuth->del((char *)"charlie", USERNAME_FRONTEND);
ok(deleted == true, "MySQL: del() returns true for existing user");
ok(GloMyAuth->exists((char *)"charlie") == false,
"MySQL: user gone after del()");
bool deleted_again = GloMyAuth->del((char *)"charlie", USERNAME_FRONTEND);
ok(deleted_again == false,
"MySQL: del() returns false for already-deleted user");
}
/**
* @brief Test lookup() returns empty struct for nonexistent user.
*/
static void test_mysql_lookup_not_found() {
dup_account_details_t dup = {true, true, true};
account_details_t ad = GloMyAuth->lookup(
(char *)"nonexistent", USERNAME_FRONTEND, dup);
ok(ad.password == nullptr,
"MySQL: lookup() returns null password for nonexistent user");
ok(ad.username == nullptr,
"MySQL: lookup() returns null username for nonexistent user");
}
// ============================================================================
// 2. MySQL_Authentication: Credential Management
// ============================================================================
/**
* @brief Test set_SHA1() stores and retrieves SHA1 hash.
*/
static void test_mysql_sha1() {
mysql_add_frontend(GloMyAuth, "sha1user", "pass");
unsigned char sha1_hash[SHA_DIGEST_LENGTH];
SHA1((unsigned char *)"pass", 4, sha1_hash);
bool set_ok = GloMyAuth->set_SHA1(
(char *)"sha1user", USERNAME_FRONTEND, sha1_hash);
ok(set_ok == true, "MySQL: set_SHA1() returns true");
// Lookup with sha1 duplication
dup_account_details_t dup = {false, true, false};
account_details_t ad = GloMyAuth->lookup(
(char *)"sha1user", USERNAME_FRONTEND, dup);
ok(ad.sha1_pass != nullptr, "MySQL: SHA1 pass retrieved via lookup()");
if (ad.sha1_pass != nullptr) {
ok(memcmp(ad.sha1_pass, sha1_hash, SHA_DIGEST_LENGTH) == 0,
"MySQL: SHA1 hash matches what was set");
} else {
ok(0, "MySQL: SHA1 hash was unexpectedly null");
}
// set_SHA1 on nonexistent user
bool set_fail = GloMyAuth->set_SHA1(
(char *)"nobody", USERNAME_FRONTEND, sha1_hash);
ok(set_fail == false,
"MySQL: set_SHA1() returns false for nonexistent user");
free_account_details(ad);
}
/**
* @brief Test set_clear_text_password() for PRIMARY and ADDITIONAL,
* and verify stored values are retrievable via lookup().
*/
static void test_mysql_clear_text_password() {
mysql_add_frontend(GloMyAuth, "ctpuser", "original");
bool set_ok = GloMyAuth->set_clear_text_password(
(char *)"ctpuser", USERNAME_FRONTEND,
"clearpass", PASSWORD_TYPE::PRIMARY);
ok(set_ok == true,
"MySQL: set_clear_text_password(PRIMARY) returns true");
set_ok = GloMyAuth->set_clear_text_password(
(char *)"ctpuser", USERNAME_FRONTEND,
"altpass", PASSWORD_TYPE::ADDITIONAL);
ok(set_ok == true,
"MySQL: set_clear_text_password(ADDITIONAL) returns true");
// Verify stored clear-text passwords are retrievable
dup_account_details_t dup = {false, false, false};
account_details_t ad = GloMyAuth->lookup(
(char *)"ctpuser", USERNAME_FRONTEND, dup);
ok(ad.clear_text_password[PASSWORD_TYPE::PRIMARY] != nullptr
&& strcmp(ad.clear_text_password[PASSWORD_TYPE::PRIMARY], "clearpass") == 0,
"MySQL: PRIMARY clear_text_password retrievable via lookup()");
ok(ad.clear_text_password[PASSWORD_TYPE::ADDITIONAL] != nullptr
&& strcmp(ad.clear_text_password[PASSWORD_TYPE::ADDITIONAL], "altpass") == 0,
"MySQL: ADDITIONAL clear_text_password retrievable via lookup()");
free_account_details(ad);
bool set_fail = GloMyAuth->set_clear_text_password(
(char *)"nobody", USERNAME_FRONTEND,
"pass", PASSWORD_TYPE::PRIMARY);
ok(set_fail == false,
"MySQL: set_clear_text_password() returns false for nonexistent user");
}
// ============================================================================
// 3. MySQL_Authentication: Connection Counting
// ============================================================================
/**
* @brief Test increase/decrease frontend user connections.
*/
static void test_mysql_connection_counting() {
mysql_add_frontend(GloMyAuth, "connuser", "pass", 0, 3);
// Increase connections until limit
int remaining;
remaining = GloMyAuth->increase_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
ok(remaining > 0, "MySQL: 1st connection: remaining > 0");
remaining = GloMyAuth->increase_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
ok(remaining > 0, "MySQL: 2nd connection: remaining > 0");
remaining = GloMyAuth->increase_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
ok(remaining > 0, "MySQL: 3rd connection: remaining > 0");
// At limit now — next should return 0
remaining = GloMyAuth->increase_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
ok(remaining == 0, "MySQL: 4th connection rejected (max_connections=3)");
// Decrease and verify we can connect again
GloMyAuth->decrease_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
remaining = GloMyAuth->increase_frontend_user_connections(
(char *)"connuser", PASSWORD_TYPE::PRIMARY);
ok(remaining > 0,
"MySQL: connection allowed after decrease");
}
// ============================================================================
// 4. MySQL_Authentication: Bulk Operations
// ============================================================================
/**
* @brief Test set_all_inactive + remove_inactives pattern.
*/
static void test_mysql_inactive_pattern() {
// Start fresh
GloMyAuth->reset();
mysql_add_frontend(GloMyAuth, "keep_me", "pass1");
mysql_add_frontend(GloMyAuth, "remove_me", "pass2");
// Mark all inactive
GloMyAuth->set_all_inactive(USERNAME_FRONTEND);
// Re-add the one we want to keep (sets __active = true)
mysql_add_frontend(GloMyAuth, "keep_me", "pass1");
// Remove inactive users
GloMyAuth->remove_inactives(USERNAME_FRONTEND);
ok(GloMyAuth->exists((char *)"keep_me") == true,
"MySQL: re-added user survives remove_inactives()");
ok(GloMyAuth->exists((char *)"remove_me") == false,
"MySQL: inactive user removed by remove_inactives()");
}
/**
* @brief Test reset() clears all users.
*/
static void test_mysql_reset() {
mysql_add_frontend(GloMyAuth, "user1", "p1");
mysql_add_frontend(GloMyAuth, "user2", "p2");
mysql_add_backend(GloMyAuth, "user3", "p3");
GloMyAuth->reset();
ok(GloMyAuth->exists((char *)"user1") == false,
"MySQL: frontend user gone after reset()");
dup_account_details_t dup = {false, false, false};
account_details_t ad = GloMyAuth->lookup(
(char *)"user3", USERNAME_BACKEND, dup);
ok(ad.password == nullptr,
"MySQL: backend user gone after reset()");
}
// ============================================================================
// 5. MySQL_Authentication: Checksums
// ============================================================================
/**
* @brief Test runtime checksum behavior.
*/
static void test_mysql_checksums() {
GloMyAuth->reset();
uint64_t empty_checksum = GloMyAuth->get_runtime_checksum();
ok(empty_checksum == 0,
"MySQL: checksum is 0 with no users");
mysql_add_frontend(GloMyAuth, "checksumA", "passA", 1);
uint64_t checksum1 = GloMyAuth->get_runtime_checksum();
ok(checksum1 != 0,
"MySQL: checksum is non-zero with users");
mysql_add_frontend(GloMyAuth, "checksumB", "passB", 2);
uint64_t checksum2 = GloMyAuth->get_runtime_checksum();
ok(checksum2 != checksum1,
"MySQL: checksum changes when users are added");
// Modify a user and verify checksum changes
mysql_add_frontend(GloMyAuth, "checksumA", "passA_changed", 1);
uint64_t checksum3 = GloMyAuth->get_runtime_checksum();
ok(checksum3 != checksum2,
"MySQL: checksum changes when password is modified");
}
// ============================================================================
// 6. MySQL_Authentication: Memory Usage
// ============================================================================
/**
* @brief Test memory_usage() tracking.
*/
static void test_mysql_memory() {
GloMyAuth->reset();
unsigned int mem_empty = GloMyAuth->memory_usage();
mysql_add_frontend(GloMyAuth, "memuser1", "password1");
unsigned int mem_one = GloMyAuth->memory_usage();
ok(mem_one > mem_empty,
"MySQL: memory_usage() increases after add()");
mysql_add_frontend(GloMyAuth, "memuser2", "password2_longer");
unsigned int mem_two = GloMyAuth->memory_usage();
ok(mem_two > mem_one,
"MySQL: memory_usage() increases with more users");
GloMyAuth->reset();
unsigned int mem_after_reset = GloMyAuth->memory_usage();
ok(mem_after_reset <= mem_empty,
"MySQL: memory_usage() returns to baseline after reset()");
}
// ============================================================================
// 7. MySQL_Authentication: Frontend vs Backend Separation
// ============================================================================
/**
* @brief Test that frontend and backend are independent.
*/
static void test_mysql_frontend_backend_separation() {
GloMyAuth->reset();
// Same username in both frontend and backend with different passwords
mysql_add_frontend(GloMyAuth, "dualuser", "frontend_pass", 1);
mysql_add_backend(GloMyAuth, "dualuser", "backend_pass", 2);
dup_account_details_t dup = {false, false, false};
account_details_t fe = GloMyAuth->lookup(
(char *)"dualuser", USERNAME_FRONTEND, dup);
ok(fe.password != nullptr && strcmp(fe.password, "frontend_pass") == 0,
"MySQL: frontend lookup returns frontend password");
ok(fe.default_hostgroup == 1,
"MySQL: frontend lookup returns frontend hostgroup");
account_details_t be = GloMyAuth->lookup(
(char *)"dualuser", USERNAME_BACKEND, dup);
ok(be.password != nullptr && strcmp(be.password, "backend_pass") == 0,
"MySQL: backend lookup returns backend password");
ok(be.default_hostgroup == 2,
"MySQL: backend lookup returns backend hostgroup");
free_account_details(fe);
free_account_details(be);
}
// ============================================================================
// 8. PgSQL_Authentication: Core CRUD
// ============================================================================
/**
* @brief Test PgSQL basic add + exists + lookup cycle.
*/
static void test_pgsql_add_exists_lookup() {
pgsql_add_frontend(GloPgAuth, "pg_alice", "pgpass", 1, 50);
ok(GloPgAuth->exists((char *)"pg_alice") == true,
"PgSQL: exists() returns true for added frontend user");
ok(GloPgAuth->exists((char *)"pg_unknown") == false,
"PgSQL: exists() returns false for nonexistent user");
// PgSQL lookup has different signature — returns password string
bool use_ssl = false;
int default_hg = -1, max_conn = -1;
bool trans_persist = false, fast_fwd = false;
void *sha1 = nullptr;
char *attrs = nullptr;
char *password = GloPgAuth->lookup(
(char *)"pg_alice", USERNAME_FRONTEND,
&use_ssl, &default_hg, &trans_persist, &fast_fwd,
&max_conn, &sha1, &attrs);
ok(password != nullptr && strcmp(password, "pgpass") == 0,
"PgSQL: lookup() returns correct password");
ok(default_hg == 1,
"PgSQL: lookup() returns correct default_hostgroup");
ok(max_conn == 50,
"PgSQL: lookup() returns correct max_connections");
if (password) free(password);
if (attrs) free(attrs);
if (sha1) free(sha1);
}
/**
* @brief Test PgSQL del() and reset().
*/
static void test_pgsql_del_and_reset() {
pgsql_add_frontend(GloPgAuth, "pg_del", "pass");
ok(GloPgAuth->exists((char *)"pg_del") == true,
"PgSQL: user exists before del()");
bool del_ok = GloPgAuth->del((char *)"pg_del", USERNAME_FRONTEND);
ok(del_ok == true, "PgSQL: del() returns true");
ok(GloPgAuth->exists((char *)"pg_del") == false,
"PgSQL: user gone after del()");
// Test reset
pgsql_add_frontend(GloPgAuth, "pg_r1", "p1");
pgsql_add_frontend(GloPgAuth, "pg_r2", "p2");
GloPgAuth->reset();
ok(GloPgAuth->exists((char *)"pg_r1") == false,
"PgSQL: user gone after reset()");
}
/**
* @brief Test PgSQL connection counting.
*/
static void test_pgsql_connection_counting() {
GloPgAuth->reset();
pgsql_add_frontend(GloPgAuth, "pg_conn", "pass", 0, 2);
int r1 = GloPgAuth->increase_frontend_user_connections(
(char *)"pg_conn");
ok(r1 > 0, "PgSQL: 1st connection allowed");
int r2 = GloPgAuth->increase_frontend_user_connections(
(char *)"pg_conn");
ok(r2 > 0, "PgSQL: 2nd connection allowed");
int r3 = GloPgAuth->increase_frontend_user_connections(
(char *)"pg_conn");
ok(r3 == 0, "PgSQL: 3rd connection rejected (max=2)");
GloPgAuth->decrease_frontend_user_connections((char *)"pg_conn");
int r4 = GloPgAuth->increase_frontend_user_connections(
(char *)"pg_conn");
ok(r4 > 0, "PgSQL: connection allowed after decrease");
}
/**
* @brief Test PgSQL inactive pattern.
*/
static void test_pgsql_inactive_pattern() {
GloPgAuth->reset();
pgsql_add_frontend(GloPgAuth, "pg_keep", "p1");
pgsql_add_frontend(GloPgAuth, "pg_drop", "p2");
GloPgAuth->set_all_inactive(USERNAME_FRONTEND);
pgsql_add_frontend(GloPgAuth, "pg_keep", "p1");
GloPgAuth->remove_inactives(USERNAME_FRONTEND);
ok(GloPgAuth->exists((char *)"pg_keep") == true,
"PgSQL: re-added user survives remove_inactives()");
ok(GloPgAuth->exists((char *)"pg_drop") == false,
"PgSQL: inactive user removed");
}
// ============================================================================
// Main
// ============================================================================
int main() {
plan(60);
test_init_minimal();
test_init_auth();
// MySQL tests
test_mysql_add_exists_lookup(); // 5 tests
test_mysql_exists_frontend_only(); // 2 tests
test_mysql_add_overwrites(); // 3 tests
test_mysql_del(); // 4 tests
test_mysql_lookup_not_found(); // 2 tests
test_mysql_sha1(); // 4 tests
test_mysql_clear_text_password(); // 3 tests
test_mysql_connection_counting(); // 5 tests
test_mysql_inactive_pattern(); // 2 tests
test_mysql_reset(); // 2 tests
test_mysql_checksums(); // 4 tests
test_mysql_memory(); // 3 tests
test_mysql_frontend_backend_separation();// 4 tests
// PgSQL tests
test_pgsql_add_exists_lookup(); // 5 tests
test_pgsql_del_and_reset(); // 4 tests (49-52)
test_pgsql_connection_counting(); // 4 tests (53-56)
test_pgsql_inactive_pattern(); // 2 tests (57-58)
// Note: PgSQL does not have set_clear_text_password()
// Note: PgSQL does not have default_schema or schema_locked
test_cleanup_auth();
test_cleanup_minimal();
return exit_status();
}