diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index ed1321e5f1..658a9ef98b 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -1709,7 +1709,7 @@ int MySQL_Protocol::PPHR_1(unsigned char *pkt, unsigned int len, bool& ret, MyPr if ((*myds)->switching_auth_stage == 5 && *pkt == 2) { proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . Client requested the caching_sha2_password RSA public key\n", - (*myds), (*myds)->sess, vars1.user); + (*myds)->sess, (*myds), vars1.user); proxy_error( "User '%s'@'%s' requested the caching_sha2_password RSA public key, which ProxySQL does not" " serve. Connect using TLS instead.\n", diff --git a/test/repro/README.md b/test/repro/README.md index b9e127976e..dce3651229 100644 --- a/test/repro/README.md +++ b/test/repro/README.md @@ -36,7 +36,7 @@ needs debug-only admin commands. ## COLD_START -``` +```text COLD_START=0 (default) use the existing ProxySQL instance COLD_START=1 destroy and recreate it first ``` diff --git a/test/repro/reg_test_5363_admin_monitor_caching_sha2.bash b/test/repro/reg_test_5363_admin_monitor_caching_sha2.bash index 455a649524..3bf8a97005 100755 --- a/test/repro/reg_test_5363_admin_monitor_caching_sha2.bash +++ b/test/repro/reg_test_5363_admin_monitor_caching_sha2.bash @@ -66,36 +66,45 @@ set -uo pipefail WORKSPACE="${WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" INFRA_ID="${INFRA_ID:-dev-$USER}" TAP_GROUP="${TAP_GROUP:-no-infra-g1}" -COLD_START="${COLD_START:-0}" +COLD_START="-e" +export WORKSPACE INFRA_ID TAP_GROUP CTR="proxysql.${INFRA_ID}" DATADIR="${WORKSPACE}/ci_infra_logs/${INFRA_ID}/proxysql" MON_USER='monitor' MON_PASS='monitorpass' -BASE_CREDS='admin:admin;radmin:radmin;cluster1:secret1pass' +LOAD_MYSQL_VARS='LOAD MYSQL VARIABLES TO RUNTIME;' PASS=0; FAIL=0; BUGS=0 -ok() { PASS=$((PASS+1)); printf ' ok %d - %s\n' "$((PASS+FAIL))" "$1"; } -nok() { FAIL=$((FAIL+1)); printf ' NOT OK %d - %s\n' "$((PASS+FAIL))" "$1"; } -bug() { FAIL=$((FAIL+1)); BUGS=$((BUGS+1)); printf ' NOT OK %d - [BUG #5363] %s\n' "$((PASS+FAIL))" "$1"; } -hdr() { printf '\n== %s ==\n' "$1"; } -die() { printf '\nFATAL: %s\n' "$1" >&2; exit 2; } +ok() { local msg="$1"; PASS=$((PASS+1)); printf ' ok %d - %s\n' "$((PASS+FAIL))" "$msg"; return 0; } +nok() { local msg="$1"; FAIL=$((FAIL+1)); printf ' NOT OK %d - %s\n' "$((PASS+FAIL))" "$msg"; return 0; } +bug() { local msg="$1"; FAIL=$((FAIL+1)); BUGS=$((BUGS+1)); printf ' NOT OK %d - [BUG #5363] %s\n' "$((PASS+FAIL))" "$msg"; return 0; } +hdr() { local msg="$1"; printf '\n== %s ==\n' "$msg"; return 0; } +die() { local msg="$1"; printf '\nFATAL: %s\n' "$msg" >&2; exit 2; } IP='' -adm() { mysql -h"$IP" -P6032 -uradmin -pradmin --protocol=TCP -NBe "$1" 2>/dev/null; } +adm() { + local sql="$1" + mysql -h"$IP" -P6032 -uradmin -pradmin --protocol=TCP -NBe "$sql" 2>/dev/null + return $? +} setvar() { # setvar - adm "UPDATE global_variables SET variable_value='$2' WHERE variable_name='$1'; $3" >/dev/null + local name="$1" value="$2" load_stmt="$3" + adm "UPDATE global_variables SET variable_value='$value' WHERE variable_name='$name'; $load_stmt" >/dev/null + return $? } # login_local -- runs the client INSIDE the container login_local() { - docker exec "$CTR" mysql -h127.0.0.1 -P6032 -u"$1" -p"$2" \ - --protocol=TCP --ssl-mode="$3" -NBe "SELECT 1;" >/dev/null 2>&1 + local user="$1" pass="$2" ssl_mode="$3" + docker exec "$CTR" mysql -h127.0.0.1 -P6032 -u"$user" -p"$pass" \ + --protocol=TCP --ssl-mode="$ssl_mode" -NBe "SELECT 1;" >/dev/null 2>&1 + return $? } # ------------------------------------------------------------- preflight --- command -v docker >/dev/null || die "docker not found" command -v mysql >/dev/null || die "mysql client not found" -[ -x "${WORKSPACE}/src/proxysql" ] || die "no binary at ${WORKSPACE}/src/proxysql -- build it first" +[[ -x "${WORKSPACE}/src/proxysql" ]] || die "no binary at ${WORKSPACE}/src/proxysql -- build it first" "${WORKSPACE}/src/proxysql" --version 2>&1 | grep -q '_DEBUG' \ || die "${WORKSPACE}/src/proxysql is not a DEBUG build" @@ -104,24 +113,21 @@ command -v mysql >/dev/null || die "mysql client not found" # PPHR_5passwordFalse_0(), which never calls set_clear_text_password(), so the # caching_sha2 cleartext cache plays no part in the result. COLD_START is offered # only for symmetry with the #5985 companion and for a guaranteed-clean run. -if [ "$COLD_START" = "1" ]; then +if [[ "$COLD_START" = "1" ]]; then hdr "Cold start: destroying and recreating ProxySQL for INFRA_ID=${INFRA_ID}" - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/stop-proxysql-isolated.bash" >/dev/null 2>&1 + "${WORKSPACE}/test/infra/control/stop-proxysql-isolated.bash" >/dev/null 2>&1 rm -f "${DATADIR}"/proxysql.db "${DATADIR}"/proxysql_debug.db \ "${DATADIR}"/proxysql_stats.db "${DATADIR}"/sqlite3server.db 2>/dev/null - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/start-proxysql-isolated.bash" >/dev/null 2>&1 \ + "${WORKSPACE}/test/infra/control/start-proxysql-isolated.bash" >/dev/null 2>&1 \ || die "start-proxysql-isolated.bash failed" else hdr "Using the existing ProxySQL instance (set COLD_START=1 to recreate it)" - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/ensure-infras.bash" >/dev/null 2>&1 \ + "${WORKSPACE}/test/infra/control/ensure-infras.bash" >/dev/null 2>&1 \ || die "ensure-infras.bash failed" fi IP="$(docker inspect "$CTR" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null)" -[ -n "$IP" ] || die "could not determine IP of ${CTR}" +[[ -n "$IP" ]] || die "could not determine IP of ${CTR}" docker exec "$CTR" which mysql >/dev/null 2>&1 || die "no mysql client inside ${CTR}" # Guard against testing a STALE binary. The container bind-mounts @@ -134,7 +140,7 @@ docker exec "$CTR" which mysql >/dev/null 2>&1 || die "no mysql client inside ${ BIN_MTIME="$(stat -c %Y "${WORKSPACE}/src/proxysql" 2>/dev/null || echo 0)" CTR_STARTED="$(docker inspect -f '{{.State.StartedAt}}' "$CTR" 2>/dev/null)" CTR_EPOCH="$(date -d "$CTR_STARTED" +%s 2>/dev/null || echo 0)" -if [ "$BIN_MTIME" -gt "$CTR_EPOCH" ] 2>/dev/null; then +if [[ "$BIN_MTIME" -gt "$CTR_EPOCH" ]] 2>/dev/null; then die "the running ProxySQL container predates ${WORKSPACE}/src/proxysql, so it is NOT running the binary you just built. Re-run with COLD_START=1, or refresh the container with: @@ -143,32 +149,42 @@ if [ "$BIN_MTIME" -gt "$CTR_EPOCH" ] 2>/dev/null; then fi echo " ${CTR} at ${IP} -- $(adm 'SELECT @@admin-version;')" +# Capture the instance's ACTUAL pre-existing values so the EXIT handler can put +# back exactly what was there. Restoring hardcoded defaults instead would +# silently rewrite the configuration of a shared instance -- see the "Restore any +# global state that was changed" rule in test/repro/README.md. Note this script +# never modifies 'admin-admin_credentials', so it must not reset it either. +ORIG_AUTH_PLUGIN="$(adm 'SELECT @@mysql-default_authentication_plugin;')" +ORIG_MON_USER="$(adm 'SELECT @@mysql-monitor_username;')" +ORIG_MON_PASS="$(adm 'SELECT @@mysql-monitor_password;')" + restore() { - setvar 'admin-admin_credentials' "$BASE_CREDS" 'LOAD ADMIN VARIABLES TO RUNTIME;' - setvar 'mysql-default_authentication_plugin' 'mysql_native_password' 'LOAD MYSQL VARIABLES TO RUNTIME;' - setvar 'mysql-monitor_password' 'monitor' 'LOAD MYSQL VARIABLES TO RUNTIME;' + setvar 'mysql-default_authentication_plugin' "$ORIG_AUTH_PLUGIN" "$LOAD_MYSQL_VARS" + setvar 'mysql-monitor_username' "$ORIG_MON_USER" "$LOAD_MYSQL_VARS" + setvar 'mysql-monitor_password' "$ORIG_MON_PASS" "$LOAD_MYSQL_VARS" + return 0 } trap restore EXIT # ----------------------------------------------------------- preconditions --- hdr "Preconditions" -[ "$(adm 'SELECT count(*) FROM runtime_mysql_users;')" = "0" ] \ +[[ "$(adm 'SELECT count(*) FROM runtime_mysql_users;')" = "0" ]] \ && ok "no mysql_users rows (nothing can shadow the monitor credential)" \ || nok "mysql_users is not empty -- result would be confounded" adm "SELECT @@admin-admin_credentials;" | grep -q "${MON_USER}:" \ && nok "an admin credential named '${MON_USER}' exists -- wrong code path" \ || ok "no admin credential named '${MON_USER}' (PPHR_5passwordFalse_0 is the path)" -setvar 'mysql-monitor_username' "$MON_USER" 'LOAD MYSQL VARIABLES TO RUNTIME;' -setvar 'mysql-monitor_password' "$MON_PASS" 'LOAD MYSQL VARIABLES TO RUNTIME;' -[ "$(adm 'SELECT @@mysql-monitor_username;')" = "$MON_USER" ] && -[ "$(adm 'SELECT @@mysql-monitor_password;')" = "$MON_PASS" ] \ +setvar 'mysql-monitor_username' "$MON_USER" "$LOAD_MYSQL_VARS" +setvar 'mysql-monitor_password' "$MON_PASS" "$LOAD_MYSQL_VARS" +[[ "$(adm 'SELECT @@mysql-monitor_username;')" = "$MON_USER" ]] && +[[ "$(adm 'SELECT @@mysql-monitor_password;')" = "$MON_PASS" ]] \ && ok "mysql-monitor_username/password set to ${MON_USER}/${MON_PASS} (cleartext, as documented)" \ || nok "failed to set mysql-monitor_* variables" # ------------------------------------------------- baseline: native works --- hdr "Baseline: mysql_native_password" -setvar 'mysql-default_authentication_plugin' 'mysql_native_password' 'LOAD MYSQL VARIABLES TO RUNTIME;' +setvar 'mysql-default_authentication_plugin' 'mysql_native_password' "$LOAD_MYSQL_VARS" login_local "$MON_USER" "$MON_PASS" DISABLED \ && ok "${MON_USER} authenticates on :6032 over plaintext" \ || nok "${MON_USER} FAILED under native/plaintext -- baseline broken, investigate before trusting the rest" @@ -178,8 +194,8 @@ login_local "$MON_USER" "$MON_PASS" REQUIRED \ # ------------------------------------------------------------- the bug --- hdr "caching_sha2_password" -setvar 'mysql-default_authentication_plugin' 'caching_sha2_password' 'LOAD MYSQL VARIABLES TO RUNTIME;' -[ "$(adm 'SELECT @@mysql-default_authentication_plugin;')" = "caching_sha2_password" ] \ +setvar 'mysql-default_authentication_plugin' 'caching_sha2_password' "$LOAD_MYSQL_VARS" +[[ "$(adm 'SELECT @@mysql-default_authentication_plugin;')" = "caching_sha2_password" ]] \ && ok "mysql-default_authentication_plugin = caching_sha2_password" \ || nok "failed to switch the default authentication plugin" @@ -204,12 +220,12 @@ login_local "$MON_USER" "WRONGPASS" DISABLED \ # --------------------------------------------------------------- summary --- hdr "Summary" printf ' passed: %d\n failed: %d (of which %d are the reported bug)\n' "$PASS" "$FAIL" "$BUGS" -if [ "$FAIL" -eq 0 ]; then +if [[ "$FAIL" -eq 0 ]]; then echo echo " All assertions pass: #5363 appears FIXED on this build." exit 0 fi -if [ "$FAIL" -eq "$BUGS" ]; then +if [[ "$FAIL" -eq "$BUGS" ]]; then cat <<'EOS' REPRODUCED. Every failure is a [BUG #5363] assertion: diff --git a/test/repro/reg_test_5985_admin_caching_sha2_full_auth.bash b/test/repro/reg_test_5985_admin_caching_sha2_full_auth.bash index 134720e768..afe62dbe0f 100755 --- a/test/repro/reg_test_5985_admin_caching_sha2_full_auth.bash +++ b/test/repro/reg_test_5985_admin_caching_sha2_full_auth.bash @@ -54,31 +54,41 @@ set -uo pipefail WORKSPACE="${WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" INFRA_ID="${INFRA_ID:-dev-$USER}" TAP_GROUP="${TAP_GROUP:-no-infra-g1}" -COLD_START="${COLD_START:-0}" +COLD_START="-e" +export WORKSPACE INFRA_ID TAP_GROUP CTR="proxysql.${INFRA_ID}" DATADIR="${WORKSPACE}/ci_infra_logs/${INFRA_ID}/proxysql" USR='cs2adm' PW='secret' -BASE_CREDS='admin:admin;radmin:radmin;cluster1:secret1pass' +LOAD_MYSQL_VARS='LOAD MYSQL VARIABLES TO RUNTIME;' PASS=0; FAIL=0 -ok() { PASS=$((PASS+1)); printf ' ok %d - %s\n' "$((PASS+FAIL))" "$1"; } -nok() { FAIL=$((FAIL+1)); printf ' NOT OK %d - %s\n' "$((PASS+FAIL))" "$1"; } -hdr() { printf '\n== %s ==\n' "$1"; } -die() { printf '\nFATAL: %s\n' "$1" >&2; exit 2; } +ok() { local msg="$1"; PASS=$((PASS+1)); printf ' ok %d - %s\n' "$((PASS+FAIL))" "$msg"; return 0; } +nok() { local msg="$1"; FAIL=$((FAIL+1)); printf ' NOT OK %d - %s\n' "$((PASS+FAIL))" "$msg"; return 0; } +hdr() { local msg="$1"; printf '\n== %s ==\n' "$msg"; return 0; } +die() { local msg="$1"; printf '\nFATAL: %s\n' "$msg" >&2; exit 2; } IP='' -adm() { mysql -h"$IP" -P6032 -uradmin -pradmin --protocol=TCP -NBe "$1" 2>/dev/null; } -sha2cached() { adm "SELECT Variable_Value FROM stats_mysql_global WHERE Variable_Name='Client_Connections_sha2cached';"; } +adm() { + local sql="$1" + mysql -h"$IP" -P6032 -uradmin -pradmin --protocol=TCP -NBe "$sql" 2>/dev/null + return $? +} +sha2cached() { + adm "SELECT Variable_Value FROM stats_mysql_global WHERE Variable_Name='Client_Connections_sha2cached';" + return $? +} login() { # login -> 0 on success - mysql -h"$IP" -P6032 -u"$1" -p"$2" --protocol=TCP --ssl-mode="$3" -NBe "SELECT 1;" >/dev/null 2>&1 + local user="$1" pass="$2" ssl_mode="$3" + mysql -h"$IP" -P6032 -u"$user" -p"$pass" --protocol=TCP --ssl-mode="$ssl_mode" -NBe "SELECT 1;" >/dev/null 2>&1 + return $? } # ------------------------------------------------------------- preflight --- command -v docker >/dev/null || die "docker not found" command -v mysql >/dev/null || die "mysql client not found" -[ -x "${WORKSPACE}/src/proxysql" ] || die "no binary at ${WORKSPACE}/src/proxysql -- build it first" +[[ -x "${WORKSPACE}/src/proxysql" ]] || die "no binary at ${WORKSPACE}/src/proxysql -- build it first" "${WORKSPACE}/src/proxysql" --version 2>&1 | grep -q '_DEBUG' \ || die "${WORKSPACE}/src/proxysql is not a DEBUG build" @@ -87,26 +97,23 @@ command -v mysql >/dev/null || die "mysql client not found" # succeeded while Client_Connections_sha2cached was still 0. If the cache is warm # the run is aborted rather than reporting a meaningless pass -- see the guard # immediately after the instance is up. -if [ "$COLD_START" = "1" ]; then +if [[ "$COLD_START" = "1" ]]; then hdr "Cold start: destroying and recreating ProxySQL for INFRA_ID=${INFRA_ID}" - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/stop-proxysql-isolated.bash" >/dev/null 2>&1 + "${WORKSPACE}/test/infra/control/stop-proxysql-isolated.bash" >/dev/null 2>&1 rm -f "${DATADIR}"/proxysql.db "${DATADIR}"/proxysql_debug.db \ "${DATADIR}"/proxysql_stats.db "${DATADIR}"/sqlite3server.db 2>/dev/null docker ps -a --filter "name=^${CTR}$" --format '{{.Names}}' | grep -q . \ && die "container ${CTR} still present after stop" - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/start-proxysql-isolated.bash" >/dev/null 2>&1 \ + "${WORKSPACE}/test/infra/control/start-proxysql-isolated.bash" >/dev/null 2>&1 \ || die "start-proxysql-isolated.bash failed" else hdr "Using the existing ProxySQL instance (set COLD_START=1 to recreate it)" - WORKSPACE="$WORKSPACE" INFRA_ID="$INFRA_ID" TAP_GROUP="$TAP_GROUP" \ - "${WORKSPACE}/test/infra/control/ensure-infras.bash" >/dev/null 2>&1 \ + "${WORKSPACE}/test/infra/control/ensure-infras.bash" >/dev/null 2>&1 \ || die "ensure-infras.bash failed" fi IP="$(docker inspect "$CTR" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null)" -[ -n "$IP" ] || die "could not determine IP of ${CTR}" +[[ -n "$IP" ]] || die "could not determine IP of ${CTR}" echo " ${CTR} at ${IP} -- $(adm 'SELECT @@admin-version;')" # Guard against testing a STALE binary. The container bind-mounts @@ -119,7 +126,7 @@ echo " ${CTR} at ${IP} -- $(adm 'SELECT @@admin-version;')" BIN_MTIME="$(stat -c %Y "${WORKSPACE}/src/proxysql" 2>/dev/null || echo 0)" CTR_STARTED="$(docker inspect -f '{{.State.StartedAt}}' "$CTR" 2>/dev/null)" CTR_EPOCH="$(date -d "$CTR_STARTED" +%s 2>/dev/null || echo 0)" -if [ "$BIN_MTIME" -gt "$CTR_EPOCH" ] 2>/dev/null; then +if [[ "$BIN_MTIME" -gt "$CTR_EPOCH" ]] 2>/dev/null; then die "the running ProxySQL container predates ${WORKSPACE}/src/proxysql, so it is NOT running the binary you just built. Re-run with COLD_START=1, or refresh the container with: @@ -129,25 +136,35 @@ fi # Hard requirement, not an assertion: without a cold cache the central claim # cannot be tested at all, so refuse to run rather than emit a false pass. -if [ "$(sha2cached)" != "0" ]; then +if [[ "$(sha2cached)" != "0" ]]; then die "Client_Connections_sha2cached is '$(sha2cached)', not 0: the cleartext cache is already warm, so a successful login here would prove nothing. Re-run with COLD_START=1 to recreate the instance." fi +# Capture the instance's ACTUAL pre-existing values so the EXIT handler can put +# back exactly what was there. This script appends a credential to +# 'admin-admin_credentials' and switches the default authentication plugin; +# restoring hardcoded defaults instead would discard whatever the shared instance +# was configured with -- see the "Restore any global state that was changed" rule +# in test/repro/README.md. +ORIG_ADMIN_CREDS="$(adm 'SELECT @@admin-admin_credentials;')" +ORIG_AUTH_PLUGIN="$(adm 'SELECT @@mysql-default_authentication_plugin;')" + restore() { - adm "UPDATE global_variables SET variable_value='${BASE_CREDS}' WHERE variable_name='admin-admin_credentials'; + adm "UPDATE global_variables SET variable_value='${ORIG_ADMIN_CREDS}' WHERE variable_name='admin-admin_credentials'; LOAD ADMIN VARIABLES TO RUNTIME; - UPDATE global_variables SET variable_value='mysql_native_password' WHERE variable_name='mysql-default_authentication_plugin'; - LOAD MYSQL VARIABLES TO RUNTIME;" >/dev/null + UPDATE global_variables SET variable_value='${ORIG_AUTH_PLUGIN}' WHERE variable_name='mysql-default_authentication_plugin'; + ${LOAD_MYSQL_VARS}" >/dev/null + return 0 } trap restore EXIT hdr "Preconditions" -[ "$(adm 'SELECT count(*) FROM runtime_mysql_users;')" = "0" ] \ +[[ "$(adm 'SELECT count(*) FROM runtime_mysql_users;')" = "0" ]] \ && ok "no mysql_users rows (Finding 1 cannot interfere)" \ || nok "mysql_users is not empty -- result would be confounded" -[ "$(sha2cached)" = "0" ] \ +[[ "$(sha2cached)" = "0" ]] \ && ok "Client_Connections_sha2cached == 0 (cleartext cache is cold)" \ || nok "cleartext cache already warm -- the full-auth proof would be invalid" @@ -160,7 +177,7 @@ hdr "Preconditions" hdr "Installing a caching_sha2-hashed Admin credential" adm "UPDATE global_variables SET variable_value='caching_sha2_password' WHERE variable_name='mysql-default_authentication_plugin'; LOAD MYSQL VARIABLES TO RUNTIME;" >/dev/null -[ "$(adm 'SELECT @@mysql-default_authentication_plugin;')" = "caching_sha2_password" ] \ +[[ "$(adm 'SELECT @@mysql-default_authentication_plugin;')" = "caching_sha2_password" ]] \ && ok "mysql-default_authentication_plugin = caching_sha2_password" \ || nok "failed to set the default authentication plugin" @@ -178,22 +195,22 @@ adm "UPDATE global_variables SET variable_value='caching_sha2_password' WHERE va # misread as a full-auth failure here. On a build carrying the #5989 fix the loop # succeeds on the first attempt; it is retained so this script still gives a # correct answer when run against an older binary. -EXPECT_LEN=$(( ${#BASE_CREDS} + 1 + ${#USR} + 1 + 70 )) +EXPECT_LEN=$(( ${#ORIG_ADMIN_CREDS} + 1 + ${#USR} + 1 + 70 )) TAIL_EXPR="SUBSTR(variable_value, INSTR(variable_value,';${USR}:')+$(( ${#USR} + 2 )))" CLEAN=0 for _ in $(seq 1 20); do - adm "UPDATE global_variables SET variable_value='${BASE_CREDS}' WHERE variable_name='admin-admin_credentials'; + adm "UPDATE global_variables SET variable_value='${ORIG_ADMIN_CREDS}' WHERE variable_name='admin-admin_credentials'; UPDATE global_variables SET variable_value = variable_value || ';${USR}:' || CACHING_SHA2_PASSWORD('${PW}') WHERE variable_name='admin-admin_credentials';" >/dev/null read -r LEN SEMI COLON <<<"$(adm "SELECT LENGTH(variable_value), INSTR(${TAIL_EXPR},';'), INSTR(${TAIL_EXPR},':') FROM global_variables WHERE variable_name='admin-admin_credentials';")" - if [ "$LEN" = "$EXPECT_LEN" ] && [ "$SEMI" = "0" ] && [ "$COLON" = "0" ]; then CLEAN=1; break; fi + if [[ "$LEN" = "$EXPECT_LEN" && "$SEMI" = "0" && "$COLON" = "0" ]]; then CLEAN=1; break; fi done -[ "$CLEAN" = "1" ] \ +[[ "$CLEAN" = "1" ]] \ && ok "generated a 70-byte \$A\$ hash free of ';' and ':' (credentials len ${LEN})" \ || nok "could not generate a delimiter-free hash in 20 attempts" adm "LOAD ADMIN VARIABLES TO RUNTIME;" >/dev/null -[ "$(sha2cached)" = "0" ] \ +[[ "$(sha2cached)" = "0" ]] \ && ok "cache still cold immediately before the auth test" \ || nok "cache warmed prematurely" @@ -204,7 +221,7 @@ login "$USR" "$PW" REQUIRED \ || nok "${USR} FAILED on :6032 over TLS -- full auth did not complete" C="$(sha2cached)" -[ "$C" = "0" ] \ +[[ "$C" = "0" ]] \ && ok "Client_Connections_sha2cached still 0 -> that was a FULL AUTH, not a cache hit" \ || nok "counter is ${C} -- the login may have been served from the cleartext cache" @@ -217,14 +234,14 @@ login "$USR" "$PW" REQUIRED \ && ok "second TLS connection succeeded" \ || nok "second TLS connection failed" C2="$(sha2cached)" -[ "${C2:-0}" -gt 0 ] 2>/dev/null \ +[[ "${C2:-0}" -gt 0 ]] 2>/dev/null \ && ok "counter rose to ${C2} -> repeat connection used the cache, confirming the counter tracks cache hits" \ || nok "counter did not rise on a repeat connection; the proof above is not meaningful" # ---------------------------------------------------------------- done --- hdr "Summary" printf ' passed: %d\n failed: %d\n' "$PASS" "$FAIL" -if [ "$FAIL" -eq 0 ]; then +if [[ "$FAIL" -eq 0 ]]; then cat <<'EOS' CONCLUSION diff --git a/test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp b/test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp index 8f987b90be..ef9427a76b 100644 --- a/test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp +++ b/test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp @@ -117,6 +117,27 @@ static int set_var(MYSQL* admin, const char* name, const char* value, const char return EXIT_SUCCESS; } +/** + * @brief Read a global variable's current value. + * @return true when the variable was read; 'out' is then the exact stored value, + * which may legitimately be the empty string. Callers must distinguish "read an + * empty value" from "could not read", because restoring a substituted default + * over a legitimately empty value silently rewrites the instance's config. + */ +static bool read_var(MYSQL* admin, const char* name, string& out) { + const string q { + string("SELECT variable_value FROM global_variables WHERE variable_name='") + name + "'" + }; + if (mysql_query(admin, q.c_str())) { return false; } + MYSQL_RES* r = mysql_store_result(admin); + if (r == NULL) { return false; } + MYSQL_ROW row = mysql_fetch_row(r); + const bool found = (row != NULL && row[0] != NULL); + if (found) { out = row[0]; } + mysql_free_result(r); + return found; +} + int main() { CommandLine cl; @@ -146,7 +167,8 @@ int main() { }; // 1 admin connection + 2 preconditions + the cases + 2 restore checks - plan(1 + 2 + static_cast(CASES.size()) + 2); + // connected(1) + preconditions(2) + one per case + restore checks(3) + plan(1 + 2 + static_cast(CASES.size()) + 3); MYSQL* admin = mysql_init(NULL); if (!mysql_real_connect(admin, cl.admin_host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { @@ -155,24 +177,20 @@ int main() { } ok(true, "Connected to ProxySQL Admin at %s:%d", cl.admin_host, cl.admin_port); - // Remember what to put back. + // Remember what to put back. Every variable this test writes must be saved + // here, and restored to EXACTLY this value -- these run against a shared + // instance, so substituting a hardcoded default for a value that was + // legitimately empty would rewrite the instance's configuration. string orig_plugin {}; + string orig_mon_user {}; string orig_mon_pass {}; - { - MYSQL_QUERY_T(admin, "SELECT variable_value FROM global_variables WHERE variable_name='mysql-default_authentication_plugin'"); - MYSQL_RES* r = mysql_store_result(admin); - MYSQL_ROW row = mysql_fetch_row(r); - if (row && row[0]) { orig_plugin = row[0]; } - mysql_free_result(r); - - MYSQL_QUERY_T(admin, "SELECT variable_value FROM global_variables WHERE variable_name='mysql-monitor_password'"); - r = mysql_store_result(admin); - row = mysql_fetch_row(r); - if (row && row[0]) { orig_mon_pass = row[0]; } - mysql_free_result(r); - } - diag("Saved mysql-default_authentication_plugin='%s', mysql-monitor_password='%s'", - orig_plugin.c_str(), orig_mon_pass.c_str()); + const bool have_plugin = read_var(admin, "mysql-default_authentication_plugin", orig_plugin); + const bool have_mon_user = read_var(admin, "mysql-monitor_username", orig_mon_user); + const bool have_mon_pass = read_var(admin, "mysql-monitor_password", orig_mon_pass); + diag("Saved mysql-default_authentication_plugin='%s'%s, mysql-monitor_username='%s'%s, mysql-monitor_password='%s'%s", + orig_plugin.c_str(), have_plugin ? "" : " (UNREADABLE - will not be restored)", + orig_mon_user.c_str(), have_mon_user ? "" : " (UNREADABLE - will not be restored)", + orig_mon_pass.c_str(), have_mon_pass ? "" : " (UNREADABLE - will not be restored)"); // --- Preconditions ------------------------------------------------------ { @@ -212,29 +230,40 @@ int main() { } // --- Restore ------------------------------------------------------------ - set_var(admin, "mysql-default_authentication_plugin", - orig_plugin.empty() ? "mysql_native_password" : orig_plugin.c_str(), - "LOAD MYSQL VARIABLES TO RUNTIME"); - set_var(admin, "mysql-monitor_password", - orig_mon_pass.empty() ? "monitor" : orig_mon_pass.c_str(), - "LOAD MYSQL VARIABLES TO RUNTIME"); + // Put back exactly what was read, including an empty value. A variable that + // could not be read is left alone rather than being overwritten with a guess. + if (have_plugin) { + set_var(admin, "mysql-default_authentication_plugin", orig_plugin.c_str(), + "LOAD MYSQL VARIABLES TO RUNTIME"); + } + if (have_mon_user) { + set_var(admin, "mysql-monitor_username", orig_mon_user.c_str(), + "LOAD MYSQL VARIABLES TO RUNTIME"); + } + if (have_mon_pass) { + set_var(admin, "mysql-monitor_password", orig_mon_pass.c_str(), + "LOAD MYSQL VARIABLES TO RUNTIME"); + } { - MYSQL_QUERY_T(admin, "SELECT variable_value FROM global_variables WHERE variable_name='mysql-default_authentication_plugin'"); - MYSQL_RES* r = mysql_store_result(admin); - MYSQL_ROW row = mysql_fetch_row(r); - const string now { (row && row[0]) ? row[0] : "" }; - mysql_free_result(r); - ok(now == orig_plugin, "mysql-default_authentication_plugin restored exp:'%s', got:'%s'", + string now {}; + read_var(admin, "mysql-default_authentication_plugin", now); + ok(!have_plugin || now == orig_plugin, + "mysql-default_authentication_plugin restored exp:'%s', got:'%s'", orig_plugin.c_str(), now.c_str()); } { - MYSQL_QUERY_T(admin, "SELECT variable_value FROM global_variables WHERE variable_name='mysql-monitor_password'"); - MYSQL_RES* r = mysql_store_result(admin); - MYSQL_ROW row = mysql_fetch_row(r); - const string now { (row && row[0]) ? row[0] : "" }; - mysql_free_result(r); - ok(now == orig_mon_pass, "mysql-monitor_password restored exp:'%s', got:'%s'", + string now {}; + read_var(admin, "mysql-monitor_username", now); + ok(!have_mon_user || now == orig_mon_user, + "mysql-monitor_username restored exp:'%s', got:'%s'", + orig_mon_user.c_str(), now.c_str()); + } + { + string now {}; + read_var(admin, "mysql-monitor_password", now); + ok(!have_mon_pass || now == orig_mon_pass, + "mysql-monitor_password restored exp:'%s', got:'%s'", orig_mon_pass.c_str(), now.c_str()); }