diff --git a/include/MySQL_Authentication.hpp b/include/MySQL_Authentication.hpp index 3c1da76c5b..1a1d628abb 100644 --- a/include/MySQL_Authentication.hpp +++ b/include/MySQL_Authentication.hpp @@ -74,6 +74,17 @@ class MySQL_Authentication { std::unique_ptr mysql_users_resultset { nullptr }; creds_group_t creds_backends; creds_group_t creds_frontends; + /** + * @brief Scope holding 'admin-admin_credentials' and 'admin-stats_credentials'. + * @details Separate from 'creds_frontends' so an Admin credential and a + * 'mysql_users' row of the same name cannot overwrite each other. Only + * populated when PROXYSQL31 is defined (see ADMIN_CRED_SCOPE); on the stable + * tier it stays empty and admin credentials remain in 'creds_frontends'. + * Never included in 'dump_all_users()', so 'runtime_mysql_users' and the + * cluster checksum are unaffected. + */ + creds_group_t creds_admins; + creds_group_t& creds_for(enum cred_username_type usertype); bool _reset(enum cred_username_type usertype); uint64_t _get_runtime_checksum(enum cred_username_type usertype); public: diff --git a/include/PgSQL_Authentication.h b/include/PgSQL_Authentication.h index 172a1ac44d..427af6d916 100644 --- a/include/PgSQL_Authentication.h +++ b/include/PgSQL_Authentication.h @@ -69,6 +69,16 @@ class PgSQL_Authentication { std::unique_ptr pgsql_users_resultset { nullptr }; creds_group_t creds_backends; creds_group_t creds_frontends; + /** + * @brief Scope holding 'admin-admin_credentials' / 'admin-stats_credentials'. + * @details Mirrors MySQL_Authentication::creds_admins. Only populated when + * PROXYSQL31 is defined (see ADMIN_CRED_SCOPE in MySQL_Authentication.hpp); + * on the stable tier admin credentials stay in 'creds_frontends' alongside + * 'pgsql_users' and behaviour is unchanged. Never walked by + * dump_all_users(), so 'runtime_pgsql_users' and the checksum are unaffected. + */ + creds_group_t creds_admins; + creds_group_t& creds_for(enum cred_username_type usertype); bool _reset(enum cred_username_type usertype); uint64_t _get_runtime_checksum(enum cred_username_type usertype); public: diff --git a/include/proxysql_structs.h b/include/proxysql_structs.h index 2a8cf047ef..8768e80ab1 100644 --- a/include/proxysql_structs.h +++ b/include/proxysql_structs.h @@ -47,7 +47,11 @@ enum log_event_type { PROXYSQL_METADATA }; -enum cred_username_type { USERNAME_BACKEND, USERNAME_FRONTEND, USERNAME_NONE }; +// USERNAME_ADMIN is a scope for 'admin-admin_credentials' / 'admin-stats_credentials'. +// It is compiled unconditionally, but only *used* when PROXYSQL31 is defined -- +// see ADMIN_CRED_SCOPE in MySQL_Authentication.hpp. On the stable tier those +// credentials continue to live in USERNAME_FRONTEND alongside mysql_users. +enum cred_username_type { USERNAME_BACKEND, USERNAME_FRONTEND, USERNAME_NONE, USERNAME_ADMIN }; #define PROXYSQL_USE_RESULT @@ -771,6 +775,42 @@ enum proxysql_session_type { PROXYSQL_SESSION_NONE }; +/** + * @brief The credential scope holding 'admin-admin_credentials' and + * 'admin-stats_credentials'. + * + * @details Historically these shared USERNAME_FRONTEND with mysql_users / + * pgsql_users -- one flat map keyed by username -- so an Admin credential and + * a row of the same name overwrote each other. That is the only reason the + * documentation states those users cannot also appear in mysql_users. + * + * From the Innovative tier onward they get their own scope, removing the + * collision rather than policing it. This is an INCOMPATIBLE change (a + * colliding name currently resolves to one entry; afterwards the two are + * independent), so it is gated to PROXYSQL31. On the stable tier this is + * USERNAME_FRONTEND, every call site passes what it always passed, and + * behaviour is unchanged. See issue #5987. + */ +#ifdef PROXYSQL31 +#define ADMIN_CRED_SCOPE USERNAME_ADMIN +#else +#define ADMIN_CRED_SCOPE USERNAME_FRONTEND +#endif /* PROXYSQL31 */ + +/** + * @brief Credential scope to resolve a username in, for a given session type. + * @details ADMIN and STATS sessions use ADMIN_CRED_SCOPE; every other session + * type (MySQL/PgSQL frontend, SQLite server, ClickHouse) uses + * USERNAME_FRONTEND. Shared by both protocol implementations so the policy + * exists once. + */ +static inline enum cred_username_type cred_scope_for_session(enum proxysql_session_type session_type) { + if (session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) { + return ADMIN_CRED_SCOPE; + } + return USERNAME_FRONTEND; +} + #endif /* PROXYSQL_ENUMS */ diff --git a/lib/MySQL_Authentication.cpp b/lib/MySQL_Authentication.cpp index 789ef133b2..39e71b51f6 100644 --- a/lib/MySQL_Authentication.cpp +++ b/lib/MySQL_Authentication.cpp @@ -57,18 +57,22 @@ MySQL_Authentication::MySQL_Authentication() { #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_init(&creds_backends.lock, NULL); pthread_rwlock_init(&creds_frontends.lock, NULL); + pthread_rwlock_init(&creds_admins.lock, NULL); #else spinlock_rwlock_init(&creds_backends.lock); spinlock_rwlock_init(&creds_frontends.lock); + spinlock_rwlock_init(&creds_admins.lock); #endif creds_backends.cred_array = new PtrArray(); creds_frontends.cred_array = new PtrArray(); + creds_admins.cred_array = new PtrArray(); }; MySQL_Authentication::~MySQL_Authentication() { reset(); delete creds_backends.cred_array; delete creds_frontends.cred_array; + delete creds_admins.cred_array; }; void MySQL_Authentication::print_version() { @@ -76,7 +80,7 @@ void MySQL_Authentication::print_version() { }; void MySQL_Authentication::set_all_inactive(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); #else @@ -95,7 +99,7 @@ void MySQL_Authentication::set_all_inactive(enum cred_username_type usertype) { } void MySQL_Authentication::remove_inactives(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); #else @@ -117,6 +121,23 @@ void MySQL_Authentication::remove_inactives(enum cred_username_type usertype) { #endif } +/** + * @brief Select the credential scope for a username type. + * @details USERNAME_ADMIN is only ever passed when PROXYSQL31 is defined; on the + * stable tier ADMIN_CRED_SCOPE is USERNAME_FRONTEND so this never returns + * 'creds_admins' and behaviour is unchanged. + */ +creds_group_t& MySQL_Authentication::creds_for(enum cred_username_type usertype) { + switch (usertype) { + case USERNAME_BACKEND: + return creds_backends; + case USERNAME_ADMIN: + return creds_admins; + default: + return creds_frontends; + } +} + bool MySQL_Authentication::add(char * username, char * password, enum cred_username_type usertype, bool use_ssl, int default_hostgroup, char *default_schema, bool schema_locked, bool transaction_persistent, bool fast_forward, int max_connections, char* attributes, char *comment) { uint64_t hash1, hash2; SpookyHash myhash; @@ -124,7 +145,7 @@ bool MySQL_Authentication::add(char * username, char * password, enum cred_usern myhash.Update(username,strlen(username)); myhash.Final(&hash1,&hash2); - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -483,7 +504,7 @@ bool MySQL_Authentication::del(char * username, enum cred_username_type usertype myhash->Final(&hash1,&hash2); delete myhash; - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); if (set_lock) #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX @@ -526,7 +547,7 @@ bool MySQL_Authentication::set_SHA1(char * username, enum cred_username_type use myhash->Final(&hash1,&hash2); delete myhash; - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -563,7 +584,7 @@ bool MySQL_Authentication::set_clear_text_password( myhash->Final(&hash1,&hash2); delete myhash; - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -631,7 +652,7 @@ account_details_t MySQL_Authentication::lookup( myhash.Update(username,strlen(username)); myhash.Final(&hash1,&hash2); - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_rdlock(&cg.lock); @@ -688,7 +709,7 @@ account_details_t MySQL_Authentication::lookup( } bool MySQL_Authentication::_reset(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -733,6 +754,11 @@ bool MySQL_Authentication::_reset(enum cred_username_type usertype) { bool MySQL_Authentication::reset() { _reset(USERNAME_BACKEND); _reset(USERNAME_FRONTEND); + // creds_admins is a distinct scope from PROXYSQL31 onward; without this its + // accounts are handed to setAllInactive() without ever being released and the + // account_details strings leak. On the stable tier ADMIN_CRED_SCOPE is + // USERNAME_FRONTEND so the scope is empty and this is a no-op. + _reset(USERNAME_ADMIN); return true; } @@ -780,7 +806,7 @@ static uint64_t compute_accounts_hash(const umap_auth& accs_map) { } uint64_t MySQL_Authentication::_get_runtime_checksum(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); uint64_t accs_hash = compute_accounts_hash(cg.bt_map); return accs_hash; diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 9c1133b746..ed1321e5f1 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -1238,7 +1238,7 @@ bool MySQL_Protocol::process_pkt_auth_swich_response(unsigned char *pkt, unsigne password = ch_account.password; #endif /* PROXYSQLCLICKHOUSE */ } else { - account_details = GloMyAuth->lookup((char*)userinfo->username, USERNAME_FRONTEND, dup_details); + account_details = GloMyAuth->lookup((char*)userinfo->username, cred_scope_for_session(session_type), dup_details); password = account_details.password; } // FIXME: add support for default schema and fast forward , issues #255 and #256 @@ -1259,7 +1259,7 @@ bool MySQL_Protocol::process_pkt_auth_swich_response(unsigned char *pkt, unsigne if (ret) { if (account_details.sha1_pass==NULL) { // currently proxysql doesn't know any sha1_pass for that specific user, let's set it! - GloMyAuth->set_SHA1((char *)userinfo->username, USERNAME_FRONTEND,reply); + GloMyAuth->set_SHA1((char *)userinfo->username, cred_scope_for_session(session_type),reply); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); userinfo->sha1_pass=sha1_pass_hex(reply); @@ -1328,7 +1328,7 @@ bool MySQL_Protocol::verify_user_pass( ret=proxy_scramble_sha1((char *)pass,(*myds)->myconn->scramble_buff,password+1, reply); if (ret) { if (sha1_pass==NULL) { - GloMyAuth->set_SHA1((char *)user, USERNAME_FRONTEND,reply); + GloMyAuth->set_SHA1((char *)user, cred_scope_for_session(session_type),reply); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); userinfo->sha1_pass=sha1_pass_hex(reply); @@ -1346,7 +1346,7 @@ bool MySQL_Protocol::verify_user_pass( if (strcasecmp(double_hashed_password,password)==0) { ret = true; if (sha1_pass==NULL) { - GloMyAuth->set_SHA1((char *)user, USERNAME_FRONTEND,md1_buf); + GloMyAuth->set_SHA1((char *)user, cred_scope_for_session(session_type),md1_buf); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); @@ -1460,7 +1460,7 @@ bool MySQL_Protocol::process_pkt_COM_CHANGE_USER(unsigned char *pkt, unsigned in ch_account_to_my(account_details, ch_account_details); #endif /* PROXYSQLCLICKHOUSE */ } else { - account_details = GloMyAuth->lookup((char *)user, USERNAME_FRONTEND, dup_details); + account_details = GloMyAuth->lookup((char *)user, cred_scope_for_session(session_type), dup_details); } /** @@ -1695,6 +1695,29 @@ int MySQL_Protocol::PPHR_1(unsigned char *pkt, unsigned int len, bool& ret, MyPr if (len==5) { ret = false; vars1.user = (unsigned char *)(*myds)->myconn->userinfo->username; + // A 1-byte payload of 0x02 at this stage is not a disconnect: it is the + // caching_sha2_password 'request_public_key' packet. ProxySQL has no RSA + // key to serve (tracked in #5988), so the exchange cannot continue -- but + // reporting it as "client is disconnecting" sent operators looking in + // entirely the wrong place. Name the real cause. + // + // This only fixes the log line. The client still receives the generic + // error produced by the normal failure path in + // MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE; + // giving the client a specific message needs that path to carry one, and + // #5988 replaces this branch with a real RSA implementation anyway. + 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); + proxy_error( + "User '%s'@'%s' requested the caching_sha2_password RSA public key, which ProxySQL does not" + " serve. Connect using TLS instead.\n", + vars1.user, (*myds)->addr.addr + ); + (*myds)->auth_in_progress = 0; + return 1; + } proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, "Session=%p , DS=%p , user='%s' . Client is disconnecting\n", (*myds), (*myds)->sess, vars1.user); proxy_error("User '%s'@'%s' is disconnecting during switch auth\n", vars1.user, (*myds)->addr.addr); (*myds)->auth_in_progress = 0; @@ -2049,26 +2072,112 @@ void MySQL_Protocol::PPHR_5passwordTrue( } +// Defined below, next to PPHR_6auth2, the other caller. +static bool caching_sha2_fast_auth_verify( + const char* cleartext_password, + const char* scramble, + const unsigned char* client_response +); + +/** + * @brief Authenticate the 'mysql-monitor_*' credential. + * @details This is the only code path that authenticates + * 'mysql-monitor_username' / 'mysql-monitor_password'. That credential is not + * stored in 'GloMyAuth', so @ref MySQL_Protocol::PPHR_verify_password reaches + * here through its 'vars1.password == NULL' branch, for ADMIN / STATS / SQLITE + * sessions only. + * + * It used to be hardcoded to the 'mysql_native_password' scramble: a SHA1 + * 'proxy_scramble' compared over SHA_DIGEST_LENGTH (20) bytes. Under + * 'caching_sha2_password' the client's fast-auth response is a 32-byte + * SHA256-derived value, so that comparison could never succeed and the + * credential was rejected outright -- breaking Kubernetes liveness/readiness + * probes and metrics exporters connecting to the Admin interface (issue #5363). + * Note it returned false without ever sending 'perform full authentication', + * which is why TLS was not a workaround either. + * + * The password is held in cleartext (as documented for 'mysql-monitor_password'), + * so every supported plugin can be verified directly and no full-auth round trip + * is ever required here. + */ void MySQL_Protocol::PPHR_5passwordFalse_0( - // FIXME: does this work only for mysql_native_password ? bool& ret, MyProt_tmp_auth_vars& vars1, char * reply, account_details_t& attr1) { - if (strcmp((const char *)vars1.user,mysql_thread___monitor_username)==0) { - proxy_scramble(reply, (*myds)->myconn->scramble_buff, mysql_thread___monitor_password); - if (memcmp(reply, vars1.pass, SHA_DIGEST_LENGTH)==0) { - (*myds)->sess->default_hostgroup=STATS_HOSTGROUP; - (*myds)->sess->default_schema=strdup((char *)"main"); // just the pointer is passed - (*myds)->sess->schema_locked=false; - (*myds)->sess->transaction_persistent=false; - (*myds)->sess->session_fast_forward=SESSION_FORWARD_TYPE_NONE; - (*myds)->sess->user_max_connections=0; - vars1.password=l_strdup(mysql_thread___monitor_password); - ret=true; - } - } else { + if (strcmp((const char *)vars1.user,mysql_thread___monitor_username)!=0) { ret=false; + return; + } + + bool verified = false; + + switch (auth_plugin_id) { + case AUTH_MYSQL_NATIVE_PASSWORD: + proxy_scramble(reply, (*myds)->myconn->scramble_buff, mysql_thread___monitor_password); + // NOTE: do NOT gate this on 'vars1.pass_len == SHA_DIGEST_LENGTH'. + // 'pass_len' is not the amount of valid data in 'vars1.pass': PPHR_2 + // strips a trailing NUL byte from the client's response + // ("remove the extra 0 if present"), so a legitimate 20-byte native + // response whose last byte is 0x00 -- about 1 in 256 -- arrives with + // pass_len == 19 while all 20 bytes are present in the buffer. + verified = (memcmp(reply, vars1.pass, SHA_DIGEST_LENGTH) == 0); + break; + + case AUTH_MYSQL_CACHING_SHA2_PASSWORD: + if ((*myds)->switching_auth_stage == 5) { + // A full-auth round trip was driven by another path (e.g. pass-through + // auth), so 'vars1.pass' already holds the cleartext. + verified = + (vars1.pass != NULL) && + (strcmp(mysql_thread___monitor_password, (const char *)vars1.pass) == 0); + } else { + verified = caching_sha2_fast_auth_verify( + mysql_thread___monitor_password, (*myds)->myconn->scramble_buff, + vars1.pass + ); + } + break; + + case AUTH_MYSQL_CLEAR_PASSWORD: + verified = + (vars1.pass != NULL) && + (strcmp(mysql_thread___monitor_password, (const char *)vars1.pass) == 0); + break; + + default: + // A client can request an arbitrary plugin; this is not a programming + // error, so do not assert. Reject and say why. + proxy_debug(PROXY_DEBUG_MYSQL_AUTH, 5, + "Session=%p , DS=%p , user='%s' . Unsupported auth_plugin_id=%d for the monitor credential\n", + (*myds), (*myds)->sess, vars1.user, auth_plugin_id); + break; + } + + if (verified == false) { + ret=false; + return; + } + + (*myds)->sess->default_hostgroup=STATS_HOSTGROUP; + (*myds)->sess->default_schema=strdup((char *)"main"); // just the pointer is passed + (*myds)->sess->schema_locked=false; + (*myds)->sess->transaction_persistent=false; + (*myds)->sess->session_fast_forward=SESSION_FORWARD_TYPE_NONE; + (*myds)->sess->user_max_connections=0; + vars1.password=l_strdup(mysql_thread___monitor_password); + ret=true; + + // caching_sha2_password requires an explicit 'fast_auth_success' marker before + // the OK packet; mirrors what the PPHR_6auth2 call site does in + // PPHR_verify_password. Without it the client rejects the subsequent OK. + if ( + auth_plugin_id == AUTH_MYSQL_CACHING_SHA2_PASSWORD + && + (*myds)->switching_auth_stage == 0 + ) { + const unsigned char fast_auth_success = '\3'; + generate_one_byte_pkt(fast_auth_success); } } @@ -2147,6 +2256,9 @@ void MySQL_Protocol::PPHR_5passwordFalse_auth2( if (attr1.sha1_pass==NULL) { // currently proxysql doesn't know any sha1_pass for that specific user, let's set it! // TODO: CHECK these usages of 'reply' + // USERNAME_FRONTEND, not the session scope: this is the LDAP + // path, which only ever backs frontend users -- an ADMIN/STATS + // session never reaches it. GloMyAuth->set_SHA1((char *)userinfo->username, USERNAME_FRONTEND,reply); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); @@ -2168,26 +2280,68 @@ void MySQL_Protocol::PPHR_5passwordFalse_auth2( } } +/** + * @brief Verify a 'caching_sha2_password' fast-auth response. + * @details The client sends + * XOR( SHA256(pw), SHA256( SHA256(SHA256(pw)) || scramble ) ) + * which the server recomputes from a password it can derive. This is the + * cache-hit path of the plugin; it requires no extra round trip and is the + * only completion possible when ProxySQL holds the cleartext. + * + * Extracted so there is exactly one implementation of the algorithm: it is + * used both by @ref MySQL_Protocol::PPHR_6auth2 for credentials found in + * 'GloMyAuth' and by @ref MySQL_Protocol::PPHR_5passwordFalse_0 for the + * 'mysql-monitor_*' credential, which is not stored there. + * + * @param cleartext_password The password ProxySQL holds, NUL-terminated. + * @param scramble The 20-byte connection scramble. + * @param client_response The response bytes sent by the client. + * @return true when the response matches. + */ +static bool caching_sha2_fast_auth_verify( + const char* cleartext_password, + const char* scramble, + const unsigned char* client_response +) { + // Deliberately NOT length-checked against 'vars1.pass_len'. That field is not + // the amount of valid data in the response buffer: PPHR_2 strips a trailing + // NUL byte ("remove the extra 0 if present"), so a legitimate 32-byte + // caching_sha2 response ending in 0x00 -- about 1 in 256 -- reports + // pass_len == 31 while all 32 bytes are present. Gating on it rejects real + // logins intermittently; measured at 20 spurious denials across ~6520 + // connections in test_auth_methods-t. + if (cleartext_password == NULL || client_response == NULL) { + return false; + } + + unsigned char a[SHA256_DIGEST_LENGTH]; + unsigned char b[SHA256_DIGEST_LENGTH]; + unsigned char c[SHA256_DIGEST_LENGTH+20]; + unsigned char d[SHA256_DIGEST_LENGTH]; + unsigned char e[SHA256_DIGEST_LENGTH]; + SHA256((const unsigned char *)cleartext_password, strlen(cleartext_password), a); + SHA256(a, SHA256_DIGEST_LENGTH, b); + memcpy(c,b,SHA256_DIGEST_LENGTH); + memcpy(c+SHA256_DIGEST_LENGTH, scramble, 20); + SHA256(c, SHA256_DIGEST_LENGTH+20, d); + for (int i=0; isess->session_type; if (session_type == PROXYSQL_SESSION_MYSQL || session_type == PROXYSQL_SESSION_SQLITE || session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) { - unsigned char a[SHA256_DIGEST_LENGTH]; - unsigned char b[SHA256_DIGEST_LENGTH]; - unsigned char c[SHA256_DIGEST_LENGTH+20]; - unsigned char d[SHA256_DIGEST_LENGTH]; - unsigned char e[SHA256_DIGEST_LENGTH]; - SHA256((const unsigned char *)vars1.password, strlen(vars1.password), a); - SHA256(a, SHA256_DIGEST_LENGTH, b); - memcpy(c,b,SHA256_DIGEST_LENGTH); - memcpy(c+SHA256_DIGEST_LENGTH, (*myds)->myconn->scramble_buff, 20); - SHA256(c, SHA256_DIGEST_LENGTH+20, d); - for (int i=0; imyconn->scramble_buff, vars1.pass + ) + ) { ret = true; } } @@ -2205,7 +2359,7 @@ void MySQL_Protocol::PPHR_7auth1( if (ret) { if (attr1.sha1_pass==NULL) { // currently proxysql doesn't know any sha1_pass for that specific user, let's set it! - GloMyAuth->set_SHA1((char *)vars1.user, USERNAME_FRONTEND,reply); + GloMyAuth->set_SHA1((char *)vars1.user, cred_scope_for_session(session_type),reply); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); @@ -2246,7 +2400,7 @@ void MySQL_Protocol::PPHR_7auth2( ret = true; if (attr1.sha1_pass==NULL) { // currently proxysql doesn't know any sha1_pass for that specific user, let's set it! - GloMyAuth->set_SHA1((char *)vars1.user, USERNAME_FRONTEND,md1_buf); + GloMyAuth->set_SHA1((char *)vars1.user, cred_scope_for_session(session_type),md1_buf); } if (userinfo->sha1_pass) free(userinfo->sha1_pass); @@ -3063,7 +3217,7 @@ bool MySQL_Protocol::process_pkt_handshake_response(unsigned char *pkt, unsigned ch_account_to_my(account_details, ch_account); #endif /* PROXYSQLCLICKHOUSE */ } else { - account_details = GloMyAuth->lookup((char*)vars1.user, USERNAME_FRONTEND, dup_details); + account_details = GloMyAuth->lookup((char*)vars1.user, cred_scope_for_session(session_type), dup_details); } vars1.password = get_password(account_details, PASSWORD_TYPE::PRIMARY); diff --git a/lib/PgSQL_Authentication.cpp b/lib/PgSQL_Authentication.cpp index f0fbd55178..7f76b573ad 100644 --- a/lib/PgSQL_Authentication.cpp +++ b/lib/PgSQL_Authentication.cpp @@ -26,18 +26,22 @@ PgSQL_Authentication::PgSQL_Authentication() { #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_init(&creds_backends.lock, NULL); pthread_rwlock_init(&creds_frontends.lock, NULL); + pthread_rwlock_init(&creds_admins.lock, NULL); #else spinlock_rwlock_init(&creds_backends.lock); spinlock_rwlock_init(&creds_frontends.lock); + spinlock_rwlock_init(&creds_admins.lock); #endif creds_backends.cred_array = new PtrArray(); creds_frontends.cred_array = new PtrArray(); + creds_admins.cred_array = new PtrArray(); }; PgSQL_Authentication::~PgSQL_Authentication() { reset(); delete creds_backends.cred_array; delete creds_frontends.cred_array; + delete creds_admins.cred_array; }; void PgSQL_Authentication::print_version() { @@ -45,7 +49,7 @@ void PgSQL_Authentication::print_version() { }; void PgSQL_Authentication::set_all_inactive(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); #else @@ -64,7 +68,7 @@ void PgSQL_Authentication::set_all_inactive(enum cred_username_type usertype) { } void PgSQL_Authentication::remove_inactives(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); #else @@ -86,6 +90,23 @@ void PgSQL_Authentication::remove_inactives(enum cred_username_type usertype) { #endif } +/** + * @brief Select the credential scope for a username type. + * @details Mirrors MySQL_Authentication::creds_for(). USERNAME_ADMIN is only + * passed when PROXYSQL31 is defined; otherwise ADMIN_CRED_SCOPE is + * USERNAME_FRONTEND and 'creds_admins' is never reached. + */ +creds_group_t& PgSQL_Authentication::creds_for(enum cred_username_type usertype) { + switch (usertype) { + case USERNAME_BACKEND: + return creds_backends; + case USERNAME_ADMIN: + return creds_admins; + default: + return creds_frontends; + } +} + bool PgSQL_Authentication::add(char * username, char * password, enum cred_username_type usertype, bool use_ssl, int default_hostgroup, bool transaction_persistent, bool fast_forward, int max_connections, char* attributes, char *comment) { uint64_t hash1, hash2; SpookyHash myhash; @@ -93,7 +114,7 @@ bool PgSQL_Authentication::add(char * username, char * password, enum cred_usern myhash.Update(username,strlen(username)); myhash.Final(&hash1,&hash2); - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -408,7 +429,7 @@ bool PgSQL_Authentication::del(char * username, enum cred_username_type usertype myhash->Final(&hash1,&hash2); delete myhash; - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); if (set_lock) #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX @@ -448,7 +469,7 @@ bool PgSQL_Authentication::set_SHA1(char * username, enum cred_username_type use myhash->Final(&hash1,&hash2); delete myhash; - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -501,7 +522,7 @@ char * PgSQL_Authentication::lookup(char * username, enum cred_username_type use myhash.Update(username,strlen(username)); myhash.Final(&hash1,&hash2); - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_rdlock(&cg.lock); @@ -536,7 +557,7 @@ char * PgSQL_Authentication::lookup(char * username, enum cred_username_type use } bool PgSQL_Authentication::_reset(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); #ifdef PROXYSQL_AUTH_PTHREAD_MUTEX pthread_rwlock_wrlock(&cg.lock); @@ -617,7 +638,7 @@ static uint64_t compute_accounts_hash(const umap_pgauth& accs_map) { } uint64_t PgSQL_Authentication::_get_runtime_checksum(enum cred_username_type usertype) { - creds_group_t &cg=(usertype==USERNAME_BACKEND ? creds_backends : creds_frontends); + creds_group_t &cg = creds_for(usertype); uint64_t accs_hash = compute_accounts_hash(cg.bt_map); return accs_hash; diff --git a/lib/PgSQL_Protocol.cpp b/lib/PgSQL_Protocol.cpp index 08b9e93c74..0c6c50f24c 100644 --- a/lib/PgSQL_Protocol.cpp +++ b/lib/PgSQL_Protocol.cpp @@ -904,7 +904,10 @@ EXECUTION_STATE PgSQL_Protocol::process_handshake_response_packet(unsigned char* goto __exit_process_pkt_handshake_response; } - password = GloPgAuth->lookup((char*)user, USERNAME_FRONTEND, &_ret_use_ssl, &default_hostgroup, &transaction_persistent, &fast_forward, &max_connections, &sha1_pass, &attributes); + // ADMIN/STATS sessions resolve against the Admin credential scope; see + // cred_scope_for_session(). On the stable tier this is USERNAME_FRONTEND and + // behaviour is unchanged. See #5987. + password = GloPgAuth->lookup((char*)user, cred_scope_for_session((*myds)->sess->session_type), &_ret_use_ssl, &default_hostgroup, &transaction_persistent, &fast_forward, &max_connections, &sha1_pass, &attributes); if (password) { #ifdef DEBUG diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 84c7215e2d..9829bd034a 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -3934,11 +3934,14 @@ void ProxySQL_Admin::add_credentials(char *credentials, int hostgroup_id) { if constexpr (pt == SERVER_TYPE_MYSQL) { if (GloMyAuth) { // this check if required if GloMyAuth doesn't exist yet - GloMyAuth->add(user, pass, USERNAME_FRONTEND, 0, hostgroup_id, (char*)"main", 0, 0, 0, 1000, (char*)"", (char*)""); + // ADMIN_CRED_SCOPE keeps these out of the mysql_users namespace from + // the Innovative tier onward; on the stable tier it is + // USERNAME_FRONTEND and behaviour is unchanged. See #5987. + GloMyAuth->add(user, pass, ADMIN_CRED_SCOPE, 0, hostgroup_id, (char*)"main", 0, 0, 0, 1000, (char*)"", (char*)""); } } else if constexpr (pt == SERVER_TYPE_PGSQL) { if (GloPgAuth) { // this check if required if GloPgAuth doesn't exist yet - GloPgAuth->add(user, pass, USERNAME_FRONTEND, 0, hostgroup_id, 0, 0, 1000, (char*)"", (char*)""); + GloPgAuth->add(user, pass, ADMIN_CRED_SCOPE, 0, hostgroup_id, 0, 0, 1000, (char*)"", (char*)""); } } @@ -3966,12 +3969,18 @@ void ProxySQL_Admin::delete_credentials(char *credentials) { if constexpr (pt == SERVER_TYPE_MYSQL) { if (GloMyAuth) { // this check if required if GloMyAuth doesn't exist yet - GloMyAuth->del(user, USERNAME_FRONTEND); + // Deleting from ADMIN_CRED_SCOPE. On the stable tier this is + // USERNAME_FRONTEND, so changing admin-admin_credentials still + // removes a same-named mysql_users row from the runtime auth map + // until the next LOAD MYSQL USERS TO RUNTIME -- the documented + // "do not reuse the name" rule. From PROXYSQL31 the scopes are + // separate and the two cannot touch each other. See #5987. + GloMyAuth->del(user, ADMIN_CRED_SCOPE); } } else if constexpr (pt == SERVER_TYPE_PGSQL) { if (GloPgAuth) { // this check if required if GloPgAuth doesn't exist yet - GloPgAuth->del(user, USERNAME_FRONTEND); + GloPgAuth->del(user, ADMIN_CRED_SCOPE); } } free(user); diff --git a/lib/ProxySQL_HTTP_Server.cpp b/lib/ProxySQL_HTTP_Server.cpp index 546424f0fa..16d904b681 100644 --- a/lib/ProxySQL_HTTP_Server.cpp +++ b/lib/ProxySQL_HTTP_Server.cpp @@ -386,7 +386,15 @@ int ProxySQL_HTTP_Server::handler(void *cls, struct MHD_Connection *connection, MHD_destroy_response(response); return ret; } - account_details_t ad { GloMyAuth->lookup(username, USERNAME_FRONTEND, { false, false, false }) }; + // The web UI authenticates the 'stats' account, which is populated from + // 'admin-stats_credentials' (see add_credentials() in ProxySQL_Admin.cpp). + // It must therefore be looked up in the SAME scope those credentials are + // added to -- ADMIN_CRED_SCOPE -- not in USERNAME_FRONTEND. On the stable + // tier ADMIN_CRED_SCOPE *is* USERNAME_FRONTEND, so this is a no-op there; + // under PROXYSQL31 the admin/stats accounts live in USERNAME_ADMIN and a + // USERNAME_FRONTEND lookup returns no password, failing every request with + // HTTP 401. See #5987. + account_details_t ad { GloMyAuth->lookup(username, ADMIN_CRED_SCOPE, { false, false, false }) }; { if ( (ad.default_hostgroup != STATS_HOSTGROUP) diff --git a/test/repro/README.md b/test/repro/README.md new file mode 100644 index 0000000000..b9e127976e --- /dev/null +++ b/test/repro/README.md @@ -0,0 +1,69 @@ +# test/repro + +Standalone, self-verifying reproductions for specific issues. + +These are **developer-facing tools**, not part of CI. They exist so that a claim +about ProxySQL's behaviour can be re-checked in one command instead of being +re-derived from the source each time. The CI artifacts are the TAP tests; where a +reproduction here has a TAP counterpart, both are listed below. + +## Scripts + +| Script | Issue | Exits | +|---|---|---| +| `reg_test_5363_admin_monitor_caching_sha2.bash` | [#5363](https://github.com/sysown/proxysql/issues/5363) | `0` fixed · `1` reproduced · `2` environment/baseline broken | +| `reg_test_5985_admin_caching_sha2_full_auth.bash` | [#5985](https://github.com/sysown/proxysql/issues/5985) | `0` behaves as documented · `1` assertion failed · `2` cannot be tested | + +`reg_test_5363_*` distinguishes exit `1` (the reported bug reproduced, every +failure tagged `[BUG #5363]`) from exit `2` (the baseline itself failed, so the +run should not be trusted). The TAP counterpart is +`test/tap/tests/reg_test_5363_admin_monitor_caching_sha2-t.cpp` in group +`no-infra-g1`. + +`reg_test_5985_*` demonstrates that caching_sha2_password full authentication +completes on the Admin interface against a hashed credential — the behaviour that +issue #5985 ask 2 assumed was broken. + +## Running them + +```bash +make clean && PROXYSQL31=1 make debug -j"$(nproc)" # a DEBUG build is required +WORKSPACE=$(pwd) INFRA_ID=dev-$USER test/repro/