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.
This commit is contained in:
@ -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
|
||||
|
@ -20,11 +20,9 @@
|
||||
#include <maxscale/protocol/mysql.h>
|
||||
#include <maxscale/secrets.h>
|
||||
#include <maxscale/mysql_utils.h>
|
||||
#include <maxscale/sqlite3.h>
|
||||
#include "../gssapi_auth.h"
|
||||
|
||||
/** Default timeout is one minute */
|
||||
#define MXS_SQLITE_BUSY_TIMEOUT 60000
|
||||
|
||||
/**
|
||||
* MySQL queries for retrieving the list of users
|
||||
*/
|
||||
|
@ -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)
|
||||
|
@ -40,6 +40,7 @@
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <mysql.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include <maxscale/dcb.h>
|
||||
#include <maxscale/service.h>
|
||||
@ -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, <value of mysql.user.password> ) ) )
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user