2018-05-16 09:31:13 +03:00

719 lines
22 KiB
C++

/*
* Copyright (c) 2016 MariaDB Corporation Ab
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file and at www.mariadb.com/bsl11.
*
* Change Date: 2020-01-01
*
* On the date above, in accordance with the Business Source License, use
* of this software will be governed by version 2 or later of the General
* Public License.
*/
/**
* @file mysql_auth.c
*
* MySQL Authentication module for handling the checking of clients credentials
* in the MySQL protocol.
*
* @verbatim
* Revision History
* Date Who Description
* 02/02/2016 Martin Brampton Initial version
*
* @endverbatim
*/
#include "mysql_auth.h"
#include <maxscale/protocol/mysql.h>
#include <maxscale/authenticator.h>
#include <maxscale/alloc.h>
#include <maxscale/poll.h>
#include <maxscale/paths.h>
#include <maxscale/secrets.h>
#include <maxscale/utils.h>
#include <maxscale/routingworker.h>
static void* mysql_auth_init(char **options);
static bool mysql_auth_set_protocol_data(DCB *dcb, GWBUF *buf);
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,
uint8_t *auth_token,
size_t auth_token_len,
MySQLProtocol *protocol,
char *username,
uint8_t *stage1_hash,
char *database
);
static bool mysql_auth_set_client_data(
MYSQL_session *client_data,
MySQLProtocol *protocol,
GWBUF *buffer);
void mysql_auth_diagnostic(DCB *dcb, SERV_LISTENER *port);
json_t* mysql_auth_diagnostic_json(const SERV_LISTENER *port);
int mysql_auth_reauthenticate(DCB *dcb, const char *user,
uint8_t *token, size_t token_len,
uint8_t *scramble, size_t scramble_len,
uint8_t *output_token, size_t output_token_len);
extern "C"
{
/**
* The module entry point routine. It is this routine that
* must populate the structure that is referred to as the
* "module object", this is a structure with the set of
* external entry points for this module.
*
* @return The module object
*/
MXS_MODULE* MXS_CREATE_MODULE()
{
static MXS_AUTHENTICATOR MyObject =
{
mysql_auth_init, /* Initialize the authenticator */
NULL, /* 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, /* Destroy entry point */
mysql_auth_load_users, /* Load users from backend databases */
mysql_auth_diagnostic,
mysql_auth_diagnostic_json,
mysql_auth_reauthenticate /* Handle COM_CHANGE_USER */
};
static MXS_MODULE info =
{
MXS_MODULE_API_AUTHENTICATOR,
MXS_MODULE_GA,
MXS_AUTHENTICATOR_VERSION,
"The MySQL client to MaxScale authenticator implementation",
"V1.1.0",
ACAP_TYPE_ASYNC,
&MyObject,
NULL, /* Process init. */
NULL, /* Process finish. */
NULL, /* Thread init. */
NULL, /* Thread finish. */
{ { MXS_END_MODULE_PARAMS} }
};
return &info;
}
}
static bool open_instance_database(const char *path, sqlite3 **handle)
{
int rc = sqlite3_open_v2(path, handle, db_flags, NULL);
if (rc != SQLITE_OK)
{
MXS_ERROR("Failed to open SQLite3 handle: %d", rc);
return false;
}
char *err;
if (sqlite3_exec(*handle, users_create_sql, NULL, NULL, &err) != SQLITE_OK ||
sqlite3_exec(*handle, databases_create_sql, NULL, NULL, &err) != SQLITE_OK ||
sqlite3_exec(*handle, pragma_sql, NULL, NULL, &err) != SQLITE_OK)
{
MXS_ERROR("Failed to create database: %s", err);
sqlite3_free(err);
sqlite3_close_v2(*handle);
return false;
}
return true;
}
sqlite3* get_handle(MYSQL_AUTH* instance)
{
int i = mxs_rworker_get_current_id();
ss_dassert(i >= 0);
if (instance->handles[i] == NULL)
{
ss_debug(bool rval = )open_instance_database(":memory:", &instance->handles[i]);
ss_dassert(rval);
}
return instance->handles[i];
}
/**
* @brief Check if service permissions should be checked
*
* @param instance Authenticator instance
*
* @return True if permissions should be checked
*/
static bool should_check_permissions(MYSQL_AUTH* instance)
{
// Only check permissions when the users are loaded for the first time.
return instance->check_permissions;
}
/**
* @brief Initialize the authenticator instance
*
* @param options Authenticator options
* @return New MYSQL_AUTH instance or NULL on error
*/
static void* mysql_auth_init(char **options)
{
MYSQL_AUTH *instance = static_cast<MYSQL_AUTH*>(MXS_MALLOC(sizeof(*instance)));
if (instance &&
(instance->handles = static_cast<sqlite3**>(MXS_CALLOC(config_threadcount(), sizeof(sqlite3*)))))
{
bool error = false;
instance->cache_dir = NULL;
instance->inject_service_user = true;
instance->skip_auth = false;
instance->check_permissions = true;
instance->lower_case_table_names = false;
for (int i = 0; options[i]; i++)
{
char *value = strchr(options[i], '=');
if (value)
{
*value++ = '\0';
if (strcmp(options[i], "cache_dir") == 0)
{
if ((instance->cache_dir = MXS_STRDUP(value)) == NULL ||
!clean_up_pathname(instance->cache_dir))
{
error = true;
}
}
else if (strcmp(options[i], "inject_service_user") == 0)
{
instance->inject_service_user = config_truth_value(value);
}
else if (strcmp(options[i], "skip_authentication") == 0)
{
instance->skip_auth = config_truth_value(value);
}
else if (strcmp(options[i], "lower_case_table_names") == 0)
{
instance->lower_case_table_names = config_truth_value(value);
}
else
{
MXS_ERROR("Unknown authenticator option: %s", options[i]);
error = true;
}
}
else
{
MXS_ERROR("Unknown authenticator option: %s", options[i]);
error = true;
}
}
if (error)
{
MXS_FREE(instance->cache_dir);
MXS_FREE(instance->handles);
MXS_FREE(instance);
instance = NULL;
}
}
else if (instance)
{
MXS_FREE(instance);
instance = NULL;
}
return instance;
}
static bool is_localhost_address(struct sockaddr_storage *addr)
{
bool rval = false;
if (addr->ss_family == AF_INET)
{
struct sockaddr_in *ip = (struct sockaddr_in*)addr;
if (ip->sin_addr.s_addr == INADDR_LOOPBACK)
{
rval = true;
}
}
else if (addr->ss_family == AF_INET6)
{
struct sockaddr_in6 *ip = (struct sockaddr_in6*)addr;
if (memcmp(&ip->sin6_addr, &in6addr_loopback, sizeof(ip->sin6_addr)) == 0)
{
rval = true;
}
}
return rval;
}
/**
* @brief Authenticates a MySQL user who is a client to MaxScale.
*
* First call the SSL authentication function. Call other functions to validate
* the user, reloading the user data if the first attempt fails.
*
* @param dcb Request handler DCB connected to the client
* @return Authentication status
* @note Authentication status codes are defined in maxscale/protocol/mysql.h
*/
static int
mysql_auth_authenticate(DCB *dcb)
{
int auth_ret = ssl_authenticate_check_status(dcb);
MYSQL_session *client_data = (MYSQL_session *)dcb->data;
if (auth_ret == MXS_AUTH_SSL_COMPLETE && *client_data->user)
{
MXS_DEBUG("Receiving connection from '%s' to database '%s'.",
client_data->user, client_data->db);
MYSQL_AUTH *instance = (MYSQL_AUTH*)dcb->listener->auth_instance;
MySQLProtocol *protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
auth_ret = validate_mysql_user(instance, dcb, client_data,
protocol->scramble, sizeof(protocol->scramble));
if (auth_ret != MXS_AUTH_SUCCEEDED &&
service_refresh_users(dcb->service) == 0)
{
auth_ret = validate_mysql_user(instance, dcb, client_data,
protocol->scramble, sizeof(protocol->scramble));
}
/* on successful authentication, set user into dcb field */
if (auth_ret == MXS_AUTH_SUCCEEDED)
{
auth_ret = MXS_AUTH_SUCCEEDED;
dcb->user = MXS_STRDUP_A(client_data->user);
/** Send an OK packet to the client */
}
else if (dcb->service->log_auth_warnings)
{
if (dcb->path)
{
MXS_WARNING("%s: login attempt for user '%s'@[%s]:%s, authentication failed.",
dcb->service->name, client_data->user, dcb->remote, dcb->path);
}
else
{
MXS_WARNING("%s: login attempt for user '%s'@[%s]:%d, authentication failed.",
dcb->service->name, client_data->user, dcb->remote, dcb_get_port(dcb));
}
if (is_localhost_address(&dcb->ip) &&
!dcb->service->localhost_match_wildcard_host)
{
MXS_NOTICE("If you have a wildcard grant that covers this address, "
"try adding 'localhost_match_wildcard_host=true' for "
"service '%s'. ", dcb->service->name);
}
}
/* let's free the auth_token now */
if (client_data->auth_token)
{
MXS_FREE(client_data->auth_token);
client_data->auth_token = NULL;
}
}
return auth_ret;
}
/**
* @brief Transfer data from the authentication request to the DCB.
*
* The request handler DCB has a field called data that contains protocol
* specific information. This function examines a buffer containing MySQL
* authentication data and puts it into a structure that is referred to
* by the DCB. If the information in the buffer is invalid, then a failure
* code is returned. A call to mysql_auth_set_client_data does the
* detailed work.
*
* @param dcb Request handler DCB connected to the client
* @param buffer Pointer to pointer to buffer containing data from client
* @return True on success, false on error
*/
static bool
mysql_auth_set_protocol_data(DCB *dcb, GWBUF *buf)
{
MySQLProtocol *protocol = NULL;
MYSQL_session *client_data = NULL;
int client_auth_packet_size = 0;
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
CHK_PROTOCOL(protocol);
client_data = (MYSQL_session *)dcb->data;
client_auth_packet_size = gwbuf_length(buf);
/* For clients supporting CLIENT_PROTOCOL_41
* the Handshake Response Packet is:
*
* 4 bytes mysql protocol heade
* 4 bytes capability flags
* 4 max-packet size
* 1 byte character set
* string[23] reserved (all [0])
* ...
* ...
* Note that the fixed elements add up to 36
*/
/* Detect now if there are enough bytes to continue */
if (client_auth_packet_size < (4 + 4 + 4 + 1 + 23))
{
/* Packet is not big enough */
return false;
}
return mysql_auth_set_client_data(client_data, protocol, buf);
}
/**
* @brief Transfer detailed data from the authentication request to the DCB.
*
* The caller has created the data structure pointed to by the DCB, and this
* function fills in the details. If problems are found with the data, the
* return code indicates failure.
*
* @param client_data The data structure for the DCB
* @param protocol The protocol structure for this connection
* @param client_auth_packet The data from the buffer received from client
* @param client_auth_packet size An integer giving the size of the data
* @return True on success, false on error
*/
static bool
mysql_auth_set_client_data(
MYSQL_session *client_data,
MySQLProtocol *protocol,
GWBUF *buffer)
{
int client_auth_packet_size = gwbuf_length(buffer);
uint8_t client_auth_packet[client_auth_packet_size];
gwbuf_copy_data(buffer, 0, client_auth_packet_size, client_auth_packet);
int packet_length_used = 0;
/* Make authentication token length 0 and token null in case none is provided */
client_data->auth_token_len = 0;
client_data->auth_token = NULL;
if (client_auth_packet_size > MYSQL_AUTH_PACKET_BASE_SIZE)
{
/* Should have a username */
uint8_t* name = client_auth_packet + MYSQL_AUTH_PACKET_BASE_SIZE;
uint8_t* end = client_auth_packet + sizeof(client_auth_packet);
int user_length = 0;
while (name < end && *name)
{
name++;
user_length++;
}
if (name == end)
{
// The name is not null terminated
return false;
}
if (client_auth_packet_size > (MYSQL_AUTH_PACKET_BASE_SIZE + user_length + 1))
{
/* Extra 1 is for the terminating null after user name */
packet_length_used = MYSQL_AUTH_PACKET_BASE_SIZE + user_length + 1;
/* We should find an authentication token next */
/* One byte of packet is the length of authentication token */
client_data->auth_token_len = client_auth_packet[packet_length_used];
if (client_auth_packet_size >
(packet_length_used + client_data->auth_token_len))
{
client_data->auth_token = (uint8_t*)MXS_MALLOC(client_data->auth_token_len);
if (client_data->auth_token)
{
/* The extra 1 is for the token length byte, just extracted*/
memcpy(client_data->auth_token,
client_auth_packet + MYSQL_AUTH_PACKET_BASE_SIZE + user_length + 1 + 1,
client_data->auth_token_len);
}
else
{
/* Failed to allocate space for authentication token string */
return false;
}
}
else
{
/* Packet was too small to contain authentication token */
return false;
}
}
else
{
return false;
}
}
return true;
}
/**
* @brief Determine whether the client is SSL capable
*
* The authentication request from the client will indicate whether the client
* is expecting to make an SSL connection. The information has been extracted
* in the previous functions.
*
* @param dcb Request handler DCB connected to the client
* @return Boolean indicating whether client is SSL capable
*/
static bool
mysql_auth_is_client_ssl_capable(DCB *dcb)
{
MySQLProtocol *protocol;
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
return (protocol->client_capabilities & (int)GW_MYSQL_CAPABILITIES_SSL) ? true : false;
}
/**
* @brief Free the client data pointed to by the passed DCB.
*
* Currently all that is required is to free the storage pointed to by
* dcb->data. But this is intended to be implemented as part of the
* authentication API at which time this code will be moved into the
* MySQL authenticator. If the data structure were to become more complex
* the mechanism would still work and be the responsibility of the authenticator.
* The DCB should not know authenticator implementation details.
*
* @param dcb Request handler DCB connected to the client
*/
static void
mysql_auth_free_client_data(DCB *dcb)
{
MXS_FREE(dcb->data);
}
/**
* @brief Inject the service user into the cache
*
* @param port Service listener
* @return True on success, false on error
*/
static bool add_service_user(SERV_LISTENER *port)
{
char *user = NULL;
char *pw = NULL;
bool rval = false;
if (serviceGetUser(port->service, &user, &pw))
{
pw = decrypt_password(pw);
if (pw)
{
char *newpw = create_hex_sha1_sha1_passwd(pw);
if (newpw)
{
MYSQL_AUTH *inst = (MYSQL_AUTH*)port->auth_instance;
sqlite3* handle = get_handle(inst);
add_mysql_user(handle, user, "%", "", "Y", newpw);
add_mysql_user(handle, user, "localhost", "", "Y", newpw);
MXS_FREE(newpw);
rval = true;
}
MXS_FREE(pw);
}
else
{
MXS_ERROR("[%s] Failed to decrypt service user password.", port->service->name);
}
}
else
{
MXS_ERROR("[%s] Failed to retrieve service credentials.", port->service->name);
}
return rval;
}
/**
* @brief Load MySQL authentication users
*
* This function loads MySQL users from the backend database.
*
* @param port Listener definition
* @return MXS_AUTH_LOADUSERS_OK on success, MXS_AUTH_LOADUSERS_ERROR and
* MXS_AUTH_LOADUSERS_FATAL on fatal error
*/
static int mysql_auth_load_users(SERV_LISTENER *port)
{
int rc = MXS_AUTH_LOADUSERS_OK;
SERVICE *service = port->listener->service;
MYSQL_AUTH *instance = (MYSQL_AUTH*)port->auth_instance;
bool first_load = false;
if (should_check_permissions(instance))
{
if (!check_service_permissions(port->service))
{
return MXS_AUTH_LOADUSERS_FATAL;
}
// Permissions are OK, no need to check them again
instance->check_permissions = false;
first_load = true;
}
int loaded = replace_mysql_users(port, first_load);
bool injected = false;
if (loaded <= 0)
{
if (loaded < 0)
{
MXS_ERROR("[%s] Unable to load users for listener %s listening at [%s]:%d.", service->name,
port->name, port->address ? port->address : "::", port->port);
}
if (instance->inject_service_user)
{
/** Inject the service user as a 'backup' user that's available
* if loading of the users fails */
if (!add_service_user(port))
{
MXS_ERROR("[%s] Failed to inject service user.", port->service->name);
}
else
{
injected = true;
}
}
}
if (injected)
{
MXS_NOTICE("[%s] No users were loaded but 'inject_service_user' is enabled. "
"Enabling service credentials for authentication until "
"database users have been successfully loaded.", service->name);
}
else if (loaded == 0 && !first_load)
{
MXS_WARNING("[%s]: failed to load any user information. Authentication"
" will probably fail as a result.", service->name);
}
else if (loaded > 0 && first_load)
{
MXS_NOTICE("[%s] Loaded %d MySQL users for listener %s.", service->name, loaded, port->name);
}
return rc;
}
int mysql_auth_reauthenticate(DCB *dcb, const char *user,
uint8_t *token, size_t token_len,
uint8_t *scramble, size_t scramble_len,
uint8_t *output_token, size_t output_token_len)
{
MYSQL_session *client_data = (MYSQL_session *)dcb->data;
MYSQL_session temp;
int rval = 1;
memcpy(&temp, client_data, sizeof(*client_data));
strcpy(temp.user, user);
temp.auth_token = token;
temp.auth_token_len = token_len;
MYSQL_AUTH *instance = (MYSQL_AUTH*)dcb->listener->auth_instance;
int rc = validate_mysql_user(instance, dcb, &temp, scramble, scramble_len);
if (rc != MXS_AUTH_SUCCEEDED && service_refresh_users(dcb->service) == 0)
{
rc = validate_mysql_user(instance, dcb, &temp, scramble, scramble_len);
}
if (rc == MXS_AUTH_SUCCEEDED)
{
memcpy(output_token, temp.client_sha1, output_token_len);
rval = 0;
}
return rval;
}
int diag_cb(void *data, int columns, char **row, char **field_names)
{
DCB *dcb = (DCB*)data;
dcb_printf(dcb, "%s@%s ", row[0], row[1]);
return 0;
}
void mysql_auth_diagnostic(DCB *dcb, SERV_LISTENER *port)
{
MYSQL_AUTH *instance = (MYSQL_AUTH*)port->auth_instance;
sqlite3* handle = get_handle(instance);
char *err;
if (sqlite3_exec(handle, "SELECT user, host FROM " MYSQLAUTH_USERS_TABLE_NAME,
diag_cb, dcb, &err) != SQLITE_OK)
{
dcb_printf(dcb, "Could not access users: %s", err);
MXS_ERROR("Could not access users: %s", err);
sqlite3_free(err);
}
}
int diag_cb_json(void *data, int columns, char **row, char **field_names)
{
json_t* obj = json_object();
json_object_set_new(obj, "user", json_string(row[0]));
json_object_set_new(obj, "host", json_string(row[1]));
json_t* arr = (json_t*)data;
json_array_append_new(arr, obj);
return 0;
}
json_t* mysql_auth_diagnostic_json(const SERV_LISTENER *port)
{
json_t* rval = json_array();
MYSQL_AUTH *instance = (MYSQL_AUTH*)port->auth_instance;
char *err;
sqlite3* handle = get_handle(instance);
if (sqlite3_exec(handle, "SELECT user, host FROM " MYSQLAUTH_USERS_TABLE_NAME,
diag_cb_json, rval, &err) != SQLITE_OK)
{
MXS_ERROR("Failed to print users: %s", err);
sqlite3_free(err);
}
return rval;
}