
973b983 Merge branch 'release-2.0.0' into develop 255dd23 Make spinlock functions take const argument 6e23bab Fix bitmask reallocation 338c189 Rename and clean up slavelag filter 3ea8f28 Fix possible NULL pointer dereference bfe6738 MXS-830: Add module information to logged messages 1fad962 Fix strncat usage d38997a Adjust log throttling policy 0be4e4b Add hashtable_item_strcasecmp 726100e Take hashtable convenience functions into use 5e7744a Fix typo in maxadmin.md c5778c8 Merge branch 'release-2.0.0' into develop b5762af Move from tmpnam to mkstemp d6f2c71 Add convenience functions to hashtable 359058a MXS-825: Add support for --execdir 636347c Enable runtime reconfiguration of log throttling ef9fba9 Improve log throttling documentation aef917a Implement log throttling e3a5349 Remove shardrouter.c 8051e80 Remove custom qc_sqlite allocation functions fd34d60 Initial implementation of the learning firewall a8752a8 Removed "filestem option" from example 1ef2519 Removed "filestem option" from example 0815cc8 Cleanup spinlock.h ab4dc99 Clean up hashtable.h ef2c078 Add prototypes for hashtable copy and free functions fb5cfaf Add 'log_throttling' configuration entry 300d823 Add proper prototypes for hashtable hash and cmp functions 1c649aa qc_mysqlembedded: Include skygw_...h without path. d276160 Add missing RPM scripts e70e644 Fix HTTPAuth installation 1b2b389 Combine utils into the server directory 3ff9913 Add missing utils headers to devel package 407efb2 Fix minor packaging problems 99aa6ad Split MaxScale into core, experimental and devel packages 1290386 Merge branch 'develop' of ssh://github.com/mariadb-corporation/maxscale-new into develop e59f148 Make scripts POSIX sh compatible 7319266 Fixed SHOW SLAVE STATUS in bonlog router f8d760a Update Binlogrouter.md 0a904ed Update Replication-Proxy-Binlog-Router-Tutorial.md 75d4202 Update Replication-Proxy-Binlog-Router-Tutorial.md b8651fc Add missing newline in listmanager.h c7ad047 Add note about user data caches to release notes 70ccc2b Merge branch 'release-2.0.0' into develop 575d1b6 Mistake - dummy session needs list markers set. 8364508 Merge branch 'develop' into binlog_server_semisync 868b902 Update MaxScale limitations 2c8b327 Store listener caches in separate directories 6e183ec Create unique user data caches for each listeners f643685 Don't free orphaned tee filter sessions 4179afa Allow binlogrouter to be used without a listener 7ad79af Add function for freeing a listener 677a0a2 Move authentication data from services to listeners 4f12af7 Merge remote-tracking branch 'origin/MXS-677' into develop 1419b81 Semi-Sync support to binlog server: code review updtate 0ea0f01 Semi-Sync support to binlog server: added missing routine 4aad909 Semi-Sync support to binlog server b824e1e Add authenticator support to httpd.c 705a688 Change tabs to spaces d0c419e Change method of adding list fields to e.g. DCB 25504fc Document the changed routing priority of hints 41666d1 Remove use_ssl_if_enabled global option a3584e9 Make routing hints have highest priority 34a1d24 Updated document with new binlog router option 01eedc5 Updated documentation with SSL usage 8a4c0f6 Update Replication-Proxy-Binlog-Router-Tutorial.md 4e374aa Update Replication-Proxy-Binlog-Router-Tutorial.md f3f3c57 Update Replication-Proxy-Binlog-Router-Tutorial.md 617b79f Binlog Server: error messages typo fix fa8dfae Binlog Server: error messages review 1b8819c Fix freeing of schemarouter session memory 07f49e1 MXS-788: new code review fix 1fd3b09 MXS-788: show services now displays SSL info 6ca2584 MXS-788 code review fix ae6a7d0 MXS-788 code review 43d3474 Master server SSL connection 90b2377 Use correct variable in listmanager pre-allocation 9a5b238 Fix listmanager pre-allocation 9c78625 Fix a memory leak when backend authentication fails e59a966 Fix hang in list_find_free ff30223 Fix freeing of shared data in schemarouter fc8f9d3 Add missing include in luafilter ecf7f53 Add missing NULL value to filter parameter array 636d849 Update memory allocation approach f0d1d38 Add new allocation functions 97d00a0 Fix writing of uninitialized data to logs e72c9b2 Merge branch 'release-2.0.0' into develop cf2b712 Merge branch 'release-2.0.0' into develop 8917c5c Change the logic behind valid list entry checks c10deff Improve documentation about version_string f59f1f7 Merge branch 'develop' of ssh://github.com/mariadb-corporation/maxscale-new into develop c88edb3 Backend authentication failure improvement abd5bee Revert "Backend authentication failure improvement" 5bb3107 Backend authentication failure improvement b7f434a Add new allocation functions 3f022fa Fix stupid mistake 99c4317 Merge remote-tracking branch 'origin/MXS-677' into develop 3c1ded6 Added connection/authentication failure error reporting in SHOW SLAVE STATUS 0a60f7b Tidy up and deal with review points. ba103ff blr_slave.c: Update strncpy usage 467331e blr_master.c: Strncpy usage updates d2b7c0c Merge remote-tracking branch 'origin/develop-nullauth-merge' into develop 5a8c1d0 qc: Measure execution time at the right place. bccdb93 Merge branch 'NullAuthDeny' into develop 2e6511c Add 5.5.5 prefix to all version strings that lack it 314655a Improve DCB and session initialization and list handling e1c43f0 MXS-655: Make MaxScale logging logrotate(8) compatible ce36afd MXS-626: Don't log a header unless maxlog enabled dcd47a7 blr_file.c: Replace uses of strncpy 6b8f576 bls_slave.c: Replace strncpy with memcpy 68a0039 Add list preallocation, tidy up, simplify init. cb37d1b Fix copyright etc headers. 11a400d Tidy; comment; fix bad copies and mistakes. 7e36ec4 Add list manager files. c4794e3 Initial code for list manager. 1b42e25 Merge remote-tracking branch 'origin/MXS-765' into develop d50f617 Fix problems, extend tests, respond to review. dcb4a91 Filter test folder removed 0b60dbe Add a couple of comments. 83cdba0 Fix overwriting problem. ba5d353 Fix overwriting problem. 53671cb Small fixes in response to review. 173d049 blr.c: Review strncpy usage 4ff6ef2 binlog_common.c: Replace strncpy with memcpy f238e03 maxbinlogcheck.s: Replace strncpy 9807f8d harness: Replace unnecessary use of strncpy 8c7fe6a avro: Modify strncpy usage 9b8008e Small improvements. b7f784f Fix mistakes in testqueuemanager.c cc26962 Restore missing poll.c code; add testqueuemanager.c. 2e91806 Format the filter harness 22059e6 Initial implementation connection queueing. c604dc2 readwritesplit.c: Improve COM_INIT_DB handling 454d920 schemarouter.c: Replace strncpy with strcpy 8e85d66 sharding_common.c: Too long a database name handled explicitly 77f4446 Astyle schemarouter 491f7c2 maxinfo.c: Replace strncpy with memcpy 6b98105 maxinfo: Reformat with astyle c1dbf08 Handle oversize user and database names 5fa4a0f Merge branch 'develop' of ssh://github.com/mariadb-corporation/maxscale-new into develop 706963b BLR_DBUSERS_TAIL new var in blr.h d75b9af Tweak comments, remove trailing blanks. ab2400a Optimise statistics gathering by inline & simpler fns. fb59ddc Remove unnecessary strncpy/strncat usage in Binlog Server bdcd551 resultset.c: Change strncpy to memcpy c6b1c5e Reject rather than cut too long a path 6d8f112 Remove unnecessary strncpy/strncat usage 18bf5ed Remove unnecessary strncpy usage dc0e2db Make maxpasswd more userfriendly c9c8695 Fix calculation of padded_len in encryptPassword 2cfd2c6 dbusers.c: Check strncpy usage 7ab9342 Make more thorough checks in secrets_readKeys be7d593 Format cli.c debugcli.c testroute.c webserver.c 1ee5efb config.c: Check usage of strncpy 3043b12 gq_utils.c: Unnecessary use of strncpy removed 77874ac Add help to maxkeys 38392a3 Update secrets_writeKeys documentation 2d1325c Make SSL optional in MaxScale's own communication bda00da Fix avro build failures b2cb31a Add more OOM macros 41ccf17 Fix strdup usage a48f732 Fix realloc calls 20771f6 Add forgotten extern "C" block 8faf35a Add maxscale allocation functions bb47890 Add macros for OOM logging afea388 Fix silly mistakes. 6dafd22 Make deny default for null auth; move code from common to auth.
2741 lines
78 KiB
C
2741 lines
78 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/bsl.
|
|
*
|
|
* Change Date: 2019-07-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 dbusers.c - Loading MySQL users from a MySQL backend server, this needs
|
|
* libmysqlclient.so and header files
|
|
*
|
|
* @verbatim
|
|
* Revision History
|
|
*
|
|
* Date Who Description
|
|
* 24/06/2013 Massimiliano Pinto Initial implementation
|
|
* 08/08/2013 Massimiliano Pinto Fixed bug for invalid memory access in row[1]+1 when row[1] is ""
|
|
* 06/02/2014 Massimiliano Pinto Mysql user root selected based on configuration flag
|
|
* 26/02/2014 Massimiliano Pinto Addd: replace_mysql_users() routine may replace users' table
|
|
* based on a checksum
|
|
* 28/02/2014 Massimiliano Pinto Added Mysql user@host authentication
|
|
* 29/09/2014 Massimiliano Pinto Added Mysql user@host authentication with wildcard in IPv4 hosts:
|
|
* x.y.z.%, x.y.%.%, x.%.%.%
|
|
* 03/10/14 Massimiliano Pinto Added netmask to user@host authentication for wildcard in IPv4 hosts
|
|
* 13/10/14 Massimiliano Pinto Added (user@host)@db authentication
|
|
* 04/12/14 Massimiliano Pinto Added support for IPv$ wildcard hosts: a.%, a.%.% and a.b.%
|
|
* 25/05/16 Massimiliano Pinto Removed log message for duplicate entry while adding an user
|
|
*
|
|
* @endverbatim
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <mysql.h>
|
|
|
|
#include <dcb.h>
|
|
#include <service.h>
|
|
#include <users.h>
|
|
#include <dbusers.h>
|
|
#include <skygw_utils.h>
|
|
#include <log_manager.h>
|
|
#include <secrets.h>
|
|
#include <mysql_client_server_protocol.h>
|
|
#include <mysqld_error.h>
|
|
#include <regex.h>
|
|
#include <mysql_utils.h>
|
|
#include <maxscale/alloc.h>
|
|
|
|
/** Don't include the root user */
|
|
#define USERS_QUERY_NO_ROOT " AND user.user NOT IN ('root')"
|
|
|
|
/** User count without databases */
|
|
#define MYSQL_USERS_COUNT "SELECT COUNT(1) AS nusers FROM mysql.user"
|
|
|
|
/** Normal password column name */
|
|
#define MYSQL_PASSWORD "password"
|
|
|
|
/** MySQL 5.7 password column name */
|
|
#define MYSQL57_PASSWORD "authentication_string"
|
|
|
|
/**
|
|
* Query template which resolves user grants and access to databases at the table level
|
|
*
|
|
* The first two parameters for this template should be the 'password' column name.
|
|
* The third parameter is either an empty string the the contents of USERS_QUERY_NO_ROOT
|
|
* if the root user is not included. These three parameters are then repeated in the same
|
|
* order for the remaining parameters. For an example on how it is used see get_users_db_query()
|
|
*/
|
|
#define MYSQL_USERS_DB_QUERY_TEMPLATE \
|
|
"SELECT DISTINCT \
|
|
user.user AS user, \
|
|
user.host AS host, \
|
|
user.%s AS password, \
|
|
concat(user.user,user.host,user.%s, \
|
|
user.Select_priv, COALESCE(db.db, '')) AS userdata, \
|
|
user.Select_priv AS anydb, \
|
|
db.db AS db \
|
|
FROM \
|
|
mysql.user LEFT JOIN \
|
|
mysql.db ON user.user=db.user AND user.host=db.host \
|
|
WHERE user.user IS NOT NULL AND user.user <> '' %s \
|
|
UNION \
|
|
SELECT DISTINCT \
|
|
user.user AS user, \
|
|
user.host AS host, \
|
|
user.%s AS password, \
|
|
concat(user.user,user.host,user.%s, \
|
|
user.Select_priv, COALESCE(tp.db, '')) AS userdata, \
|
|
user.Select_priv AS anydb, \
|
|
tp.db as db FROM \
|
|
mysql.tables_priv AS tp LEFT JOIN \
|
|
mysql.user ON user.user=tp.user AND user.host=tp.host \
|
|
WHERE user.user IS NOT NULL AND user.user <> '' %s"
|
|
|
|
#define MYSQL_USERS_QUERY_TEMPLATE "SELECT \
|
|
user, host, %s, concat(user, host, %s, Select_priv) AS userdata, \
|
|
Select_priv AS anydb FROM mysql.user WHERE user.user IS NOT NULL AND user.user <> ''"
|
|
|
|
/** User count query split into two parts. This way the actual query used to
|
|
* fetch the users can be inserted as a subquery between the START and END
|
|
* portions of them. */
|
|
#define MYSQL_USERS_COUNT_TEMPLATE_START "SELECT COUNT(1) AS nusers_db FROM ("
|
|
#define MYSQL_USERS_COUNT_TEMPLATE_END ") AS tbl_count"
|
|
|
|
/** The maximum possible length of the query */
|
|
#define MAX_QUERY_STR_LEN strlen(MYSQL_USERS_COUNT_TEMPLATE_START MYSQL_USERS_COUNT_TEMPLATE_END \
|
|
MYSQL_USERS_DB_QUERY_TEMPLATE) + strlen(USERS_QUERY_NO_ROOT) * 2 + strlen(MYSQL57_PASSWORD) * 4 + 1
|
|
|
|
#define LOAD_MYSQL_DATABASE_NAMES "SELECT * \
|
|
FROM ( (SELECT COUNT(1) AS ndbs \
|
|
FROM INFORMATION_SCHEMA.SCHEMATA) AS tbl1, \
|
|
(SELECT GRANTEE,PRIVILEGE_TYPE from INFORMATION_SCHEMA.USER_PRIVILEGES \
|
|
WHERE privilege_type='SHOW DATABASES' AND REPLACE(GRANTEE, \'\\'\',\'\')=CURRENT_USER()) AS tbl2)"
|
|
|
|
#define ERROR_NO_SHOW_DATABASES "%s: Unable to load database grant information, \
|
|
MaxScale authentication will proceed without including database permissions. \
|
|
See earlier error messages for user '%s' for more information."
|
|
|
|
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);
|
|
static void *dbusers_keyread(int fd);
|
|
static int dbusers_keywrite(int fd, void *key);
|
|
static void *dbusers_valueread(int fd);
|
|
static int dbusers_valuewrite(int fd, void *value);
|
|
static int get_all_users(SERV_LISTENER *listener, USERS *users);
|
|
static int get_databases(SERV_LISTENER *listener, MYSQL *users);
|
|
static int get_users(SERV_LISTENER *listener, USERS *users);
|
|
static MYSQL *gw_mysql_init(void);
|
|
static int gw_mysql_set_timeouts(MYSQL* handle);
|
|
static bool host_has_singlechar_wildcard(const char *host);
|
|
static bool host_matches_singlechar_wildcard(const char* user, const char* wild);
|
|
static bool is_ipaddress(const char* host);
|
|
static char *mysql_format_user_entry(void *data);
|
|
static char *mysql_format_user_entry(void *data);
|
|
static int normalize_hostname(const char *input_host, char *output_host);
|
|
static int resource_add(HASHTABLE *, char *, char *);
|
|
static HASHTABLE *resource_alloc();
|
|
static void *resource_fetch(HASHTABLE *, char *);
|
|
static void resource_free(HASHTABLE *resource);
|
|
static int uh_cmpfun(const void* v1, const void* v2);
|
|
static int uh_hfun(const void* key);
|
|
static MYSQL_USER_HOST *uh_keydup(const MYSQL_USER_HOST* key);
|
|
static void uh_keyfree(MYSQL_USER_HOST* key);
|
|
static int wildcard_db_grant(char* str);
|
|
|
|
/**
|
|
* Get the user data query with databases
|
|
*
|
|
* @param server_version Server version string
|
|
* @param include_root Include root user
|
|
* @param buffer Destination where the query is written. Must be at least
|
|
* MAX_QUERY_STR_LEN bytes long
|
|
* @return Users query with databases included
|
|
*/
|
|
static char* get_users_db_query(const char* server_version, bool include_root, char* buffer)
|
|
{
|
|
const char* password = strstr(server_version, "5.7.") ?
|
|
MYSQL57_PASSWORD : MYSQL_PASSWORD;
|
|
|
|
int nchars = snprintf(buffer, MAX_QUERY_STR_LEN, MYSQL_USERS_DB_QUERY_TEMPLATE,
|
|
password, password, include_root ? "" : USERS_QUERY_NO_ROOT,
|
|
password, password, include_root ? "" : USERS_QUERY_NO_ROOT);
|
|
ss_dassert(nchars < MAX_QUERY_STR_LEN);
|
|
(void) nchars;
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* Get the user data query
|
|
*
|
|
* @param server_version Server version string
|
|
* @param include_root Include root user
|
|
* @param buffer Destination where the query is written. Must be at least
|
|
* MAX_QUERY_STR_LEN bytes long
|
|
* @return Users query
|
|
*/
|
|
static char* get_users_query(const char* server_version, bool include_root, char* buffer)
|
|
{
|
|
const char* password = strstr(server_version, "5.7.") ?
|
|
MYSQL57_PASSWORD : MYSQL_PASSWORD;
|
|
|
|
int nchars = snprintf(buffer, MAX_QUERY_STR_LEN, MYSQL_USERS_QUERY_TEMPLATE "%s",
|
|
password, password, include_root ? "" : USERS_QUERY_NO_ROOT);
|
|
ss_dassert(nchars < MAX_QUERY_STR_LEN);
|
|
(void) nchars;
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* Get the user count query
|
|
*
|
|
* @param server_version Server version string
|
|
* @param buffer Destination where the query is written. Must be at least
|
|
* MAX_QUERY_STR_LEN bytes long
|
|
* @return User count query
|
|
* */
|
|
static char* get_usercount_query(const char* server_version, bool include_root, char* buffer)
|
|
{
|
|
const char* password = strstr(server_version, "5.7.") ?
|
|
MYSQL57_PASSWORD : MYSQL_PASSWORD;
|
|
|
|
int nchars = snprintf(buffer, MAX_QUERY_STR_LEN, MYSQL_USERS_COUNT_TEMPLATE_START
|
|
MYSQL_USERS_DB_QUERY_TEMPLATE MYSQL_USERS_COUNT_TEMPLATE_END,
|
|
password, password, include_root ? "" : USERS_QUERY_NO_ROOT,
|
|
password, password, include_root ? "" : USERS_QUERY_NO_ROOT);
|
|
ss_dassert(nchars < MAX_QUERY_STR_LEN);
|
|
(void) nchars;
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param userhost User host address
|
|
* @param wildcardhost Host address in the grant
|
|
* @return True if the host address matches
|
|
*/
|
|
static bool host_matches_singlechar_wildcard(const char* user, const char* wild)
|
|
{
|
|
while (*user != '\0' && *wild != '\0')
|
|
{
|
|
if (*user != *wild && *wild != '_')
|
|
{
|
|
return false;
|
|
}
|
|
user++;
|
|
wild++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Load the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
load_mysql_users(SERV_LISTENER *listener)
|
|
{
|
|
return get_users(listener, listener->users);
|
|
}
|
|
|
|
/**
|
|
* Reload the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
reload_mysql_users(SERV_LISTENER *listener)
|
|
{
|
|
int i;
|
|
USERS *newusers, *oldusers;
|
|
HASHTABLE *oldresources;
|
|
|
|
if ((newusers = mysql_users_alloc()) == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
spinlock_acquire(&listener->lock);
|
|
|
|
oldresources = listener->resources;
|
|
i = get_users(listener, newusers);
|
|
oldusers = listener->users;
|
|
listener->users = newusers;
|
|
|
|
spinlock_release(&listener->lock);
|
|
|
|
users_free(oldusers);
|
|
resource_free(oldresources);
|
|
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Replace the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
* The replacement is succesful only if the users' table checksums differ
|
|
*
|
|
* @param service The current service
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
int
|
|
replace_mysql_users(SERV_LISTENER *listener)
|
|
{
|
|
int i;
|
|
USERS *newusers, *oldusers;
|
|
HASHTABLE *oldresources;
|
|
|
|
if ((newusers = mysql_users_alloc()) == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
spinlock_acquire(&listener->lock);
|
|
oldresources = listener->resources;
|
|
|
|
/* load db users ad db grants */
|
|
i = get_users(listener, newusers);
|
|
|
|
if (i <= 0)
|
|
{
|
|
users_free(newusers);
|
|
/* restore resources */
|
|
listener->resources = oldresources;
|
|
spinlock_release(&listener->lock);
|
|
return i;
|
|
}
|
|
|
|
oldusers = listener->users;
|
|
|
|
/* digest compare */
|
|
if (oldusers != NULL && memcmp(oldusers->cksum, newusers->cksum,
|
|
SHA_DIGEST_LENGTH) == 0)
|
|
{
|
|
/* same data, nothing to do */
|
|
MXS_DEBUG("%lu [replace_mysql_users] users' tables not switched, checksum is the same",
|
|
pthread_self());
|
|
|
|
/* free the new table */
|
|
users_free(newusers);
|
|
i = 0;
|
|
}
|
|
else
|
|
{
|
|
/* replace the service with effective new data */
|
|
MXS_DEBUG("%lu [replace_mysql_users] users' tables replaced, checksum differs",
|
|
pthread_self());
|
|
listener->users = newusers;
|
|
}
|
|
|
|
spinlock_release(&listener->lock);
|
|
|
|
/* free old resources */
|
|
resource_free(oldresources);
|
|
|
|
if (i && oldusers)
|
|
{
|
|
/* free the old table */
|
|
users_free(oldusers);
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Check if the IP address is a valid MySQL IP address. The IP address can contain
|
|
* single or multi-character wildcards as used by MySQL.
|
|
* @param host IP address to check
|
|
* @return True if the address is a valid, MySQL type IP address
|
|
*/
|
|
static bool is_ipaddress(const char* host)
|
|
{
|
|
while (*host != '\0')
|
|
{
|
|
if (!isdigit(*host) && *host != '.' && *host != '_' && *host != '%')
|
|
{
|
|
return false;
|
|
}
|
|
host++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if an IP address has single-character wildcards. A single-character
|
|
* wildcard is represented by an underscore in the MySQL hostnames.
|
|
* @param host Hostname to check
|
|
* @return True if the hostname is a valid IP address with a single character wildcard
|
|
*/
|
|
static bool host_has_singlechar_wildcard(const char *host)
|
|
{
|
|
const char* chrptr = host;
|
|
bool retval = false;
|
|
|
|
while (*chrptr != '\0')
|
|
{
|
|
if (!isdigit(*chrptr) && *chrptr != '.')
|
|
{
|
|
if (*chrptr == '_')
|
|
{
|
|
retval = true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
chrptr++;
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* Add a new MySQL user with host, password and netmask into the service users table
|
|
*
|
|
* The netmask values are:
|
|
* 0 for any, 32 for single IPv4
|
|
* 24 for a class C from a.b.c.%, 16 for a Class B from a.b.%.% and 8 for a Class A from a.%.%.%
|
|
*
|
|
* @param users The users table
|
|
* @param user The user name
|
|
* @param host The host to add, with possible wildcards
|
|
* @param passwd The sha1(sha1(passoword)) to add
|
|
* @return 1 on success, 0 on failure and -1 on duplicate user
|
|
*/
|
|
|
|
int add_mysql_users_with_host_ipv4(USERS *users, const char *user, const char *host,
|
|
char *passwd, const char *anydb, const char *db)
|
|
{
|
|
struct sockaddr_in serv_addr;
|
|
MYSQL_USER_HOST key;
|
|
char ret_ip[400] = "";
|
|
int ret = 0;
|
|
|
|
if (users == NULL || user == NULL || host == NULL)
|
|
{
|
|
return ret;
|
|
}
|
|
|
|
/* prepare the user@host data struct */
|
|
memset(&serv_addr, 0, sizeof(serv_addr));
|
|
memset(&key, 0, sizeof(key));
|
|
|
|
/* set user */
|
|
key.user = MXS_STRDUP(user);
|
|
|
|
if (key.user == NULL)
|
|
{
|
|
return ret;
|
|
}
|
|
|
|
/* for anydb == Y key.resource is '\0' as set by memset */
|
|
if (anydb == NULL)
|
|
{
|
|
key.resource = NULL;
|
|
}
|
|
else
|
|
{
|
|
if (strcmp(anydb, "N") == 0)
|
|
{
|
|
if (db != NULL)
|
|
{
|
|
key.resource = MXS_STRDUP(db);
|
|
MXS_ABORT_IF_NULL(key.resource);
|
|
}
|
|
else
|
|
{
|
|
key.resource = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
key.resource = MXS_STRDUP("");
|
|
MXS_ABORT_IF_NULL(key.resource);
|
|
}
|
|
}
|
|
|
|
/* handle ANY, Class C,B,A */
|
|
|
|
/* ANY */
|
|
if (strcmp(host, "%") == 0)
|
|
{
|
|
strcpy(ret_ip, "0.0.0.0");
|
|
key.netmask = 0;
|
|
}
|
|
else if (strnlen(host, MYSQL_HOST_MAXLEN + 1) <= MYSQL_HOST_MAXLEN &&
|
|
is_ipaddress(host) &&
|
|
host_has_singlechar_wildcard(host))
|
|
{
|
|
strcpy(key.hostname, host);
|
|
strcpy(ret_ip, "0.0.0.0");
|
|
key.netmask = 0;
|
|
}
|
|
else
|
|
{
|
|
/* hostname without % wildcards has netmask = 32 */
|
|
key.netmask = normalize_hostname(host, ret_ip);
|
|
|
|
if (key.netmask == -1)
|
|
{
|
|
MXS_ERROR("strdup() failed in normalize_hostname for %s@%s", user, host);
|
|
}
|
|
}
|
|
|
|
/* fill IPv4 data struct */
|
|
if (setipaddress(&serv_addr.sin_addr, ret_ip) && strlen(ret_ip))
|
|
{
|
|
|
|
/* copy IPv4 data into key.ipv4 */
|
|
memcpy(&key.ipv4, &serv_addr, sizeof(serv_addr));
|
|
|
|
/* if netmask < 32 there are % wildcards */
|
|
if (key.netmask < 32)
|
|
{
|
|
/* let's zero the last IP byte: a.b.c.0 we may have set above to 1*/
|
|
key.ipv4.sin_addr.s_addr &= 0x00FFFFFF;
|
|
}
|
|
|
|
/* add user@host as key and passwd as value in the MySQL users hash table */
|
|
if (mysql_users_add(users, &key, passwd))
|
|
{
|
|
ret = 1;
|
|
}
|
|
else if (key.user)
|
|
{
|
|
ret = -1;
|
|
}
|
|
}
|
|
|
|
MXS_FREE(key.user);
|
|
MXS_FREE(key.resource);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Add the database specific grants from mysql.db table into the service resources hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @param users The users table into which to load the users
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
static int
|
|
add_databases(SERV_LISTENER *listener, MYSQL *con)
|
|
{
|
|
SERVICE *service = listener->service;
|
|
MYSQL_ROW row;
|
|
MYSQL_RES *result = NULL;
|
|
char *service_user = NULL;
|
|
char *service_passwd = NULL;
|
|
int ndbs = 0;
|
|
|
|
char *get_showdbs_priv_query = LOAD_MYSQL_DATABASE_NAMES;
|
|
|
|
serviceGetUser(service, &service_user, &service_passwd);
|
|
|
|
if (service_user == NULL || service_passwd == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (mysql_query(con, get_showdbs_priv_query))
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
return -1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
return -1;
|
|
}
|
|
|
|
/* Result has only one row */
|
|
row = mysql_fetch_row(result);
|
|
|
|
if (row)
|
|
{
|
|
ndbs = atoi(row[0]);
|
|
}
|
|
else
|
|
{
|
|
ndbs = 0;
|
|
|
|
MXS_ERROR("Failed to retrieve database names: %s", mysql_error(con));
|
|
MXS_ERROR(ERROR_NO_SHOW_DATABASES, service->name, service_user);
|
|
}
|
|
|
|
/* free resut set */
|
|
mysql_free_result(result);
|
|
|
|
if (!ndbs)
|
|
{
|
|
/* return if no db names are available */
|
|
return 0;
|
|
}
|
|
|
|
if (mysql_query(con, "SHOW DATABASES"))
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
|
|
return -1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* insert key and value "" */
|
|
while ((row = mysql_fetch_row(result)))
|
|
{
|
|
if (resource_add(listener->resources, row[0], ""))
|
|
{
|
|
MXS_DEBUG("%s: Adding database %s to the resouce hash.", service->name, row[0]);
|
|
}
|
|
}
|
|
|
|
mysql_free_result(result);
|
|
|
|
return ndbs;
|
|
}
|
|
|
|
/**
|
|
* Load the database specific grants from mysql.db table into the service resources hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @param users The users table into which to load the users
|
|
* @return -1 on any error or the number of users inserted (0 means no users at all)
|
|
*/
|
|
static int
|
|
get_databases(SERV_LISTENER *listener, MYSQL *con)
|
|
{
|
|
SERVICE *service = listener->service;
|
|
MYSQL_ROW row;
|
|
MYSQL_RES *result = NULL;
|
|
char *service_user = NULL;
|
|
char *service_passwd = NULL;
|
|
int ndbs = 0;
|
|
|
|
char *get_showdbs_priv_query = LOAD_MYSQL_DATABASE_NAMES;
|
|
|
|
serviceGetUser(service, &service_user, &service_passwd);
|
|
|
|
if (service_user == NULL || service_passwd == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (mysql_query(con, get_showdbs_priv_query))
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error when querying database privileges: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
return -1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error when storing result set of database privilege query: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
return -1;
|
|
}
|
|
|
|
/* Result has only one row */
|
|
row = mysql_fetch_row(result);
|
|
|
|
if (row)
|
|
{
|
|
ndbs = atoi(row[0]);
|
|
}
|
|
else
|
|
{
|
|
ndbs = 0;
|
|
|
|
MXS_ERROR("Failed to retrieve database names: %s", mysql_error(con));
|
|
MXS_ERROR(ERROR_NO_SHOW_DATABASES, service->name, service_user);
|
|
}
|
|
|
|
/* free resut set */
|
|
mysql_free_result(result);
|
|
|
|
if (!ndbs)
|
|
{
|
|
/* return if no db names are available */
|
|
return 0;
|
|
}
|
|
|
|
if (mysql_query(con, "SHOW DATABASES"))
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error when executing SHOW DATABASES query: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
|
|
return -1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading database names for service %s encountered "
|
|
"error when storing the result set: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Now populate service->resources hashatable with db names */
|
|
listener->resources = resource_alloc();
|
|
|
|
/* insert key and value "" */
|
|
while ((row = mysql_fetch_row(result)))
|
|
{
|
|
MXS_DEBUG("%s: Adding database %s to the resouce hash.", service->name, row[0]);
|
|
resource_add(listener->resources, row[0], "");
|
|
}
|
|
|
|
mysql_free_result(result);
|
|
|
|
return ndbs;
|
|
}
|
|
|
|
/**
|
|
* Load the user/passwd from mysql.user table into the service users' hashtable
|
|
* environment from all the backend servers.
|
|
*
|
|
* @param service The current service
|
|
* @param users The users table into which to load the users
|
|
* @return -1 on any error or the number of users inserted
|
|
*/
|
|
static int
|
|
get_all_users(SERV_LISTENER *listener, USERS *users)
|
|
{
|
|
SERVICE *service = listener->service;
|
|
MYSQL *con = NULL;
|
|
MYSQL_ROW row;
|
|
MYSQL_RES *result = NULL;
|
|
char *service_user = NULL;
|
|
char *service_passwd = NULL;
|
|
char *dpwd = NULL;
|
|
int total_users = 0;
|
|
SERVER_REF *server;
|
|
const char *userquery;
|
|
char *tmp;
|
|
unsigned char hash[SHA_DIGEST_LENGTH] = "";
|
|
char *users_data = NULL;
|
|
char *final_data = NULL;
|
|
char dbnm[MYSQL_DATABASE_MAXLEN + 1];
|
|
int nusers = -1;
|
|
int users_data_row_len = MYSQL_USER_MAXLEN + MYSQL_HOST_MAXLEN +
|
|
MYSQL_PASSWORD_LEN + sizeof(char) + MYSQL_DATABASE_MAXLEN;
|
|
int dbnames = 0;
|
|
int db_grants = 0;
|
|
bool anon_user = false;
|
|
|
|
if (serviceGetUser(service, &service_user, &service_passwd) == 0)
|
|
{
|
|
ss_dassert(service_passwd == NULL || service_user == NULL);
|
|
return -1;
|
|
}
|
|
|
|
if (service->svc_do_shutdown)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
dpwd = decryptPassword(service_passwd);
|
|
final_data = (char*) MXS_MALLOC(sizeof(char));
|
|
MXS_ABORT_IF_NULL(final_data);
|
|
*final_data = '\0';
|
|
|
|
/**
|
|
* Attempt to connect to one of the databases database or until we run
|
|
* out of databases
|
|
* to try
|
|
*/
|
|
server = service->dbref;
|
|
|
|
if (server == NULL)
|
|
{
|
|
goto cleanup;
|
|
}
|
|
|
|
listener->resources = resource_alloc();
|
|
|
|
while (server != NULL)
|
|
{
|
|
while (!service->svc_do_shutdown && server != NULL)
|
|
{
|
|
con = gw_mysql_init();
|
|
if (con)
|
|
{
|
|
if (mxs_mysql_real_connect(con, server->server, service_user, dpwd) == NULL)
|
|
{
|
|
MXS_ERROR("Failure loading users data from backend "
|
|
"[%s:%i] for service [%s]. MySQL error %i, %s",
|
|
server->server->name, server->server->port,
|
|
service->name, mysql_errno(con), mysql_error(con));
|
|
mysql_close(con);
|
|
}
|
|
else
|
|
{
|
|
/** Successfully connected to a server */
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
server = NULL;
|
|
break;
|
|
}
|
|
|
|
server = server->next;
|
|
}
|
|
|
|
if (server == NULL)
|
|
{
|
|
MXS_ERROR("Unable to get user data from backend database "
|
|
"for service [%s]. Missing server information.",
|
|
service->name);
|
|
goto cleanup;
|
|
}
|
|
|
|
add_databases(listener, con);
|
|
mysql_close(con);
|
|
server = server->next;
|
|
}
|
|
|
|
server = service->dbref;
|
|
|
|
while (server != NULL)
|
|
{
|
|
while (!service->svc_do_shutdown && server != NULL)
|
|
{
|
|
con = gw_mysql_init();
|
|
if (con)
|
|
{
|
|
if (mxs_mysql_real_connect(con, server->server, service_user, dpwd) == NULL)
|
|
{
|
|
MXS_ERROR("Failure loading users data from backend "
|
|
"[%s:%i] for service [%s]. MySQL error %i, %s",
|
|
server->server->name, server->server->port,
|
|
service->name, mysql_errno(con), mysql_error(con));
|
|
mysql_close(con);
|
|
}
|
|
else
|
|
{
|
|
/** Successfully connected to a server */
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
server = NULL;
|
|
break;
|
|
}
|
|
|
|
server = server->next;
|
|
}
|
|
|
|
if (server == NULL)
|
|
{
|
|
MXS_ERROR("Unable to get user data from backend database "
|
|
"for service [%s]. Missing server information.",
|
|
service->name);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (server->server->server_string == NULL)
|
|
{
|
|
const char *server_string = mysql_get_server_info(con);
|
|
if (!server_set_version_string(server->server, server_string))
|
|
{
|
|
mysql_close(con);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
char querybuffer[MAX_QUERY_STR_LEN];
|
|
/** Count users. Start with users and db grants for users */
|
|
const char *usercount = get_usercount_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
if (mysql_query(con, usercount))
|
|
{
|
|
if (mysql_errno(con) != ER_TABLEACCESS_DENIED_ERROR)
|
|
{
|
|
/* This is an error we cannot handle, return */
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name,
|
|
mysql_error(con));
|
|
mysql_close(con);
|
|
goto cleanup;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We have got ER_TABLEACCESS_DENIED_ERROR
|
|
* try counting users from mysql.user without DB names.
|
|
*/
|
|
if (mysql_query(con, MYSQL_USERS_COUNT))
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name,
|
|
mysql_error(con));
|
|
mysql_close(con);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name,
|
|
mysql_error(con));
|
|
mysql_close(con);
|
|
goto cleanup;
|
|
}
|
|
|
|
row = mysql_fetch_row(result);
|
|
|
|
nusers = atoi(row[0]);
|
|
|
|
mysql_free_result(result);
|
|
|
|
if (!nusers)
|
|
{
|
|
MXS_ERROR("Counting users for service %s returned 0.", service->name);
|
|
mysql_close(con);
|
|
goto cleanup;
|
|
}
|
|
|
|
userquery = get_users_db_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
|
|
/* send first the query that fetches users and db grants */
|
|
if (mysql_query(con, userquery))
|
|
{
|
|
/*
|
|
* An error occurred executing the query
|
|
*
|
|
* Check mysql_errno() against ER_TABLEACCESS_DENIED_ERROR)
|
|
*/
|
|
|
|
if (1142 != mysql_errno(con))
|
|
{
|
|
/* This is an error we cannot handle, return */
|
|
|
|
MXS_ERROR("Loading users with dbnames for service [%s] encountered "
|
|
"error: [%s], MySQL errno %i",
|
|
service->name,
|
|
mysql_error(con),
|
|
mysql_errno(con));
|
|
|
|
mysql_close(con);
|
|
|
|
goto cleanup;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We have got ER_TABLEACCESS_DENIED_ERROR
|
|
* try loading users from mysql.user without DB names.
|
|
*/
|
|
|
|
MXS_ERROR("Failed to retrieve users: %s", mysql_error(con));
|
|
MXS_ERROR(ERROR_NO_SHOW_DATABASES, service->name, service_user);
|
|
|
|
userquery = get_users_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
|
|
if (mysql_query(con, userquery))
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered "
|
|
"error: [%s], code %i",
|
|
service->name,
|
|
mysql_error(con),
|
|
mysql_errno(con));
|
|
|
|
mysql_close(con);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
/* users successfully loaded but without db grants */
|
|
|
|
MXS_NOTICE("Loading users from [mysql.user] without access to [mysql.db] for "
|
|
"service [%s]. MaxScale Authentication with DBname on connect "
|
|
"will not consider database grants.",
|
|
service->name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* users successfully loaded with db grants.
|
|
*/
|
|
MXS_DEBUG("[%s] Loading users with db grants.", service->name);
|
|
db_grants = 1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading users for service %s encountered error: %s.",
|
|
service->name,
|
|
mysql_error(con));
|
|
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
users_data = (char *) MXS_CALLOC(nusers, (users_data_row_len * sizeof(char)) + 1);
|
|
|
|
if (users_data == NULL)
|
|
{
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
while ((row = mysql_fetch_row(result)))
|
|
{
|
|
|
|
/**
|
|
* Up to six fields could be returned.
|
|
* user,host,passwd,concat(),anydb,db
|
|
* passwd+1 (escaping the first byte that is '*')
|
|
*/
|
|
|
|
int rc = 0;
|
|
char *password = NULL;
|
|
|
|
/** If the username is empty, the backend server still has anonymous
|
|
* user in it. This will mean that localhost addresses do not match
|
|
* the wildcard host '%' */
|
|
if (strlen(row[0]) == 0)
|
|
{
|
|
anon_user = true;
|
|
continue;
|
|
}
|
|
|
|
if (row[2] != NULL)
|
|
{
|
|
/* detect mysql_old_password (pre 4.1 protocol) */
|
|
if (strlen(row[2]) == 16)
|
|
{
|
|
MXS_ERROR("%s: The user %s@%s has on old password in the "
|
|
"backend database. MaxScale does not support these "
|
|
"old passwords. This user will not be able to connect "
|
|
"via MaxScale. Update the users password to correct "
|
|
"this.",
|
|
service->name,
|
|
row[0],
|
|
row[1]);
|
|
continue;
|
|
}
|
|
|
|
if (strlen(row[2]) > 1)
|
|
{
|
|
password = row[2] + 1;
|
|
}
|
|
else
|
|
{
|
|
password = row[2];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* add user@host and DB global priv and specificsa grant (if possible)
|
|
*/
|
|
bool havedb = false;
|
|
|
|
if (db_grants)
|
|
{
|
|
/* we have dbgrants, store them */
|
|
if (row[5])
|
|
{
|
|
unsigned long *rowlen = mysql_fetch_lengths(result);
|
|
memcpy(dbnm, row[5], rowlen[5]);
|
|
memset(dbnm + rowlen[5], 0, 1);
|
|
havedb = true;
|
|
if (service->strip_db_esc)
|
|
{
|
|
strip_escape_chars(dbnm);
|
|
MXS_DEBUG("[%s]: %s -> %s",
|
|
service->name,
|
|
row[5],
|
|
dbnm);
|
|
}
|
|
}
|
|
|
|
rc = add_mysql_users_with_host_ipv4(users, row[0], row[1],
|
|
password, row[4],
|
|
havedb ? dbnm : NULL);
|
|
|
|
MXS_DEBUG("%s: Adding user:%s host:%s anydb:%s db:%s.",
|
|
service->name, row[0], row[1], row[4],
|
|
havedb ? dbnm : NULL);
|
|
}
|
|
else
|
|
{
|
|
/* we don't have dbgrants, simply set ANY DB for the user */
|
|
rc = add_mysql_users_with_host_ipv4(users, row[0], row[1],
|
|
password, "Y", NULL);
|
|
}
|
|
|
|
if (rc == 1)
|
|
{
|
|
if (db_grants)
|
|
{
|
|
char dbgrant[MYSQL_DATABASE_MAXLEN + 1] = "";
|
|
if (row[4] != NULL)
|
|
{
|
|
if (strcmp(row[4], "Y") == 0)
|
|
{
|
|
strcpy(dbgrant, "ANY");
|
|
}
|
|
else if (row[5])
|
|
{
|
|
strncpy(dbgrant, row[5], MYSQL_DATABASE_MAXLEN);
|
|
dbgrant[MYSQL_DATABASE_MAXLEN] = 0;
|
|
}
|
|
}
|
|
|
|
if (!strlen(dbgrant))
|
|
{
|
|
strcpy(dbgrant, "no db");
|
|
}
|
|
|
|
/* Log the user being added with its db grants */
|
|
MXS_INFO("%s: User %s@%s for database %s added to service user table.",
|
|
service->name, row[0], row[1], dbgrant);
|
|
}
|
|
else
|
|
{
|
|
/* Log the user being added (without db grants) */
|
|
MXS_INFO("%s: User %s@%s added to service user table.",
|
|
service->name, row[0], row[1]);
|
|
}
|
|
|
|
/* Append data in the memory area for SHA1 digest */
|
|
strncat(users_data, row[3], users_data_row_len);
|
|
total_users++;
|
|
}
|
|
else
|
|
{
|
|
/** Log errors and not the duplicate user */
|
|
if (service->log_auth_warnings && rc != -1)
|
|
{
|
|
MXS_WARNING("Failed to add user %s@%s for service [%s]."
|
|
" This user will be unavailable via MaxScale.",
|
|
row[0], row[1], service->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
|
|
if ((tmp = MXS_REALLOC(final_data, (strlen(final_data) + strlen(users_data)
|
|
+ 1) * sizeof(char))) == NULL)
|
|
{
|
|
MXS_FREE(users_data);
|
|
goto cleanup;
|
|
}
|
|
|
|
final_data = tmp;
|
|
|
|
strcat(final_data, users_data);
|
|
MXS_FREE(users_data);
|
|
|
|
if (service->users_from_all)
|
|
{
|
|
server = server->next;
|
|
}
|
|
else
|
|
{
|
|
server = NULL;
|
|
}
|
|
}
|
|
|
|
/* compute SHA1 digest for users' data */
|
|
SHA1((const unsigned char *) final_data, strlen(final_data), hash);
|
|
|
|
memcpy(users->cksum, hash, SHA_DIGEST_LENGTH);
|
|
|
|
/** Set the parameter if it is not configured by the user */
|
|
if (service->localhost_match_wildcard_host == SERVICE_PARAM_UNINIT)
|
|
{
|
|
service->localhost_match_wildcard_host = anon_user ? 0 : 1;
|
|
}
|
|
cleanup:
|
|
|
|
MXS_FREE(dpwd);
|
|
MXS_FREE(final_data);
|
|
|
|
return total_users;
|
|
}
|
|
|
|
/**
|
|
* Load the user/passwd form mysql.user table into the service users' hashtable
|
|
* environment.
|
|
*
|
|
* @param service The current service
|
|
* @param users The users table into which to load the users
|
|
* @return -1 on any error or the number of users inserted
|
|
*/
|
|
static int
|
|
get_users(SERV_LISTENER *listener, USERS *users)
|
|
{
|
|
SERVICE *service = listener->service;
|
|
MYSQL *con = NULL;
|
|
MYSQL_ROW row;
|
|
MYSQL_RES *result = NULL;
|
|
char *service_user = NULL;
|
|
char *service_passwd = NULL;
|
|
char *dpwd;
|
|
int total_users = 0;
|
|
SERVER_REF *server;
|
|
const char *userquery;
|
|
unsigned char hash[SHA_DIGEST_LENGTH] = "";
|
|
char *users_data = NULL;
|
|
int nusers = 0;
|
|
int users_data_row_len = MYSQL_USER_MAXLEN +
|
|
MYSQL_HOST_MAXLEN +
|
|
MYSQL_PASSWORD_LEN +
|
|
sizeof(char) +
|
|
MYSQL_DATABASE_MAXLEN;
|
|
int dbnames = 0;
|
|
int db_grants = 0;
|
|
char dbnm[MYSQL_DATABASE_MAXLEN + 1];
|
|
bool anon_user = false;
|
|
|
|
if (serviceGetUser(service, &service_user, &service_passwd) == 0)
|
|
{
|
|
ss_dassert(service_passwd == NULL || service_user == NULL);
|
|
return -1;
|
|
}
|
|
|
|
if (service->users_from_all)
|
|
{
|
|
return get_all_users(listener, users);
|
|
}
|
|
|
|
con = gw_mysql_init();
|
|
|
|
if (!con)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Attempt to connect to one of the databases database or until we run
|
|
* out of databases
|
|
* to try
|
|
*/
|
|
server = service->dbref;
|
|
dpwd = decryptPassword(service_passwd);
|
|
|
|
/* Select a server with Master bit, if available */
|
|
while (server != NULL && !(server->server->status & SERVER_MASTER))
|
|
{
|
|
server = server->next;
|
|
}
|
|
|
|
if (service->svc_do_shutdown)
|
|
{
|
|
MXS_FREE(dpwd);
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
/* Try loading data from master server */
|
|
if (server != NULL &&
|
|
(mxs_mysql_real_connect(con, server->server, service_user, dpwd) != NULL))
|
|
{
|
|
MXS_DEBUG("Dbusers : Loading data from backend database with "
|
|
"Master role [%s:%i] for service [%s]",
|
|
server->server->name,
|
|
server->server->port,
|
|
service->name);
|
|
}
|
|
else
|
|
{
|
|
mysql_close(con);
|
|
/* load data from other servers via loop */
|
|
server = service->dbref;
|
|
|
|
while (!service->svc_do_shutdown && server != NULL)
|
|
{
|
|
con = gw_mysql_init();
|
|
if (con)
|
|
{
|
|
if (mxs_mysql_real_connect(con, server->server, service_user, dpwd) == NULL)
|
|
{
|
|
MXS_ERROR("Failure loading users data from backend "
|
|
"[%s:%i] for service [%s]. MySQL error %i, %s",
|
|
server->server->name, server->server->port,
|
|
service->name, mysql_errno(con), mysql_error(con));
|
|
mysql_close(con);
|
|
}
|
|
else
|
|
{
|
|
/** Successfully connected to a server */
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
server = NULL;
|
|
break;
|
|
}
|
|
|
|
server = server->next;
|
|
}
|
|
|
|
if (service->svc_do_shutdown)
|
|
{
|
|
MXS_FREE(dpwd);
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
if (server != NULL)
|
|
{
|
|
MXS_DEBUG("Loading data from backend database [%s:%i] for service [%s]",
|
|
server->server->name, server->server->port, service->name);
|
|
}
|
|
}
|
|
|
|
MXS_FREE(dpwd);
|
|
|
|
if (server == NULL)
|
|
{
|
|
MXS_ERROR("Unable to get user data from backend database for service [%s]."
|
|
" Failed to connect to any of the backend databases.", service->name);
|
|
return -1;
|
|
}
|
|
|
|
if (server->server->server_string == NULL)
|
|
{
|
|
const char *server_string = mysql_get_server_info(con);
|
|
if (!server_set_version_string(server->server, server_string))
|
|
{
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
char querybuffer[MAX_QUERY_STR_LEN];
|
|
const char *usercount = get_usercount_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
/** Count users. Start with users and db grants for users */
|
|
if (mysql_query(con, usercount))
|
|
{
|
|
if (mysql_errno(con) != ER_TABLEACCESS_DENIED_ERROR)
|
|
{
|
|
/* This is an error we cannot handle, return */
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name, mysql_error(con));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We have got ER_TABLEACCESS_DENIED_ERROR
|
|
* try counting users from mysql.user without DB names.
|
|
*/
|
|
if (mysql_query(con, MYSQL_USERS_COUNT))
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name, mysql_error(con));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered error: [%s].",
|
|
service->name, mysql_error(con));
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
row = mysql_fetch_row(result);
|
|
|
|
nusers = atoi(row[0]);
|
|
|
|
mysql_free_result(result);
|
|
|
|
if (!nusers)
|
|
{
|
|
MXS_ERROR("Counting users for service %s returned 0.", service->name);
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
userquery = get_users_db_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
/* send first the query that fetches users and db grants */
|
|
if (mysql_query(con, userquery))
|
|
{
|
|
/*
|
|
* An error occurred executing the query
|
|
*
|
|
* Check mysql_errno() against ER_TABLEACCESS_DENIED_ERROR)
|
|
*/
|
|
|
|
if (1142 != mysql_errno(con))
|
|
{
|
|
/* This is an error we cannot handle, return */
|
|
|
|
MXS_ERROR("Loading users with dbnames for service [%s] encountered "
|
|
"error: [%s], MySQL errno %i", service->name,
|
|
mysql_error(con), mysql_errno(con));
|
|
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We have got ER_TABLEACCESS_DENIED_ERROR
|
|
* try loading users from mysql.user without DB names.
|
|
*/
|
|
MXS_ERROR("Failed to retrieve users: %s", mysql_error(con));
|
|
MXS_ERROR(ERROR_NO_SHOW_DATABASES, service->name, service_user);
|
|
|
|
userquery = get_users_query(server->server->server_string,
|
|
service->enable_root, querybuffer);
|
|
|
|
if (mysql_query(con, userquery))
|
|
{
|
|
MXS_ERROR("Loading users for service [%s] encountered error: "
|
|
"[%s], code %i", service->name, mysql_error(con),
|
|
mysql_errno(con));
|
|
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
/* users successfully loaded but without db grants */
|
|
|
|
MXS_NOTICE("Loading users from [mysql.user] without access to [mysql.db] for "
|
|
"service [%s]. MaxScale Authentication with DBname on connect "
|
|
"will not consider database grants.", service->name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/** Users successfully loaded with database grants */
|
|
db_grants = 1;
|
|
}
|
|
|
|
result = mysql_store_result(con);
|
|
|
|
if (result == NULL)
|
|
{
|
|
MXS_ERROR("Loading users for service %s encountered error: %s.",
|
|
service->name, mysql_error(con));
|
|
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
users_data = (char *) MXS_CALLOC(nusers, (users_data_row_len * sizeof(char)) + 1);
|
|
|
|
if (users_data == NULL)
|
|
{
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
return -1;
|
|
}
|
|
|
|
if (db_grants)
|
|
{
|
|
/* load all mysql database names */
|
|
dbnames = get_databases(listener, con);
|
|
MXS_DEBUG("Loaded %d MySQL Database Names for service [%s]",
|
|
dbnames, service->name);
|
|
}
|
|
else
|
|
{
|
|
listener->resources = NULL;
|
|
}
|
|
|
|
while ((row = mysql_fetch_row(result)))
|
|
{
|
|
|
|
/**
|
|
* Up to six fields could be returned.
|
|
* user,host,passwd,concat(),anydb,db
|
|
* passwd+1 (escaping the first byte that is '*')
|
|
*/
|
|
|
|
int rc = 0;
|
|
char *password = NULL;
|
|
|
|
/** If the username is empty, the backend server still has anonymous
|
|
* user in it. This will mean that localhost addresses do not match
|
|
* the wildcard host '%' */
|
|
if (strlen(row[0]) == 0)
|
|
{
|
|
anon_user = true;
|
|
continue;
|
|
}
|
|
|
|
if (row[2] != NULL)
|
|
{
|
|
/* detect mysql_old_password (pre 4.1 protocol) */
|
|
if (strlen(row[2]) == 16)
|
|
{
|
|
MXS_ERROR("%s: The user %s@%s has on old password in the "
|
|
"backend database. MaxScale does not support these "
|
|
"old passwords. This user will not be able to connect "
|
|
"via MaxScale. Update the users password to correct "
|
|
"this.", service->name, row[0], row[1]);
|
|
continue;
|
|
}
|
|
|
|
if (strlen(row[2]) > 1)
|
|
{
|
|
password = row[2] + 1;
|
|
}
|
|
else
|
|
{
|
|
password = row[2];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* add user@host and DB global priv and specificsa grant (if possible)
|
|
*/
|
|
if (db_grants)
|
|
{
|
|
bool havedb = false;
|
|
/* we have dbgrants, store them */
|
|
if (row[5])
|
|
{
|
|
unsigned long *rowlen = mysql_fetch_lengths(result);
|
|
memcpy(dbnm, row[5], rowlen[5]);
|
|
memset(dbnm + rowlen[5], 0, 1);
|
|
havedb = true;
|
|
if (service->strip_db_esc)
|
|
{
|
|
strip_escape_chars(dbnm);
|
|
MXS_DEBUG("[%s]: %s -> %s", service->name, row[5], dbnm);
|
|
}
|
|
}
|
|
|
|
if (havedb && wildcard_db_grant(row[5]))
|
|
{
|
|
/** Use ANYDB for wildcard grants */
|
|
rc = add_mysql_users_with_host_ipv4(users, row[0], row[1],
|
|
password, "Y", NULL);
|
|
}
|
|
else
|
|
{
|
|
rc = add_mysql_users_with_host_ipv4(users, row[0], row[1],
|
|
password, row[4],
|
|
havedb ? dbnm : NULL);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
/* we don't have dbgrants, simply set ANY DB for the user */
|
|
rc = add_mysql_users_with_host_ipv4(users, row[0], row[1], password,
|
|
"Y", NULL);
|
|
}
|
|
|
|
if (rc == 1)
|
|
{
|
|
if (db_grants)
|
|
{
|
|
char dbgrant[MYSQL_DATABASE_MAXLEN + 1] = "";
|
|
if (row[4] != NULL)
|
|
{
|
|
if (strcmp(row[4], "Y") == 0)
|
|
{
|
|
strcpy(dbgrant, "ANY");
|
|
}
|
|
else if (row[5])
|
|
{
|
|
strncpy(dbgrant, row[5], MYSQL_DATABASE_MAXLEN);
|
|
dbgrant[MYSQL_DATABASE_MAXLEN] = 0;
|
|
}
|
|
}
|
|
|
|
if (!strlen(dbgrant))
|
|
{
|
|
strcpy(dbgrant, "no db");
|
|
}
|
|
|
|
/* Log the user being added with its db grants */
|
|
MXS_INFO("%s: User %s@%s for database %s added to "
|
|
"service user table.",
|
|
service->name,
|
|
row[0],
|
|
row[1],
|
|
dbgrant);
|
|
}
|
|
else
|
|
{
|
|
/* Log the user being added (without db grants) */
|
|
MXS_INFO("%s: User %s@%s added to service user table.",
|
|
service->name,
|
|
row[0],
|
|
row[1]);
|
|
}
|
|
|
|
/* Append data in the memory area for SHA1 digest */
|
|
strncat(users_data, row[3], users_data_row_len);
|
|
total_users++;
|
|
}
|
|
else
|
|
{
|
|
/** Log errors and not the duplicate user */
|
|
if (service->log_auth_warnings && rc != -1)
|
|
{
|
|
MXS_WARNING("Failed to add user %s@%s for"
|
|
" service [%s]. This user will be unavailable"
|
|
" via MaxScale.", row[0], row[1], service->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* compute SHA1 digest for users' data */
|
|
SHA1((const unsigned char *) users_data, strlen(users_data), hash);
|
|
|
|
memcpy(users->cksum, hash, SHA_DIGEST_LENGTH);
|
|
|
|
/** Set the parameter if it is not configured by the user */
|
|
if (service->localhost_match_wildcard_host == SERVICE_PARAM_UNINIT)
|
|
{
|
|
service->localhost_match_wildcard_host = anon_user ? 0 : 1;
|
|
}
|
|
|
|
MXS_FREE(users_data);
|
|
mysql_free_result(result);
|
|
mysql_close(con);
|
|
|
|
return total_users;
|
|
}
|
|
|
|
/**
|
|
* Allocate a new MySQL users table for mysql specific users@host as key
|
|
*
|
|
* @return The users table
|
|
*/
|
|
USERS *
|
|
mysql_users_alloc()
|
|
{
|
|
USERS *rval;
|
|
|
|
if ((rval = MXS_CALLOC(1, sizeof(USERS))) == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if ((rval->data = hashtable_alloc(USERS_HASHTABLE_DEFAULT_SIZE, uh_hfun,
|
|
uh_cmpfun)) == NULL)
|
|
{
|
|
MXS_FREE(rval);
|
|
return NULL;
|
|
}
|
|
|
|
/* set the MySQL user@host print routine for the debug interface */
|
|
rval->usersCustomUserFormat = mysql_format_user_entry;
|
|
|
|
/* the key is handled by uh_keydup/uh_keyfree.
|
|
* the value is a (char *): it's handled by strdup/free
|
|
*/
|
|
hashtable_memory_fns(rval->data,
|
|
(HASHCOPYFN) uh_keydup, hashtable_item_strdup,
|
|
(HASHFREEFN) uh_keyfree, hashtable_item_free);
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Add a new MySQL user to the user table. The user name must be unique
|
|
*
|
|
* @param users The users table
|
|
* @param user The user name
|
|
* @param auth The authentication data
|
|
* @return The number of users added to the table
|
|
*/
|
|
int
|
|
mysql_users_add(USERS *users, MYSQL_USER_HOST *key, char *auth)
|
|
{
|
|
int add;
|
|
|
|
if (key == NULL || key->user == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
atomic_add(&users->stats.n_adds, 1);
|
|
add = hashtable_add(users->data, key, auth);
|
|
atomic_add(&users->stats.n_entries, add);
|
|
|
|
return add;
|
|
}
|
|
|
|
/**
|
|
* Fetch the authentication data for a particular user from the users table
|
|
*
|
|
* @param users The MySQL users table
|
|
* @param key The key with user@host
|
|
* @return The authentication data or NULL on error
|
|
*/
|
|
char *mysql_users_fetch(USERS *users, MYSQL_USER_HOST *key)
|
|
{
|
|
if (key == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
atomic_add(&users->stats.n_fetches, 1);
|
|
return hashtable_fetch(users->data, key);
|
|
}
|
|
|
|
/**
|
|
* The hash function we use for storing MySQL users as: users@hosts.
|
|
* Currently only IPv4 addresses are supported
|
|
*
|
|
* @param key The key value, i.e. username@host (IPv4)
|
|
* @return The hash key
|
|
*/
|
|
|
|
static int uh_hfun(const void* key)
|
|
{
|
|
const MYSQL_USER_HOST *hu = (const MYSQL_USER_HOST *) key;
|
|
|
|
if (key == NULL || hu == NULL || hu->user == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return (*hu->user + * (hu->user + 1) +
|
|
(unsigned int) (hu->ipv4.sin_addr.s_addr & 0xFF000000 / (256 * 256 * 256)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The compare function we use for compare MySQL users as: users@hosts.
|
|
* Currently only IPv4 addresses are supported
|
|
*
|
|
* @param key1 The key value, i.e. username@host (IPv4)
|
|
* @param key2 The key value, i.e. username@host (IPv4)
|
|
* @return The compare value
|
|
*/
|
|
|
|
static int uh_cmpfun(const void* v1, const void* v2)
|
|
{
|
|
const MYSQL_USER_HOST *hu1 = (const MYSQL_USER_HOST *) v1;
|
|
const MYSQL_USER_HOST *hu2 = (const MYSQL_USER_HOST *) v2;
|
|
|
|
if (v1 == NULL || v2 == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (hu1->user == NULL || hu2->user == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/** If the stored user has the unmodified address stored, that means we were not able
|
|
* to resolve it at the time we loaded the users. We need to check if the
|
|
* address contains wildcards and if the user's address matches that. */
|
|
|
|
const bool wildcard_host = strlen(hu2->hostname) > 0 && strlen(hu1->hostname) > 0;
|
|
|
|
if ((strcmp(hu1->user, hu2->user) == 0) &&
|
|
/** Check for wildcard hostnames */
|
|
((wildcard_host && host_matches_singlechar_wildcard(hu1->hostname, hu2->hostname)) ||
|
|
/** If no wildcard hostname is stored, check for network address. */
|
|
(!wildcard_host && (hu1->ipv4.sin_addr.s_addr == hu2->ipv4.sin_addr.s_addr) &&
|
|
(hu1->netmask >= hu2->netmask))))
|
|
{
|
|
/* if no database name was passed, auth is ok */
|
|
if (hu1->resource == NULL || (hu1->resource && !strlen(hu1->resource)))
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
/* (1) check for no database grants at all and deny auth */
|
|
if (hu2->resource == NULL)
|
|
{
|
|
return 1;
|
|
}
|
|
/* (2) check for ANY database grant and allow auth */
|
|
if (!strlen(hu2->resource))
|
|
{
|
|
return 0;
|
|
}
|
|
/* (3) check for database name specific grant and allow auth */
|
|
if (hu1->resource && hu2->resource && strcmp(hu1->resource,
|
|
hu2->resource) == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (hu2->resource && strlen(hu2->resource) && strchr(hu2->resource, '%') != NULL)
|
|
{
|
|
regex_t re;
|
|
char db[MYSQL_DATABASE_MAXLEN * 2 + 1];
|
|
strcpy(db, hu2->resource);
|
|
int len = strlen(db);
|
|
char* ptr = strrchr(db, '%');
|
|
|
|
if (ptr == NULL)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
while (ptr)
|
|
{
|
|
memmove(ptr + 1, ptr, (len - (ptr - db)) + 1);
|
|
*ptr = '.';
|
|
*(ptr + 1) = '*';
|
|
len = strlen(db);
|
|
ptr = strrchr(db, '%');
|
|
}
|
|
|
|
if ((regcomp(&re, db, REG_ICASE | REG_NOSUB)))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (regexec(&re, hu1->resource, 0, NULL, 0) == 0)
|
|
{
|
|
regfree(&re);
|
|
return 0;
|
|
}
|
|
regfree(&re);
|
|
}
|
|
|
|
/* no matches, deny auth */
|
|
return 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*The key dup function we use for duplicate the users@hosts.
|
|
*
|
|
* @param key The key value, i.e. username@host ip4/ip6 data
|
|
*/
|
|
|
|
static MYSQL_USER_HOST *uh_keydup(const MYSQL_USER_HOST* key)
|
|
{
|
|
if ((key == NULL) || (key->user == NULL))
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
MYSQL_USER_HOST *rval = (MYSQL_USER_HOST *) MXS_CALLOC(1, sizeof(MYSQL_USER_HOST));
|
|
char* user = MXS_STRDUP(key->user);
|
|
char* resource = key->resource ? MXS_STRDUP(key->resource) : NULL;
|
|
|
|
if (!user || !rval || (key->resource && !resource))
|
|
{
|
|
MXS_FREE(rval);
|
|
MXS_FREE(user);
|
|
MXS_FREE(resource);
|
|
return NULL;
|
|
}
|
|
|
|
rval->user = user;
|
|
rval->ipv4 = key->ipv4;
|
|
rval->netmask = key->netmask;
|
|
rval->resource = resource;
|
|
strcpy(rval->hostname, key->hostname);
|
|
|
|
return (void *) rval;
|
|
}
|
|
|
|
/**
|
|
* The key free function we use for freeing the users@hosts data
|
|
*
|
|
* @param key The key value, i.e. username@host ip4 data
|
|
*/
|
|
static void uh_keyfree(MYSQL_USER_HOST* key)
|
|
{
|
|
if (key)
|
|
{
|
|
MXS_FREE(key->user);
|
|
MXS_FREE(key->resource);
|
|
MXS_FREE(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format the mysql user as user@host
|
|
* The returned memory must be freed by the caller
|
|
*
|
|
* @param data Input data
|
|
* @return the MySQL user@host
|
|
*/
|
|
static char *mysql_format_user_entry(void *data)
|
|
{
|
|
MYSQL_USER_HOST *entry;
|
|
char *mysql_user;
|
|
/* the returned user string is "USER" + "@" + "HOST" + '\0' */
|
|
int mysql_user_len = MYSQL_USER_MAXLEN + 1 + INET_ADDRSTRLEN + 10 +
|
|
MYSQL_USER_MAXLEN + 1;
|
|
|
|
if (data == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
entry = (MYSQL_USER_HOST *) data;
|
|
|
|
mysql_user = (char *) MXS_CALLOC(mysql_user_len, sizeof(char));
|
|
|
|
if (mysql_user == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* format user@host based on wildcards */
|
|
|
|
if (entry->ipv4.sin_addr.s_addr == INADDR_ANY && entry->netmask == 0)
|
|
{
|
|
snprintf(mysql_user, mysql_user_len - 1, "%s@%%", entry->user);
|
|
}
|
|
else if ((entry->ipv4.sin_addr.s_addr & 0xFF000000) == 0 && entry->netmask == 24)
|
|
{
|
|
snprintf(mysql_user, mysql_user_len - 1, "%s@%i.%i.%i.%%", entry->user,
|
|
entry->ipv4.sin_addr.s_addr & 0x000000FF,
|
|
(entry->ipv4.sin_addr.s_addr & 0x0000FF00) / (256),
|
|
(entry->ipv4.sin_addr.s_addr & 0x00FF0000) / (256 * 256));
|
|
}
|
|
else if ((entry->ipv4.sin_addr.s_addr & 0xFFFF0000) == 0 && entry->netmask == 16)
|
|
{
|
|
snprintf(mysql_user, mysql_user_len - 1, "%s@%i.%i.%%.%%", entry->user,
|
|
entry->ipv4.sin_addr.s_addr & 0x000000FF,
|
|
(entry->ipv4.sin_addr.s_addr & 0x0000FF00) / (256));
|
|
}
|
|
else if ((entry->ipv4.sin_addr.s_addr & 0xFFFFFF00) == 0 && entry->netmask == 8)
|
|
{
|
|
snprintf(mysql_user, mysql_user_len - 1, "%s@%i.%%.%%.%%", entry->user,
|
|
entry->ipv4.sin_addr.s_addr & 0x000000FF);
|
|
}
|
|
else if (entry->netmask == 32)
|
|
{
|
|
strcpy(mysql_user, entry->user);
|
|
strcat(mysql_user, "@");
|
|
inet_ntop(AF_INET, &(entry->ipv4).sin_addr, mysql_user + strlen(mysql_user),
|
|
INET_ADDRSTRLEN);
|
|
}
|
|
else
|
|
{
|
|
snprintf(mysql_user, MYSQL_USER_MAXLEN - 5, "Err: %s", entry->user);
|
|
strcat(mysql_user, "@");
|
|
inet_ntop(AF_INET, &(entry->ipv4).sin_addr, mysql_user + strlen(mysql_user),
|
|
INET_ADDRSTRLEN);
|
|
}
|
|
|
|
return mysql_user;
|
|
}
|
|
|
|
/**
|
|
* Remove the resources table
|
|
*
|
|
* @param resources The resources table to remove
|
|
*/
|
|
static void
|
|
resource_free(HASHTABLE *resources)
|
|
{
|
|
if (resources)
|
|
{
|
|
hashtable_free(resources);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allocate a MySQL database names table
|
|
*
|
|
* @return The database names table
|
|
*/
|
|
static HASHTABLE *
|
|
resource_alloc()
|
|
{
|
|
HASHTABLE *resources;
|
|
|
|
if ((resources = hashtable_alloc(10, hashtable_item_strhash, hashtable_item_strcmp)) == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
hashtable_memory_fns(resources,
|
|
hashtable_item_strdup, hashtable_item_strdup,
|
|
hashtable_item_free, hashtable_item_free);
|
|
|
|
return resources;
|
|
}
|
|
|
|
/**
|
|
* Add a new MySQL database name to the resources table. The resource name must
|
|
* be unique.
|
|
* @param resources The resources table
|
|
* @param key The resource name
|
|
* @param value The value for resource (not used)
|
|
* @return The number of resources dded to the table
|
|
*/
|
|
static int
|
|
resource_add(HASHTABLE *resources, char *key, char *value)
|
|
{
|
|
return hashtable_add(resources, key, value);
|
|
}
|
|
|
|
/**
|
|
* Fetch a particular database name from the resources table
|
|
*
|
|
* @param resources The MySQL database names table
|
|
* @param key The database name to fetch
|
|
* @return The database esists or NULL if not found
|
|
*/
|
|
static void *
|
|
resource_fetch(HASHTABLE *resources, char *key)
|
|
{
|
|
return hashtable_fetch(resources, key);
|
|
}
|
|
|
|
/**
|
|
* Normalize hostname with % wildcards to a valid IP string.
|
|
*
|
|
* Valid input values:
|
|
* a.b.c.d, a.b.c.%, a.b.%.%, a.%.%.%
|
|
* Short formats a.% and a.%.% are both converted to a.%.%.%
|
|
* Short format a.b.% is converted to a.b.%.%
|
|
*
|
|
* Last host byte is set to 1, avoiding setipadress() failure
|
|
*
|
|
* @param input_host The hostname with possible % wildcards
|
|
* @param output_host The normalized hostname (buffer must be preallocated)
|
|
* @return The calculated netmask or -1 on failure
|
|
*/
|
|
static int normalize_hostname(const char *input_host, char *output_host)
|
|
{
|
|
int netmask, bytes, bits = 0, found_wildcard = 0;
|
|
char *p, *lasts, *tmp;
|
|
int useorig = 0;
|
|
|
|
output_host[0] = 0;
|
|
bytes = 0;
|
|
|
|
tmp = MXS_STRDUP(input_host);
|
|
|
|
if (tmp == NULL)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
p = strtok_r(tmp, ".", &lasts);
|
|
while (p != NULL)
|
|
{
|
|
|
|
if (strcmp(p, "%"))
|
|
{
|
|
if (!isdigit(*p))
|
|
{
|
|
useorig = 1;
|
|
}
|
|
|
|
strcat(output_host, p);
|
|
bits += 8;
|
|
}
|
|
else if (bytes == 3)
|
|
{
|
|
found_wildcard = 1;
|
|
strcat(output_host, "1");
|
|
}
|
|
else
|
|
{
|
|
found_wildcard = 1;
|
|
strcat(output_host, "0");
|
|
}
|
|
bytes++;
|
|
p = strtok_r(NULL, ".", &lasts);
|
|
if (p)
|
|
{
|
|
strcat(output_host, ".");
|
|
}
|
|
}
|
|
if (found_wildcard)
|
|
{
|
|
netmask = bits;
|
|
while (bytes++ < 4)
|
|
{
|
|
if (bytes == 4)
|
|
{
|
|
strcat(output_host, ".1");
|
|
}
|
|
else
|
|
{
|
|
strcat(output_host, ".0");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
netmask = 32;
|
|
}
|
|
|
|
if (useorig == 1)
|
|
{
|
|
netmask = 32;
|
|
strcpy(output_host, input_host);
|
|
}
|
|
|
|
MXS_FREE(tmp);
|
|
|
|
return netmask;
|
|
}
|
|
|
|
/**
|
|
* Returns a MYSQL object suitably configured.
|
|
*
|
|
* @return An object or NULL if something fails.
|
|
*/
|
|
MYSQL *gw_mysql_init()
|
|
{
|
|
MYSQL* con = mysql_init(NULL);
|
|
|
|
if (con)
|
|
{
|
|
if (gw_mysql_set_timeouts(con) == 0)
|
|
{
|
|
// MYSQL_OPT_USE_REMOTE_CONNECTION must be set if the embedded
|
|
// libary is used. With Connector-C (at least 2.2.1) the call
|
|
// fails.
|
|
#if !defined(LIBMARIADB)
|
|
if (mysql_options(con, MYSQL_OPT_USE_REMOTE_CONNECTION, NULL) != 0)
|
|
{
|
|
MXS_ERROR("Failed to set external connection. "
|
|
"It is needed for backend server connections.");
|
|
mysql_close(con);
|
|
con = NULL;
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Failed to set timeout values for backend connection.");
|
|
mysql_close(con);
|
|
con = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("mysql_init: %s", mysql_error(NULL));
|
|
}
|
|
|
|
return con;
|
|
}
|
|
|
|
/**
|
|
* Set read, write and connect timeout values for MySQL database connection.
|
|
*
|
|
* @param handle MySQL handle
|
|
* @param read_timeout Read timeout value in seconds
|
|
* @param write_timeout Write timeout value in seconds
|
|
* @param connect_timeout Connect timeout value in seconds
|
|
*
|
|
* @return 0 if succeed, 1 if failed
|
|
*/
|
|
static int gw_mysql_set_timeouts(MYSQL* handle)
|
|
{
|
|
int rc;
|
|
|
|
GATEWAY_CONF* cnf = config_get_global_options();
|
|
|
|
if ((rc = mysql_options(handle, MYSQL_OPT_READ_TIMEOUT,
|
|
(void *) &cnf->auth_read_timeout)))
|
|
{
|
|
MXS_ERROR("Failed to set read timeout for backend connection.");
|
|
goto retblock;
|
|
}
|
|
|
|
if ((rc = mysql_options(handle, MYSQL_OPT_CONNECT_TIMEOUT,
|
|
(void *) &cnf->auth_conn_timeout)))
|
|
{
|
|
MXS_ERROR("Failed to set connect timeout for backend connection.");
|
|
goto retblock;
|
|
}
|
|
|
|
if ((rc = mysql_options(handle, MYSQL_OPT_WRITE_TIMEOUT,
|
|
(void *) &cnf->auth_write_timeout)))
|
|
{
|
|
MXS_ERROR("Failed to set write timeout for backend connection.");
|
|
goto retblock;
|
|
}
|
|
|
|
retblock:
|
|
return rc;
|
|
}
|
|
|
|
/*
|
|
* Serialise a key for the dbusers hashtable to a file
|
|
*
|
|
* @param fd File descriptor to write to
|
|
* @param key The key to write
|
|
* @return 0 on error, 1 if the key was written
|
|
*/
|
|
static int
|
|
dbusers_keywrite(int fd, void *key)
|
|
{
|
|
MYSQL_USER_HOST *dbkey = (MYSQL_USER_HOST *) key;
|
|
int tmp;
|
|
|
|
tmp = strlen(dbkey->user);
|
|
if (write(fd, &tmp, sizeof(tmp)) != sizeof(tmp))
|
|
{
|
|
return 0;
|
|
}
|
|
if (write(fd, dbkey->user, tmp) != tmp)
|
|
{
|
|
return 0;
|
|
}
|
|
if (write(fd, &dbkey->ipv4, sizeof(dbkey->ipv4)) != sizeof(dbkey->ipv4))
|
|
{
|
|
return 0;
|
|
}
|
|
if (write(fd, &dbkey->netmask, sizeof(dbkey->netmask)) != sizeof(dbkey->netmask))
|
|
{
|
|
return 0;
|
|
}
|
|
if (dbkey->resource)
|
|
{
|
|
tmp = strlen(dbkey->resource);
|
|
if (write(fd, &tmp, sizeof(tmp)) != sizeof(tmp))
|
|
{
|
|
return 0;
|
|
}
|
|
if (write(fd, dbkey->resource, tmp) != tmp)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else // NULL is valid, so represent with a length of -1
|
|
{
|
|
tmp = -1;
|
|
if (write(fd, &tmp, sizeof(tmp)) != sizeof(tmp))
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Serialise a value for the dbusers hashtable to a file
|
|
*
|
|
* @param fd File descriptor to write to
|
|
* @param value The value to write
|
|
* @return 0 on error, 1 if the value was written
|
|
*/
|
|
static int
|
|
dbusers_valuewrite(int fd, void *value)
|
|
{
|
|
int tmp;
|
|
|
|
tmp = strlen(value);
|
|
if (write(fd, &tmp, sizeof(tmp)) != sizeof(tmp))
|
|
{
|
|
return 0;
|
|
}
|
|
if (write(fd, value, tmp) != tmp)
|
|
{
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Unserialise a key for the dbusers hashtable from a file
|
|
*
|
|
* @param fd File descriptor to read from
|
|
* @return Pointer to the new key or NULL on error
|
|
*/
|
|
static void *
|
|
dbusers_keyread(int fd)
|
|
{
|
|
MYSQL_USER_HOST *dbkey;
|
|
|
|
if ((dbkey = (MYSQL_USER_HOST *) MXS_MALLOC(sizeof(MYSQL_USER_HOST))) == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
*dbkey->hostname = '\0';
|
|
|
|
int user_size;
|
|
if (read(fd, &user_size, sizeof(user_size)) != sizeof(user_size))
|
|
{
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
if ((dbkey->user = (char *) MXS_MALLOC(user_size + 1)) == NULL)
|
|
{
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
if (read(fd, dbkey->user, user_size) != user_size)
|
|
{
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
dbkey->user[user_size] = 0; // NULL Terminate
|
|
if (read(fd, &dbkey->ipv4, sizeof(dbkey->ipv4)) != sizeof(dbkey->ipv4))
|
|
{
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
if (read(fd, &dbkey->netmask, sizeof(dbkey->netmask)) != sizeof(dbkey->netmask))
|
|
{
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
|
|
int res_size;
|
|
if (read(fd, &res_size, sizeof(res_size)) != sizeof(res_size))
|
|
{
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
else if (res_size != -1)
|
|
{
|
|
if ((dbkey->resource = (char *) MXS_MALLOC(res_size + 1)) == NULL)
|
|
{
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
if (read(fd, dbkey->resource, res_size) != res_size)
|
|
{
|
|
MXS_FREE(dbkey->resource);
|
|
MXS_FREE(dbkey->user);
|
|
MXS_FREE(dbkey);
|
|
return NULL;
|
|
}
|
|
dbkey->resource[res_size] = 0; // NULL Terminate
|
|
}
|
|
else // NULL is valid, so represent with a length of -1
|
|
{
|
|
dbkey->resource = NULL;
|
|
}
|
|
return (void *) dbkey;
|
|
}
|
|
|
|
/**
|
|
* Unserialise a value for the dbusers hashtable from a file
|
|
*
|
|
* @param fd File descriptor to read from
|
|
* @return Return the new value data or NULL on error
|
|
*/
|
|
static void *
|
|
dbusers_valueread(int fd)
|
|
{
|
|
char *value;
|
|
int tmp;
|
|
|
|
if (read(fd, &tmp, sizeof(tmp)) != sizeof(tmp))
|
|
{
|
|
return NULL;
|
|
}
|
|
if ((value = (char *) MXS_MALLOC(tmp + 1)) == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
if (read(fd, value, tmp) != tmp)
|
|
{
|
|
MXS_FREE(value);
|
|
return NULL;
|
|
}
|
|
value[tmp] = 0;
|
|
return (void *) value;
|
|
}
|
|
|
|
/**
|
|
* Save the dbusers data to a hashtable file
|
|
*
|
|
* @param users The hashtable that stores the user data
|
|
* @param filename The filename to save the data in
|
|
* @return The number of entries saved
|
|
*/
|
|
int
|
|
dbusers_save(USERS *users, const char *filename)
|
|
{
|
|
return hashtable_save(users->data, filename, dbusers_keywrite, dbusers_valuewrite);
|
|
}
|
|
|
|
/**
|
|
* Load the dbusers data from a saved hashtable file
|
|
*
|
|
* @param users The hashtable that stores the user data
|
|
* @param filename The filename to laod the data from
|
|
* @return The number of entries loaded
|
|
*/
|
|
int
|
|
dbusers_load(USERS *users, const char *filename)
|
|
{
|
|
return hashtable_load(users->data, filename, dbusers_keyread, dbusers_valueread);
|
|
}
|
|
|
|
/**
|
|
* Check if the database name contains a wildcard character
|
|
* @param str Database grant
|
|
* @return 1 if the name contains the '%' wildcard character, 0 if it does not
|
|
*/
|
|
static int wildcard_db_grant(char* str)
|
|
{
|
|
char* ptr = str;
|
|
|
|
while (ptr && *ptr != '\0')
|
|
{
|
|
if (*ptr == '%')
|
|
{
|
|
return 1;
|
|
}
|
|
ptr++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param users Pointer to USERS struct
|
|
* @param name Username of the client
|
|
* @param host Host address of the client
|
|
* @param password Client password
|
|
* @param anydb If the user has access to all databases
|
|
* @param db Database, in wildcard form
|
|
* @param hash Hashtable with all database names
|
|
* @return number of unique grants generated from wildcard database name
|
|
*/
|
|
static int add_wildcard_users(USERS *users, char* name, char* host, char* password,
|
|
char* anydb, char* db, HASHTABLE* hash)
|
|
{
|
|
HASHITERATOR* iter;
|
|
HASHTABLE* ht = hash;
|
|
char *restr, *ptr, *value;
|
|
int len, err, rval = 0;
|
|
char errbuf[1024];
|
|
regex_t re;
|
|
|
|
if (db == NULL || hash == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if ((restr = MXS_MALLOC(sizeof(char) * strlen(db) * 2)) == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
strcpy(restr, db);
|
|
|
|
len = strlen(restr);
|
|
ptr = strchr(restr, '%');
|
|
|
|
if (ptr == NULL)
|
|
{
|
|
MXS_FREE(restr);
|
|
return 0;
|
|
}
|
|
|
|
while (ptr)
|
|
{
|
|
memmove(ptr + 1, ptr, (len - (ptr - restr)) + 1);
|
|
*ptr++ = '.';
|
|
*ptr = '*';
|
|
len = strlen(restr);
|
|
ptr = strchr(restr, '%');
|
|
}
|
|
|
|
if ((err = regcomp(&re, restr, REG_ICASE | REG_NOSUB)))
|
|
{
|
|
regerror(err, &re, errbuf, 1024);
|
|
MXS_ERROR("Failed to compile regex when resolving wildcard database grants: %s",
|
|
errbuf);
|
|
MXS_FREE(restr);
|
|
return 0;
|
|
}
|
|
|
|
iter = hashtable_iterator(ht);
|
|
|
|
while (iter && (value = hashtable_next(iter)))
|
|
{
|
|
if (regexec(&re, value, 0, NULL, 0) == 0)
|
|
{
|
|
rval += add_mysql_users_with_host_ipv4(users, name, host, password,
|
|
anydb, value);
|
|
}
|
|
}
|
|
|
|
hashtable_iterator_free(iter);
|
|
regfree(&re);
|
|
MXS_FREE(restr);
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Check service permissions on one server
|
|
*
|
|
* @param server Server to check
|
|
* @param user Username
|
|
* @param password Password
|
|
* @return True if the service permissions are OK, false if one or more permissions
|
|
* are missing.
|
|
*/
|
|
static bool check_server_permissions(SERVICE *service, SERVER* server,
|
|
const char* user, const char* password)
|
|
{
|
|
MYSQL *mysql = gw_mysql_init();
|
|
|
|
if (mysql == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
GATEWAY_CONF* cnf = config_get_global_options();
|
|
mysql_options(mysql, MYSQL_OPT_READ_TIMEOUT, &cnf->auth_read_timeout);
|
|
mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, &cnf->auth_conn_timeout);
|
|
mysql_options(mysql, MYSQL_OPT_WRITE_TIMEOUT, &cnf->auth_write_timeout);
|
|
|
|
if (mxs_mysql_real_connect(mysql, server, user, password) == NULL)
|
|
{
|
|
int my_errno = mysql_errno(mysql);
|
|
|
|
MXS_ERROR("[%s] Failed to connect to server '%s' (%s:%d) when"
|
|
" checking authentication user credentials and permissions: %d %s",
|
|
service->name, server->unique_name, server->name, server->port,
|
|
my_errno, mysql_error(mysql));
|
|
|
|
mysql_close(mysql);
|
|
return my_errno != ER_ACCESS_DENIED_ERROR;
|
|
}
|
|
|
|
if (server->server_string == NULL)
|
|
{
|
|
const char *server_string = mysql_get_server_info(mysql);
|
|
server_set_version_string(server, server_string);
|
|
}
|
|
|
|
char query[MAX_QUERY_STR_LEN];
|
|
const char* query_pw = strstr(server->server_string, "5.7.") ?
|
|
MYSQL57_PASSWORD : MYSQL_PASSWORD;
|
|
bool rval = true;
|
|
snprintf(query, sizeof(query), "SELECT user, host, %s, Select_priv FROM mysql.user limit 1", query_pw);
|
|
|
|
if (mysql_query(mysql, query) != 0)
|
|
{
|
|
if (mysql_errno(mysql) == ER_TABLEACCESS_DENIED_ERROR)
|
|
{
|
|
MXS_ERROR("[%s] User '%s' is missing SELECT privileges"
|
|
" on mysql.user table. MySQL error message: %s",
|
|
service->name, user, mysql_error(mysql));
|
|
rval = false;
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("[%s] Failed to query from mysql.user table."
|
|
" MySQL error message: %s", service->name, mysql_error(mysql));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
MYSQL_RES* res = mysql_use_result(mysql);
|
|
if (res == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Result retrieval failed when checking for permissions to "
|
|
"the mysql.user table: %s", service->name, mysql_error(mysql));
|
|
}
|
|
else
|
|
{
|
|
mysql_free_result(res);
|
|
}
|
|
}
|
|
|
|
if (mysql_query(mysql, "SELECT user, host, db FROM mysql.db limit 1") != 0)
|
|
{
|
|
if (mysql_errno(mysql) == ER_TABLEACCESS_DENIED_ERROR)
|
|
{
|
|
MXS_WARNING("[%s] User '%s' is missing SELECT privileges on mysql.db table. "
|
|
"Database name will be ignored in authentication. "
|
|
"MySQL error message: %s", service->name, user, mysql_error(mysql));
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("[%s] Failed to query from mysql.db table. MySQL error message: %s",
|
|
service->name, mysql_error(mysql));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MYSQL_RES* res = mysql_use_result(mysql);
|
|
if (res == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Result retrieval failed when checking for permissions "
|
|
"to the mysql.db table: %s", service->name, mysql_error(mysql));
|
|
}
|
|
else
|
|
{
|
|
mysql_free_result(res);
|
|
}
|
|
}
|
|
|
|
if (mysql_query(mysql, "SELECT user, host, db FROM mysql.tables_priv limit 1") != 0)
|
|
{
|
|
if (mysql_errno(mysql) == ER_TABLEACCESS_DENIED_ERROR)
|
|
{
|
|
MXS_WARNING("[%s] User '%s' is missing SELECT privileges on mysql.tables_priv table. "
|
|
"Database name will be ignored in authentication. "
|
|
"MySQL error message: %s", service->name, user, mysql_error(mysql));
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("[%s] Failed to query from mysql.tables_priv table. "
|
|
"MySQL error message: %s", service->name, mysql_error(mysql));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MYSQL_RES* res = mysql_use_result(mysql);
|
|
if (res == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Result retrieval failed when checking for permissions "
|
|
"to the mysql.tables_priv table: %s", service->name, mysql_error(mysql));
|
|
}
|
|
else
|
|
{
|
|
mysql_free_result(res);
|
|
}
|
|
}
|
|
|
|
mysql_close(mysql);
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if the service user has all required permissions to operate properly.
|
|
*
|
|
* This checks for SELECT permissions on mysql.user, mysql.db and mysql.tables_priv
|
|
* tables and for SHOW DATABASES permissions. If permissions are not adequate,
|
|
* an error message is logged and the service is not started.
|
|
*
|
|
* @param service Service to inspect
|
|
* @return True if service permissions are correct on at least one server, false
|
|
* if permissions are missing or if an error occurred.
|
|
*/
|
|
bool check_service_permissions(SERVICE* service)
|
|
{
|
|
if (is_internal_service(service->routerModule))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (service->dbref == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Service is missing the servers parameter.", service->name);
|
|
return false;
|
|
}
|
|
|
|
char *user, *password;
|
|
|
|
if (serviceGetUser(service, &user, &password) == 0)
|
|
{
|
|
MXS_ERROR("[%s] Service is missing the user credentials for authentication.",
|
|
service->name);
|
|
return false;
|
|
}
|
|
|
|
char *dpasswd = decryptPassword(password);
|
|
bool rval = false;
|
|
|
|
for (SERVER_REF *server = service->dbref; server; server = server->next)
|
|
{
|
|
if (check_server_permissions(service, server->server, user, dpasswd))
|
|
{
|
|
rval = true;
|
|
}
|
|
}
|
|
|
|
free(dpasswd);
|
|
|
|
return rval;
|
|
}
|