From 2b4ff829991c0bf08b65e7d33fa1aac80ff5742f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Sat, 28 Jan 2017 12:58:24 +0200 Subject: [PATCH] Use SQLite3 based authentication The user data is now stored inside a SQLite3 database. By storing the data inside a database, we remove the restriction that the previous hashtable based implementation had. --- include/maxscale/sqlite3.h | 3 + .../GSSAPI/GSSAPIAuth/gssapi_auth.c | 4 +- .../authenticator/MySQLAuth/CMakeLists.txt | 2 +- .../modules/authenticator/MySQLAuth/dbusers.c | 308 ++++++++++++++++++ .../authenticator/MySQLAuth/mysql_auth.c | 79 +++-- .../authenticator/MySQLAuth/mysql_auth.h | 9 +- 6 files changed, 378 insertions(+), 27 deletions(-) diff --git a/include/maxscale/sqlite3.h b/include/maxscale/sqlite3.h index 108333f5d..f17aa9740 100644 --- a/include/maxscale/sqlite3.h +++ b/include/maxscale/sqlite3.h @@ -25,3 +25,6 @@ #if SQLITE_VERSION_NUMBER < 3007014 #define sqlite3_close_v2 sqlite3_close #endif + +/** Default timeout is one minute */ +#define MXS_SQLITE_BUSY_TIMEOUT 60000 diff --git a/server/modules/authenticator/GSSAPI/GSSAPIAuth/gssapi_auth.c b/server/modules/authenticator/GSSAPI/GSSAPIAuth/gssapi_auth.c index e19fc8d09..52bb5b4d7 100644 --- a/server/modules/authenticator/GSSAPI/GSSAPIAuth/gssapi_auth.c +++ b/server/modules/authenticator/GSSAPI/GSSAPIAuth/gssapi_auth.c @@ -20,11 +20,9 @@ #include #include #include +#include #include "../gssapi_auth.h" -/** Default timeout is one minute */ -#define MXS_SQLITE_BUSY_TIMEOUT 60000 - /** * MySQL queries for retrieving the list of users */ diff --git a/server/modules/authenticator/MySQLAuth/CMakeLists.txt b/server/modules/authenticator/MySQLAuth/CMakeLists.txt index 9bbcd952a..4f26199c6 100644 --- a/server/modules/authenticator/MySQLAuth/CMakeLists.txt +++ b/server/modules/authenticator/MySQLAuth/CMakeLists.txt @@ -1,4 +1,4 @@ add_library(MySQLAuth SHARED mysql_auth.c dbusers.c) -target_link_libraries(MySQLAuth maxscale-common MySQLCommon) +target_link_libraries(MySQLAuth maxscale-common MySQLCommon sqlite3) set_target_properties(MySQLAuth PROPERTIES VERSION "1.0.0") install_module(MySQLAuth core) diff --git a/server/modules/authenticator/MySQLAuth/dbusers.c b/server/modules/authenticator/MySQLAuth/dbusers.c index 1a2843e8b..92b92dade 100644 --- a/server/modules/authenticator/MySQLAuth/dbusers.c +++ b/server/modules/authenticator/MySQLAuth/dbusers.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -121,6 +122,15 @@ MaxScale authentication will proceed without including database permissions. \ See earlier error messages for user '%s' for more information." +#define NEW_LOAD_DBUSERS_QUERY "SELECT u.user, u.host, d.db, u.select_priv, u.%s \ + FROM mysql.user AS u LEFT JOIN mysql.db AS d \ + ON (u.user = d.user AND u.host = d.host) %s \ + UNION \ + SELECT u.user, u.host, t.db, u.select_priv, u.%s \ + FROM mysql.user AS u LEFT JOIN mysql.tables_priv AS t \ + ON (u.user = t.user AND u.host = t.host) %s\ + ORDER BY user" + static int add_databases(SERV_LISTENER *listener, MYSQL *con); static int add_wildcard_users(USERS *users, char* name, char* host, char* password, char* anydb, char* db, HASHTABLE* hash); @@ -215,6 +225,22 @@ static char* get_usercount_query(const char* server_version, bool include_root, return buffer; } +static char* get_new_users_query(const char *server_version, bool include_root) +{ + const char* password = strstr(server_version, "5.7.") ? MYSQL57_PASSWORD : MYSQL_PASSWORD; + const char *with_root = include_root ? "user.user NOT IN ('root')" : ""; + + size_t n_bytes = snprintf(NULL, 0, NEW_LOAD_DBUSERS_QUERY, password, with_root, password, with_root); + char *rval = MXS_MALLOC(n_bytes + 1); + + if (rval) + { + snprintf(rval, n_bytes + 1, NEW_LOAD_DBUSERS_QUERY, password, with_root, password, with_root); + } + + return rval; +} + /** * Check if the IP address of the user matches the one in the grant. This assumes * that the grant has one or more single-character wildcards in it. @@ -1172,6 +1198,207 @@ cleanup: return total_users; } +static bool check_password(const char *output, + uint8_t *token, size_t token_len, + uint8_t *scramble, size_t scramble_len) +{ + uint8_t stored_token[SHA_DIGEST_LENGTH] = {}; + size_t stored_token_len = sizeof(stored_token); + + if (*output) + { + /** Convert the hexadecimal string to binary */ + gw_hex2bin(stored_token, output, strlen(output)); + } + + /** + * The client authentication token is made up of: + * + * XOR( SHA1(real_password), SHA1( CONCAT( scramble, ) ) ) + * + * Since we know the scramble and the value stored in mysql.user.password, + * we can extract the SHA1 of the real password by doing a XOR of the client + * authentication token with the SHA1 of the scramble concatenated with the + * value of mysql.user.password. + * + * Once we have the SHA1 of the original password, we can create the SHA1 + * of this hash and compare the value with the one stored in the backend + * database. If the values match, the user has sent the right password. + */ + + /** First, calculate the SHA1 of the scramble and the hash stored in the database */ + uint8_t step1[SHA_DIGEST_LENGTH]; + gw_sha1_2_str(scramble, scramble_len, stored_token, stored_token_len, step1); + + /** Next, extract the SHA1 of the real password by XOR'ing it with + * the output of the previous calculation */ + uint8_t step2[SHA_DIGEST_LENGTH]; + gw_str_xor(step2, token, step1, token_len); + + /** Finally, calculate the SHA1 of the hashed real password */ + uint8_t final_step[SHA_DIGEST_LENGTH]; + gw_sha1_str(step2, SHA_DIGEST_LENGTH, final_step); + + /** If the two values match, the client has sent the correct password */ + return memcmp(final_step, stored_token, stored_token_len) == 0; +} + +/** Used to detect empty result sets */ +struct user_query_result +{ + bool ok; + char output[SHA_DIGEST_LENGTH * 2 + 1]; +}; + +/** @brief Callback for sqlite3_exec() */ +static int auth_cb(void *data, int columns, char** rows, char** row_names) +{ + struct user_query_result *res = (struct user_query_result*)data; + strcpy(res->output, rows[0] ? rows[0] : ""); + res->ok = true; + return 0; +} + +/** + * @brief Verify the user has access to the database + * + * @param auth Authenticator session + * @param dcb Client DCB + * @param session MySQL session + * @param pw Client password + * + * @return True if the user has access to the database + */ +bool validate_mysql_user(sqlite3 *handle, DCB *dcb, MYSQL_session *session) +{ + size_t len = sizeof(mysqlauth_validation_query) + strlen(session->user) * 2 + + strlen(session->db) * 2 + MYSQL_HOST_MAXLEN + session->auth_token_len * 4 + 1; + char sql[len + 1]; + bool rval = false; + char *err; + + /** + * Try authentication twice; first time with the current users, second + * time with fresh users + */ + for (int i = 0; i < 2 && !rval; i++) + { + sprintf(sql, mysqlauth_validation_query, session->user, dcb->remote, + session->db, session->db); + + struct user_query_result res = {}; + + if (sqlite3_exec(handle, sql, auth_cb, &res, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to execute auth query: %s", err); + sqlite3_free(err); + rval = false; + } + + if (!res.ok) + { + /** Try authentication with the hostname */ + char client_hostname[MYSQL_HOST_MAXLEN]; + wildcard_domain_match(dcb->remote, client_hostname); + sprintf(sql, mysqlauth_validation_query, session->user, client_hostname, + session->db, session->db); + + if (sqlite3_exec(handle, sql, auth_cb, &res, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to execute auth query: %s", err); + sqlite3_free(err); + rval = false; + } + } + + if (res.ok) + { + /** Found a matching row */ + MySQLProtocol *proto = (MySQLProtocol*)dcb->protocol; + rval = check_password(res.output, session->auth_token, session->auth_token_len, + proto->scramble, sizeof(proto->scramble)); + } + + if (!rval && i == 0) + { + service_refresh_users(dcb->service); + } + } + + return rval; +} + +/** + * @brief Delete all users + * + * @param handle SQLite handle + */ +static void delete_mysql_users(sqlite3 *handle) +{ + char *err; + + if (sqlite3_exec(handle, delete_query, NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to delete old users: %s", err); + sqlite3_free(err); + } +} + +/** + * @brief Add new MySQL user to the internal user database + * + * @param handle Database handle + * @param user Username + * @param host Host + * @param db Database + * @param anydb Global access to databases + */ +static void add_mysql_user(sqlite3 *handle, const char *user, const char *host, + const char *db, bool anydb, const char *pw) +{ + size_t dblen = db && *db ? strlen(db) + 2 : sizeof(null_token); /** +2 for single quotes */ + char dbstr[dblen + 1]; + + if (db && *db) + { + sprintf(dbstr, "'%s'", db); + } + else + { + strcpy(dbstr, null_token); + } + + size_t pwlen = pw && *pw ? strlen(pw) + 2 : sizeof(null_token); /** +2 for single quotes */ + char pwstr[pwlen + 1]; + + if (pw && *pw) + { + if (*pw == '*') + { + pw++; + } + sprintf(pwstr, "'%s'", pw); + } + else + { + strcpy(pwstr, null_token); + } + + size_t len = sizeof(insert_sql_pattern) + strlen(user) + strlen(host) + dblen + pwlen + 1; + + char insert_sql[len + 1]; + sprintf(insert_sql, insert_sql_pattern, user, host, dbstr, anydb ? "1" : "0", pwstr); + + char *err; + if (sqlite3_exec(handle, insert_sql, NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to insert user: %s", err); + sqlite3_free(err); + } + + MXS_INFO("Added user: %s", insert_sql); +} + /** * Load the user/passwd form mysql.user table into the service users' hashtable * environment. @@ -1618,6 +1845,37 @@ get_users(SERV_LISTENER *listener, USERS *users) MXS_FREE(users_data); mysql_free_result(result); + + /** Testing new users query */ + char *query = get_new_users_query(server->server->server_string, service->enable_root); + + if (query) + { + if (mysql_query(con, query) == 0) + { + MYSQL_AUTH *instance = (MYSQL_AUTH*)listener->auth_instance; + delete_mysql_users(instance->handle); + + if ((result = mysql_store_result(con))) + { + while ((row = mysql_fetch_row(result))) + { + add_mysql_user(instance->handle, row[0], row[1], row[2], + row[3] && strcmp(row[3], "Y") == 0, + row[4]); + } + + mysql_free_result(result); + } + } + else + { + MXS_ERROR("Failed to load users: %s", mysql_error(con)); + } + + MXS_FREE(query); + } + mysql_close(con); return total_users; @@ -2753,3 +3011,53 @@ static void merge_netmask(char *host) "Merge incomplete: %s", host); } } + +/** + * @brief Check if an ip matches a wildcard hostname. + * + * One of the parameters should be an IP-address without wildcards, the other a + * hostname with wildcards. The hostname corresponding to the ip-address will be + * looked up and compared to the hostname with wildcard(s). Any error in the + * parameters or looking up the hostname will result in a false match. + * + * @param ip-address or a hostname with wildcard(s) + * @param ip-address or a hostname with wildcard(s) + * @return True if the host represented by the IP matches the wildcard string + */ +static bool wildcard_domain_match(const char *ip_address, char *client_hostname) +{ + /* Looks like the parameters are valid. First, convert the client IP string + * to binary form. This is somewhat silly, since just a while ago we had the + * binary address but had to zero it. dbusers.c should be refactored to fix this. + */ + struct sockaddr_in bin_address; + bin_address.sin_family = AF_INET; + if (inet_pton(bin_address.sin_family, ip_address, &(bin_address.sin_addr)) != 1) + { + MXS_ERROR("Could not convert to binary ip-address: '%s'.", ip_address); + return false; + } + + /* Try to lookup the domain name of the given IP-address. This is a slow + * i/o-operation, which will stall the entire thread. TODO: cache results + * if this feature is used often. + */ + MXS_DEBUG("Resolving '%s'", ip_address); + int lookup_result = getnameinfo((struct sockaddr*)&bin_address, + sizeof(struct sockaddr_in), + client_hostname, sizeof(client_hostname), + NULL, 0, // No need for the port + NI_NAMEREQD); // Text address only + + if (lookup_result != 0) + { + MXS_ERROR("Client hostname lookup failed, getnameinfo() returned: '%s'.", + gai_strerror(lookup_result)); + } + else + { + MXS_DEBUG("IP-lookup success, hostname is: '%s'", client_hostname); + } + + return false; +} diff --git a/server/modules/authenticator/MySQLAuth/mysql_auth.c b/server/modules/authenticator/MySQLAuth/mysql_auth.c index 5439c0e4b..4f08008c6 100644 --- a/server/modules/authenticator/MySQLAuth/mysql_auth.c +++ b/server/modules/authenticator/MySQLAuth/mysql_auth.c @@ -41,6 +41,8 @@ static bool mysql_auth_is_client_ssl_capable(DCB *dcb); static int mysql_auth_authenticate(DCB *dcb); static void mysql_auth_free_client_data(DCB *dcb); static int mysql_auth_load_users(SERV_LISTENER *port); +static void *mysql_auth_create(void *instance); +static void mysql_auth_destroy(void *data); static int combined_auth_check( DCB *dcb, @@ -72,12 +74,12 @@ MXS_MODULE* MXS_CREATE_MODULE() static MXS_AUTHENTICATOR MyObject = { mysql_auth_init, /* Initialize the authenticator */ - NULL, /* No create entry point */ + mysql_auth_create, /* Create entry point */ mysql_auth_set_protocol_data, /* Extract data into structure */ mysql_auth_is_client_ssl_capable, /* Check if client supports SSL */ mysql_auth_authenticate, /* Authenticate user credentials */ mysql_auth_free_client_data, /* Free the client data held in DCB */ - NULL, /* No destroy entry point */ + mysql_auth_destroy, /* Destroy entry point */ mysql_auth_load_users, /* Load users from backend databases */ mysql_auth_reauthenticate /* Handle COM_CHANGE_USER */ }; @@ -117,6 +119,24 @@ static void* mysql_auth_init(char **options) instance->inject_service_user = true; instance->skip_auth = false; + if (sqlite3_open_v2(MYSQLAUTH_DATABASE_NAME, &instance->handle, db_flags, NULL) != SQLITE_OK) + { + MXS_ERROR("Failed to open SQLite3 handle."); + MXS_FREE(instance); + return NULL; + } + + char *err; + + if (sqlite3_exec(instance->handle, create_sql, NULL, NULL, &err) != SQLITE_OK) + { + MXS_ERROR("Failed to create database: %s", err); + sqlite3_free(err); + sqlite3_close_v2(instance->handle); + MXS_FREE(instance); + return NULL; + } + for (int i = 0; options[i]; i++) { char *value = strchr(options[i], '='); @@ -165,6 +185,37 @@ static void* mysql_auth_init(char **options) return instance; } +static void* mysql_auth_create(void *instance) +{ + mysql_auth_t *rval = MXS_MALLOC(sizeof(*rval)); + + if (rval) + { + if (sqlite3_open_v2(MYSQLAUTH_DATABASE_NAME, &rval->handle, db_flags, NULL) == SQLITE_OK) + { + sqlite3_busy_timeout(rval->handle, MXS_SQLITE_BUSY_TIMEOUT); + } + else + { + MXS_ERROR("Failed to open SQLite3 handle."); + MXS_FREE(rval); + rval = NULL; + } + } + + return rval; +} + +static void mysql_auth_destroy(void *data) +{ + mysql_auth_t *auth = (mysql_auth_t*)data; + if (auth) + { + sqlite3_close_v2(auth->handle); + MXS_FREE(auth); + } +} + /** * @brief Authenticates a MySQL user who is a client to MaxScale. * @@ -183,7 +234,7 @@ mysql_auth_authenticate(DCB *dcb) { MySQLProtocol *protocol = DCB_PROTOCOL(dcb, MySQLProtocol); MYSQL_session *client_data = (MYSQL_session *)dcb->data; - int auth_ret; + int auth_ret = MXS_AUTH_FAILED; /** * We record the SSL status before and after the authentication. This allows @@ -199,24 +250,16 @@ mysql_auth_authenticate(DCB *dcb) { auth_ret = (SSL_ERROR_CLIENT_NOT_SSL == ssl_ret) ? MXS_AUTH_FAILED_SSL : MXS_AUTH_FAILED; } - else if (!health_after) { auth_ret = MXS_AUTH_SSL_INCOMPLETE; } - else if (!health_before && health_after) { auth_ret = MXS_AUTH_SSL_INCOMPLETE; poll_add_epollin_event_to_dcb(dcb, NULL); } - - else if (0 == strlen(client_data->user)) - { - auth_ret = MXS_AUTH_FAILED; - } - - else + else if (*client_data->user) { MXS_DEBUG("Receiving connection from '%s' to database '%s'.", client_data->user, client_data->db); @@ -226,17 +269,15 @@ mysql_auth_authenticate(DCB *dcb) MYSQL_AUTH *instance = (MYSQL_AUTH*)dcb->listener->auth_instance; - /* On failed authentication try to load user table from backend database */ - /* Success for service_refresh_users returns 0 */ - if (MXS_AUTH_SUCCEEDED != auth_ret && !instance->skip_auth && - 0 == service_refresh_users(dcb->service)) + bool is_ok = validate_mysql_user(instance->handle, dcb, client_data); + + if (!is_ok && !instance->skip_auth && service_refresh_users(dcb->service) == 0) { - auth_ret = combined_auth_check(dcb, client_data->auth_token, client_data->auth_token_len, protocol, - client_data->user, client_data->client_sha1, client_data->db); + is_ok = validate_mysql_user(instance->handle, dcb, client_data); } /* on successful authentication, set user into dcb field */ - if (MXS_AUTH_SUCCEEDED == auth_ret || instance->skip_auth) + if (is_ok || instance->skip_auth) { auth_ret = MXS_AUTH_SUCCEEDED; dcb->user = MXS_STRDUP_A(client_data->user); diff --git a/server/modules/authenticator/MySQLAuth/mysql_auth.h b/server/modules/authenticator/MySQLAuth/mysql_auth.h index 42e27f7f1..d9a037a5d 100644 --- a/server/modules/authenticator/MySQLAuth/mysql_auth.h +++ b/server/modules/authenticator/MySQLAuth/mysql_auth.h @@ -41,7 +41,7 @@ MXS_BEGIN_DECLS static const char DBUSERS_DIR[] = "cache"; static const char DBUSERS_FILE[] = "dbusers"; -#define MYSQLAUTH_DATABASE_NAME "file:mysqlauth.db" +#define MYSQLAUTH_DATABASE_NAME "file:mysqlauth.db?mode=memory&cache=shared" /** The table name where we store the users */ #define MYSQLAUTH_TABLE_NAME "mysqlauth_users" @@ -52,10 +52,10 @@ static const char create_sql[] = "(user varchar(255), host varchar(255), db varchar(255), anydb boolean, password text)"; /** The query that is executed when a user is authenticated */ -static const char mysqlauth_auth_query[] = - "SELECT * FROM " MYSQLAUTH_TABLE_NAME +static const char mysqlauth_validation_query[] = + "SELECT password FROM " MYSQLAUTH_TABLE_NAME " WHERE user = '%s' AND '%s' LIKE host AND (anydb = '1' OR '%s' = '' OR '%s' LIKE db)" - " AND ('%s' = '%s') LIMIT 1"; + " LIMIT 1"; /** Delete query used to clean up the database before loading new users */ static const char delete_query[] = "DELETE FROM " MYSQLAUTH_TABLE_NAME; @@ -121,5 +121,6 @@ int gw_find_mysql_user_password_sha1( const char *username, uint8_t *gateway_password, DCB *dcb); +bool validate_mysql_user(sqlite3 *handle, DCB *dcb, MYSQL_session *session); MXS_END_DECLS