Merge branch '2.0' into develop
This commit is contained in:
@ -37,13 +37,25 @@
|
||||
* 23/07/13 Mark Riddoch Addition of error mechanism to add user
|
||||
* 23/05/16 Massimiliano Pinto admin_add_user and admin_remove_user
|
||||
* no longer accept password parameter
|
||||
* 02/09/16 Johan Wikman Enabled Linux accounts and MaxScale users
|
||||
*
|
||||
* @endverbatim
|
||||
*/
|
||||
static USERS *loadUsers();
|
||||
static void initialise();
|
||||
|
||||
static USERS *users = NULL;
|
||||
static USERS *loadLinuxUsers();
|
||||
static USERS *loadInetUsers();
|
||||
|
||||
static const char *admin_add_user(USERS** pusers, const char* fname,
|
||||
const char* uname, const char* password);
|
||||
static const char* admin_remove_user(USERS *users, const char* fname,
|
||||
const char *uname, const char *passwd);
|
||||
static bool admin_search_user(USERS *users, const char *uname);
|
||||
|
||||
|
||||
|
||||
static USERS *linux_users = NULL;
|
||||
static USERS *inet_users = NULL;
|
||||
static int admin_init = 0;
|
||||
|
||||
static char *ADMIN_ERR_NOMEM = "Out of memory";
|
||||
@ -61,7 +73,11 @@ static char *ADMIN_SUCCESS = NULL;
|
||||
|
||||
static const int LINELEN = 80;
|
||||
|
||||
static const char USERS_FILE_NAME[] = "maxadmin-users";
|
||||
static const char LINUX_USERS_FILE_NAME[] = "maxadmin-users";
|
||||
static const char INET_USERS_FILE_NAME[] = "passwd";
|
||||
|
||||
static const char INET_DEFAULT_USERNAME[] = "admin";
|
||||
static const char INET_DEFAULT_PASSWORD[] = "mariadb";
|
||||
|
||||
/**
|
||||
* Admin Users initialisation
|
||||
@ -75,125 +91,15 @@ initialise()
|
||||
}
|
||||
|
||||
admin_init = 1;
|
||||
users = loadUsers();
|
||||
linux_users = loadLinuxUsers();
|
||||
inet_users = loadInetUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a username and password
|
||||
*
|
||||
* @param username Username to verify
|
||||
* @param password Password to verify
|
||||
* @return Non-zero if the username/password combination is valid
|
||||
*/
|
||||
int
|
||||
admin_verify(char *username, char *password)
|
||||
{
|
||||
char *pw;
|
||||
|
||||
initialise();
|
||||
if (users == NULL)
|
||||
{
|
||||
if (strcmp(username, "admin") == 0 && strcmp(password, "mariadb") == 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((pw = users_fetch(users, username)) == NULL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
struct crypt_data cdata;
|
||||
cdata.initialized = 0;
|
||||
if (strcmp(pw, crypt_r(password, ADMIN_SALT, &cdata)) == 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the admin users
|
||||
*
|
||||
* @return Table of users
|
||||
*/
|
||||
static USERS *
|
||||
loadUsers()
|
||||
{
|
||||
USERS *rval;
|
||||
FILE *fp;
|
||||
char fname[PATH_MAX], *home;
|
||||
char uname[80];
|
||||
int added_users = 0;
|
||||
|
||||
initialise();
|
||||
snprintf(fname, sizeof(fname), "%s/%s", get_datadir(), USERS_FILE_NAME);
|
||||
if ((fp = fopen(fname, "r")) == NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
if ((rval = users_alloc()) == NULL)
|
||||
{
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
while (fgets(uname, sizeof(uname), fp))
|
||||
{
|
||||
char *nl = strchr(uname, '\n');
|
||||
|
||||
if (nl)
|
||||
{
|
||||
*nl = '\0';
|
||||
}
|
||||
else if (!feof(fp))
|
||||
{
|
||||
MXS_ERROR("Line length exceeds %d characters, possible corrupted "
|
||||
"'passwd' file in: %s", LINELEN, fname);
|
||||
users_free(rval);
|
||||
rval = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
char *tmp_ptr = strchr(uname, ':');
|
||||
if (tmp_ptr)
|
||||
{
|
||||
*tmp_ptr = '\0';
|
||||
MXS_WARNING("Found user '%s' with password. "
|
||||
"This user might not be compatible with new maxadmin in MaxScale 2.0. "
|
||||
"Remove it with \"remove user %s\" through MaxAdmin", uname, uname);
|
||||
}
|
||||
if (users_add(rval, uname, ""))
|
||||
{
|
||||
added_users++;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
if (!added_users)
|
||||
{
|
||||
users_free(rval);
|
||||
rval = NULL;
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user
|
||||
*
|
||||
* @param uname Name of the new user
|
||||
* @return NULL on success or an error string on failure
|
||||
*/
|
||||
char *
|
||||
admin_add_user(char *uname)
|
||||
static const char *admin_add_user(USERS** pusers, const char* fname,
|
||||
const char* uname, const char* password)
|
||||
{
|
||||
FILE *fp;
|
||||
char fname[PATH_MAX], *home;
|
||||
|
||||
initialise();
|
||||
char path[PATH_MAX], *home;
|
||||
|
||||
if (access(get_datadir(), F_OK) != 0)
|
||||
{
|
||||
@ -203,51 +109,51 @@ admin_add_user(char *uname)
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(fname, sizeof(fname), "%s/%s", get_datadir(), USERS_FILE_NAME);
|
||||
if (users == NULL)
|
||||
snprintf(path, sizeof(path), "%s/%s", get_datadir(), fname);
|
||||
if (*pusers == NULL)
|
||||
{
|
||||
MXS_NOTICE("Create initial password file.");
|
||||
|
||||
if ((users = users_alloc()) == NULL)
|
||||
if ((*pusers = users_alloc()) == NULL)
|
||||
{
|
||||
return ADMIN_ERR_NOMEM;
|
||||
}
|
||||
if ((fp = fopen(fname, "w")) == NULL)
|
||||
if ((fp = fopen(path, "w")) == NULL)
|
||||
{
|
||||
MXS_ERROR("Unable to create password file %s.", fname);
|
||||
MXS_ERROR("Unable to create password file %s.", path);
|
||||
return ADMIN_ERR_PWDFILEOPEN;
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
if (users_fetch(users, uname) != NULL)
|
||||
if (users_fetch(*pusers, (char*)uname) != NULL) // TODO: Make users const correct.
|
||||
{
|
||||
return ADMIN_ERR_DUPLICATE;
|
||||
}
|
||||
users_add(users, uname, "");
|
||||
if ((fp = fopen(fname, "a")) == NULL)
|
||||
users_add(*pusers, (char*)uname, password ? (char*)password : ""); // TODO: Make users const correct.
|
||||
if ((fp = fopen(path, "a")) == NULL)
|
||||
{
|
||||
MXS_ERROR("Unable to append to password file %s.", fname);
|
||||
MXS_ERROR("Unable to append to password file %s.", path);
|
||||
return ADMIN_ERR_FILEAPPEND;
|
||||
}
|
||||
fprintf(fp, "%s\n", uname);
|
||||
if (password)
|
||||
{
|
||||
fprintf(fp, "%s:%s\n", uname, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(fp, "%s\n", uname);
|
||||
}
|
||||
fclose(fp);
|
||||
return ADMIN_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove maxscale user from in-memory structure and from password file
|
||||
*
|
||||
* @param uname Name of the new user
|
||||
* @return NULL on success or an error string on failure
|
||||
*/
|
||||
char* admin_remove_user(
|
||||
char* uname)
|
||||
static const char* admin_remove_user(USERS *users, const char* fname,
|
||||
const char *uname, const char *passwd)
|
||||
{
|
||||
FILE* fp;
|
||||
FILE* fp_tmp;
|
||||
char fname[PATH_MAX];
|
||||
char fname_tmp[PATH_MAX];
|
||||
char path[PATH_MAX];
|
||||
char path_tmp[PATH_MAX];
|
||||
char* home;
|
||||
char fusr[LINELEN];
|
||||
char fpwd[LINELEN];
|
||||
@ -260,43 +166,53 @@ char* admin_remove_user(
|
||||
return ADMIN_ERR_DELROOT;
|
||||
}
|
||||
|
||||
if (!admin_search_user(uname))
|
||||
if (!admin_search_user(users, uname))
|
||||
{
|
||||
MXS_ERROR("Couldn't find user %s. Removing user failed.", uname);
|
||||
return ADMIN_ERR_USERNOTFOUND;
|
||||
}
|
||||
|
||||
if (passwd)
|
||||
{
|
||||
if (admin_verify_inet_user(uname, passwd) == 0)
|
||||
{
|
||||
MXS_ERROR("Authentication failed, wrong user/password "
|
||||
"combination. Removing user failed.");
|
||||
return ADMIN_ERR_AUTHENTICATION;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove user from in-memory structure */
|
||||
users_delete(users, uname);
|
||||
users_delete(users, (char*)uname); // TODO: Make users const correct.
|
||||
|
||||
/**
|
||||
* Open passwd file and remove user from the file.
|
||||
*/
|
||||
snprintf(fname, sizeof(fname), "%s/%s", get_datadir(), USERS_FILE_NAME);
|
||||
snprintf(fname_tmp, sizeof(fname_tmp), "%s/%s_tmp", get_datadir(), USERS_FILE_NAME);
|
||||
snprintf(path, sizeof(path), "%s/%s", get_datadir(), fname);
|
||||
snprintf(path_tmp, sizeof(path_tmp), "%s/%s_tmp", get_datadir(), fname);
|
||||
/**
|
||||
* Rewrite passwd file from memory.
|
||||
*/
|
||||
if ((fp = fopen(fname, "r")) == NULL)
|
||||
if ((fp = fopen(path, "r")) == NULL)
|
||||
{
|
||||
int err = errno;
|
||||
MXS_ERROR("Unable to open password file %s : errno %d.\n"
|
||||
"Removing user from file failed; it must be done "
|
||||
"manually.",
|
||||
fname,
|
||||
path,
|
||||
err);
|
||||
return ADMIN_ERR_PWDFILEOPEN;
|
||||
}
|
||||
/**
|
||||
* Open temporary passwd file.
|
||||
*/
|
||||
if ((fp_tmp = fopen(fname_tmp, "w")) == NULL)
|
||||
if ((fp_tmp = fopen(path_tmp, "w")) == NULL)
|
||||
{
|
||||
int err = errno;
|
||||
MXS_ERROR("Unable to open tmp file %s : errno %d.\n"
|
||||
"Removing user from passwd file failed; it must be done "
|
||||
"manually.",
|
||||
fname_tmp,
|
||||
path_tmp,
|
||||
err);
|
||||
fclose(fp);
|
||||
return ADMIN_ERR_TMPFILEOPEN;
|
||||
@ -311,11 +227,11 @@ char* admin_remove_user(
|
||||
MXS_ERROR("Unable to process passwd file %s : errno %d.\n"
|
||||
"Removing user from file failed, and must be done "
|
||||
"manually.",
|
||||
fname,
|
||||
path,
|
||||
err);
|
||||
fclose(fp);
|
||||
fclose(fp_tmp);
|
||||
unlink(fname_tmp);
|
||||
unlink(path_tmp);
|
||||
return ADMIN_ERR_PWDFILEACCESS;
|
||||
}
|
||||
|
||||
@ -330,7 +246,7 @@ char* admin_remove_user(
|
||||
else if (!feof(fp))
|
||||
{
|
||||
MXS_ERROR("Line length exceeds %d characters, possible corrupted "
|
||||
"'passwd' file in: %s", LINELEN, fname);
|
||||
"'passwd' file in: %s", LINELEN, path);
|
||||
fclose(fp);
|
||||
fclose(fp_tmp);
|
||||
return ADMIN_ERR_PWDFILEACCESS;
|
||||
@ -358,11 +274,11 @@ char* admin_remove_user(
|
||||
"errno %d.\n"
|
||||
"Removing user from file failed, and must be "
|
||||
"done manually.",
|
||||
fname,
|
||||
path,
|
||||
err);
|
||||
fclose(fp);
|
||||
fclose(fp_tmp);
|
||||
unlink(fname_tmp);
|
||||
unlink(path_tmp);
|
||||
return ADMIN_ERR_PWDFILEACCESS;
|
||||
}
|
||||
}
|
||||
@ -370,16 +286,16 @@ char* admin_remove_user(
|
||||
/**
|
||||
* Replace original passwd file with new.
|
||||
*/
|
||||
if (rename(fname_tmp, fname))
|
||||
if (rename(path_tmp, path))
|
||||
{
|
||||
int err = errno;
|
||||
MXS_ERROR("Unable to rename new passwd file %s : errno "
|
||||
"%d.\n"
|
||||
"Rename it to %s manually.",
|
||||
fname_tmp,
|
||||
path_tmp,
|
||||
err,
|
||||
fname);
|
||||
unlink(fname_tmp);
|
||||
path);
|
||||
unlink(path_tmp);
|
||||
fclose(fp_tmp);
|
||||
return ADMIN_ERR_PWDFILEACCESS;
|
||||
}
|
||||
@ -387,47 +303,284 @@ char* admin_remove_user(
|
||||
return ADMIN_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check for existance of the user
|
||||
*
|
||||
* @param user The user name to test
|
||||
* @return Non-zero if the user exists
|
||||
* @param uname The user name to test
|
||||
* @return True if the user exists
|
||||
*/
|
||||
int
|
||||
admin_search_user(char *user)
|
||||
static bool admin_search_user(USERS *users, const char *uname)
|
||||
{
|
||||
return (users_fetch(users, (char*)uname) != NULL); // TODO: Make users const correct.
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
void dcb_print_users(DCB *dcb, const char* heading, USERS *users)
|
||||
{
|
||||
dcb_printf(dcb, "%s", heading);
|
||||
|
||||
if (users)
|
||||
{
|
||||
HASHITERATOR *iter = hashtable_iterator(users->data);
|
||||
|
||||
if (iter)
|
||||
{
|
||||
char *sep = "";
|
||||
const char *user;
|
||||
|
||||
while ((user = hashtable_next(iter)) != NULL)
|
||||
{
|
||||
dcb_printf(dcb, "%s%s", sep, user);
|
||||
sep = ", ";
|
||||
}
|
||||
|
||||
hashtable_iterator_free(iter);
|
||||
}
|
||||
}
|
||||
|
||||
dcb_printf(dcb, "%s", "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the admin users
|
||||
*
|
||||
* @return Table of users
|
||||
*/
|
||||
static USERS *
|
||||
loadUsers(const char *fname)
|
||||
{
|
||||
USERS *rval;
|
||||
FILE *fp;
|
||||
char path[PATH_MAX], *home;
|
||||
char uname[80];
|
||||
int added_users = 0;
|
||||
|
||||
initialise();
|
||||
snprintf(path, sizeof(path), "%s/%s", get_datadir(), fname);
|
||||
if ((fp = fopen(path, "r")) == NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
if ((rval = users_alloc()) == NULL)
|
||||
{
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
while (fgets(uname, sizeof(uname), fp))
|
||||
{
|
||||
char *nl = strchr(uname, '\n');
|
||||
|
||||
if (nl)
|
||||
{
|
||||
*nl = '\0';
|
||||
}
|
||||
else if (!feof(fp))
|
||||
{
|
||||
MXS_ERROR("Line length exceeds %d characters, possibly corrupted "
|
||||
"'passwd' file in: %s", LINELEN, path);
|
||||
users_free(rval);
|
||||
rval = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
char *password;
|
||||
char *colon = strchr(uname, ':');
|
||||
if (colon)
|
||||
{
|
||||
// Inet case
|
||||
*colon = 0;
|
||||
password = colon + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux case.
|
||||
password = "";
|
||||
}
|
||||
|
||||
if (users_add(rval, uname, password))
|
||||
{
|
||||
added_users++;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
if (!added_users)
|
||||
{
|
||||
users_free(rval);
|
||||
rval = NULL;
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
|
||||
static USERS *loadLinuxUsers()
|
||||
{
|
||||
return loadUsers(LINUX_USERS_FILE_NAME);
|
||||
}
|
||||
|
||||
static USERS *loadInetUsers()
|
||||
{
|
||||
return loadUsers(INET_USERS_FILE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Linux account
|
||||
*
|
||||
* @param uname Name of Linux user
|
||||
*
|
||||
* @return NULL on success or an error string on failure.
|
||||
*/
|
||||
const char *admin_enable_linux_account(const char *uname)
|
||||
{
|
||||
initialise();
|
||||
|
||||
int rv = 0;
|
||||
return admin_add_user(&linux_users, LINUX_USERS_FILE_NAME, uname, NULL);
|
||||
}
|
||||
|
||||
if (strcmp(user, DEFAULT_ADMIN_USER) == 0)
|
||||
/**
|
||||
* Disable Linux account
|
||||
*
|
||||
* @param uname Name of Linux user
|
||||
*
|
||||
* @return NULL on success or an error string on failure.
|
||||
*/
|
||||
const char* admin_disable_linux_account(const char* uname)
|
||||
{
|
||||
initialise();
|
||||
|
||||
return admin_remove_user(linux_users, LINUX_USERS_FILE_NAME, uname, NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether Linux account is enabled
|
||||
*
|
||||
* @param uname The user name
|
||||
*
|
||||
* @return True if the account is enabled, false otherwise.
|
||||
*/
|
||||
bool admin_linux_account_enabled(const char *uname)
|
||||
{
|
||||
initialise();
|
||||
|
||||
bool rv = false;
|
||||
|
||||
if (strcmp(uname, DEFAULT_ADMIN_USER) == 0)
|
||||
{
|
||||
rv = 1;
|
||||
rv = true;
|
||||
}
|
||||
else if (users)
|
||||
else if (linux_users)
|
||||
{
|
||||
rv = (users_fetch(users, user) != NULL);
|
||||
rv = admin_search_user(linux_users, uname);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the statistics and user names of the administration users
|
||||
* Add insecure remote (network) user.
|
||||
*
|
||||
* @param dcb A DCB to send the output to
|
||||
* @param uname Name of the new user.
|
||||
* @param password Password of the new user.
|
||||
*
|
||||
* @return NULL on success or an error string on failure.
|
||||
*/
|
||||
void
|
||||
dcb_PrintAdminUsers(DCB *dcb)
|
||||
const char *admin_add_inet_user(const char *uname, const char* password)
|
||||
{
|
||||
if (users)
|
||||
initialise();
|
||||
|
||||
struct crypt_data cdata;
|
||||
cdata.initialized = 0;
|
||||
char *cpassword = crypt_r(password, ADMIN_SALT, &cdata);
|
||||
|
||||
return admin_add_user(&inet_users, INET_USERS_FILE_NAME, uname, cpassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove insecure remote (network) user
|
||||
*
|
||||
* @param uname Name of user to be removed.
|
||||
* @param password Password of user to be removed.
|
||||
*
|
||||
* @return NULL on success or an error string on failure.
|
||||
*/
|
||||
const char* admin_remove_inet_user(const char* uname, const char *password)
|
||||
{
|
||||
initialise();
|
||||
|
||||
return admin_remove_user(inet_users, INET_USERS_FILE_NAME, uname, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for existance of remote user.
|
||||
*
|
||||
* @param user The user name to test.
|
||||
*
|
||||
* @return True if the user exists, false otherwise.
|
||||
*/
|
||||
bool admin_inet_user_exists(const char *uname)
|
||||
{
|
||||
initialise();
|
||||
|
||||
bool rv = false;
|
||||
|
||||
if (inet_users)
|
||||
{
|
||||
dcb_usersPrint(dcb, users);
|
||||
rv = admin_search_user(inet_users, uname);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a remote user name and password
|
||||
*
|
||||
* @param username Username to verify
|
||||
* @param password Password to verify
|
||||
*
|
||||
* @return True if the username/password combination is valid
|
||||
*/
|
||||
bool
|
||||
admin_verify_inet_user(const char *username, const char *password)
|
||||
{
|
||||
bool rv = false;
|
||||
|
||||
initialise();
|
||||
|
||||
if (inet_users)
|
||||
{
|
||||
const char* pw = users_fetch(inet_users, (char*)username); // TODO: Make users const-correct.
|
||||
|
||||
if (pw)
|
||||
{
|
||||
struct crypt_data cdata;
|
||||
cdata.initialized = 0;
|
||||
if (strcmp(pw, crypt_r(password, ADMIN_SALT, &cdata)) == 0)
|
||||
{
|
||||
rv = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dcb_printf(dcb, "No administration users have been defined.\n");
|
||||
if (strcmp(username, INET_DEFAULT_USERNAME) == 0
|
||||
&& strcmp(password, INET_DEFAULT_PASSWORD) == 0)
|
||||
{
|
||||
rv = true;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print Linux and and inet users
|
||||
*
|
||||
* @param dcb A DCB to send the output to.
|
||||
*/
|
||||
void dcb_PrintAdminUsers(DCB *dcb)
|
||||
{
|
||||
dcb_print_users(dcb, "Enabled Linux accounts (secure) : ", linux_users);
|
||||
dcb_print_users(dcb, "Created network accounts (insecure): ", inet_users);
|
||||
}
|
||||
|
||||
@ -84,6 +84,7 @@ static bool process_config_context(CONFIG_CONTEXT *);
|
||||
static int process_config_update(CONFIG_CONTEXT *);
|
||||
static void free_config_context(CONFIG_CONTEXT *);
|
||||
static char *config_get_value(CONFIG_PARAMETER *, const char *);
|
||||
static char *config_get_password(CONFIG_PARAMETER *);
|
||||
static const char *config_get_value_string(CONFIG_PARAMETER *, const char *);
|
||||
static int handle_global_item(const char *, const char *);
|
||||
static int handle_feedback_item(const char *, const char *);
|
||||
@ -119,7 +120,8 @@ static char *service_params[] =
|
||||
"router_options",
|
||||
"servers",
|
||||
"user",
|
||||
"passwd",
|
||||
"passwd", // DEPRECATE: See config_get_password.
|
||||
"password",
|
||||
"enable_root_user",
|
||||
"max_connections",
|
||||
"max_queued_connections",
|
||||
@ -139,6 +141,7 @@ static char *service_params[] =
|
||||
"ignore_databases_regex",
|
||||
"log_auth_warnings",
|
||||
"source", /**< Avrorouter only */
|
||||
"retry_on_failure",
|
||||
NULL
|
||||
};
|
||||
|
||||
@ -166,7 +169,8 @@ static char *monitor_params[] =
|
||||
"module",
|
||||
"servers",
|
||||
"user",
|
||||
"passwd",
|
||||
"passwd", // DEPRECATE: See config_get_password.
|
||||
"password",
|
||||
"script",
|
||||
"events",
|
||||
"mysql51_replication",
|
||||
@ -594,6 +598,30 @@ config_get_value(CONFIG_PARAMETER *params, const char *name)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// DEPRECATE: In 2.1 complain but accept if "passwd" is provided, in 2.2
|
||||
// DEPRECATE: drop support for "passwd".
|
||||
/**
|
||||
* Get the value of the password parameter
|
||||
*
|
||||
* The words looked for are "password" and "passwd".
|
||||
*
|
||||
* @param params The linked list of config parameters
|
||||
* @return the parameter value or NULL if not found
|
||||
*/
|
||||
static char *
|
||||
config_get_password(CONFIG_PARAMETER *params)
|
||||
{
|
||||
char *password = config_get_value(params, "password");
|
||||
char *passwd = config_get_value(params, "passwd");
|
||||
|
||||
if (password && passwd)
|
||||
{
|
||||
MXS_WARNING("Both 'password' and 'passwd' specified. Using value of 'password'.");
|
||||
}
|
||||
|
||||
return passwd ? passwd : password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a config parameter as a string
|
||||
*
|
||||
@ -1386,7 +1414,7 @@ process_config_update(CONFIG_CONTEXT *context)
|
||||
max_queued_connections = config_get_value_string(obj->parameters, "max_queued_connections");
|
||||
queued_connection_timeout = config_get_value_string(obj->parameters, "queued_connection_timeout");
|
||||
user = config_get_value(obj->parameters, "user");
|
||||
auth = config_get_value(obj->parameters, "passwd");
|
||||
auth = config_get_password(obj->parameters);
|
||||
|
||||
auth_all_servers = config_get_value(obj->parameters, "auth_all_servers");
|
||||
strip_db_esc = config_get_value(obj->parameters, "strip_db_esc");
|
||||
@ -2318,7 +2346,7 @@ int create_new_service(CONFIG_CONTEXT *obj)
|
||||
}
|
||||
|
||||
char *user = config_get_value(obj->parameters, "user");
|
||||
char *auth = config_get_value(obj->parameters, "passwd");
|
||||
char *auth = config_get_password(obj->parameters);
|
||||
|
||||
if (user && auth)
|
||||
{
|
||||
@ -2331,7 +2359,7 @@ int create_new_service(CONFIG_CONTEXT *obj)
|
||||
obj->object,
|
||||
user ? "" : "the 'user' parameter",
|
||||
!user && !auth ? " and " : "",
|
||||
auth ? "" : "the 'passwd' parameter");
|
||||
auth ? "" : "the 'password' or 'passwd' parameter");
|
||||
}
|
||||
|
||||
char *subservices = config_get_value(obj->parameters, "subservices");
|
||||
@ -2736,7 +2764,7 @@ int create_new_monitor(CONFIG_CONTEXT *context, CONFIG_CONTEXT *obj, HASHTABLE*
|
||||
}
|
||||
|
||||
char *user = config_get_value(obj->parameters, "user");
|
||||
char *passwd = config_get_value(obj->parameters, "passwd");
|
||||
char *passwd = config_get_password(obj->parameters);
|
||||
if (user && passwd)
|
||||
{
|
||||
monitorAddUser(obj->element, user, passwd);
|
||||
|
||||
@ -137,6 +137,7 @@ static struct option long_options[] =
|
||||
{"execdir", required_argument, 0, 'E'},
|
||||
{"language", required_argument, 0, 'N'},
|
||||
{"piddir", required_argument, 0, 'P'},
|
||||
{"basedir", required_argument, 0, 'R'},
|
||||
{"user", required_argument, 0, 'U'},
|
||||
{"syslog", required_argument, 0, 's'},
|
||||
{"maxlog", required_argument, 0, 'S'},
|
||||
@ -164,7 +165,7 @@ static void write_footer(void);
|
||||
static int ntfw_cb(const char*, const struct stat*, int, struct FTW*);
|
||||
static bool file_is_readable(const char* absolute_pathname);
|
||||
static bool file_is_writable(const char* absolute_pathname);
|
||||
bool handle_path_arg(char** dest, char* path, char* arg, bool rd, bool wr);
|
||||
bool handle_path_arg(char** dest, const char* path, char* arg, bool rd, bool wr);
|
||||
static void set_log_augmentation(const char* value);
|
||||
static void usage(void);
|
||||
static char* get_expanded_pathname(
|
||||
@ -697,30 +698,27 @@ static void print_log_n_stderr(
|
||||
const char* fprstr, /*< string to be printed to stderr */
|
||||
int eno) /*< errno, if it is set, zero, otherwise */
|
||||
{
|
||||
char* log_err = "Error :";
|
||||
char* fpr_err = "*\n* Error :";
|
||||
char* fpr_end = "\n*\n";
|
||||
|
||||
if (do_log)
|
||||
{
|
||||
mxs_log_init(NULL, get_logdir(), MXS_LOG_TARGET_FS);
|
||||
char errbuf[STRERROR_BUFLEN];
|
||||
MXS_ERROR("%s %s %s %s",
|
||||
log_err,
|
||||
logstr,
|
||||
eno == 0 ? " " : "Error :",
|
||||
eno == 0 ? " " : strerror_r(eno, errbuf, sizeof(errbuf)));
|
||||
if (mxs_log_init(NULL, get_logdir(), MXS_LOG_TARGET_FS))
|
||||
{
|
||||
char errbuf[STRERROR_BUFLEN];
|
||||
MXS_ERROR("%s%s%s%s",
|
||||
logstr,
|
||||
eno == 0 ? "" : " (",
|
||||
eno == 0 ? "" : strerror_r(eno, errbuf, sizeof(errbuf)),
|
||||
eno == 0 ? "" : ")");
|
||||
}
|
||||
}
|
||||
if (do_stderr)
|
||||
{
|
||||
char errbuf[STRERROR_BUFLEN];
|
||||
fprintf(stderr,
|
||||
"%s %s %s %s %s",
|
||||
fpr_err,
|
||||
"* Error: %s%s%s%s\n",
|
||||
fprstr,
|
||||
eno == 0 ? " " : "Error :",
|
||||
eno == 0 ? " " : strerror_r(eno, errbuf, sizeof(errbuf)),
|
||||
fpr_end);
|
||||
eno == 0 ? "" : " (",
|
||||
eno == 0 ? "" : strerror_r(eno, errbuf, sizeof(errbuf)),
|
||||
eno == 0 ? "" : ")");
|
||||
}
|
||||
}
|
||||
|
||||
@ -887,32 +885,52 @@ static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"\nUsage : %s [OPTION]...\n\n"
|
||||
" -c, --config-check Validate configuration file and exit\n"
|
||||
" -d, --nodaemon enable running in terminal process (default:disabled)\n"
|
||||
" -f, --config=FILE relative or absolute pathname of MaxScale configuration file\n"
|
||||
" (default:/etc/maxscale.cnf)\n"
|
||||
" -l, --log=[file|shm|stdout] log to file, shared memory or stdout (default: file)\n"
|
||||
" -L, --logdir=PATH path to log file directory (default: /var/log/maxscale)\n"
|
||||
" -A, --cachedir=PATH path to cache directory (default: /var/cache/maxscale)\n"
|
||||
" -B, --libdir=PATH path to module directory (default: /usr/lib64/maxscale)\n"
|
||||
" -C, --configdir=PATH path to configuration file directory (default: /etc/)\n"
|
||||
" -D, --datadir=PATH path to data directory, stored embedded mysql tables\n"
|
||||
" (default: /var/cache/maxscale)\n"
|
||||
" -c, --config-check validate configuration file and exit\n"
|
||||
" -d, --nodaemon enable running in terminal process\n"
|
||||
" -f, --config=FILE relative or absolute pathname of config file\n"
|
||||
" -l, --log=[file|shm|stdout] log to file, shared memory or stdout\n"
|
||||
" (default: file)\n"
|
||||
" -L, --logdir=PATH path to log file directory\n"
|
||||
" -A, --cachedir=PATH path to cache directory\n"
|
||||
" -B, --libdir=PATH path to module directory\n"
|
||||
" -C, --configdir=PATH path to configuration file directory\n"
|
||||
" -D, --datadir=PATH path to data directory,\n"
|
||||
" stored embedded mysql tables\n"
|
||||
" -E, --execdir=PATH path to the maxscale and other executable files\n"
|
||||
" (default: /usr/bin)\n"
|
||||
" -N, --language=PATH path to errmsg.sys file (default: /var/lib/maxscale)\n"
|
||||
" -P, --piddir=PATH path to PID file directory (default: /var/run/maxscale)\n"
|
||||
" -U, --user=USER run MaxScale as another user.\n"
|
||||
" The user ID and group ID of this user are used to run MaxScale.\n"
|
||||
" -N, --language=PATH path to errmsg.sys file\n"
|
||||
" -P, --piddir=PATH path to PID file directory\n"
|
||||
" -R, --basedir=PATH base path for all other paths\n"
|
||||
" -U, --user=USER user ID and group ID of specified user are used to\n"
|
||||
" run MaxScale\n"
|
||||
" -s, --syslog=[yes|no] log messages to syslog (default:yes)\n"
|
||||
" -S, --maxlog=[yes|no] log messages to MaxScale log (default: yes)\n"
|
||||
" -G, --log_augmentation=0|1 augment messages with the name of the function where\n"
|
||||
" the message was logged (default: 0). Primarily for \n"
|
||||
" development purposes.\n"
|
||||
" -G, --log_augmentation=0|1 augment messages with the name of the function\n"
|
||||
" where the message was logged (default: 0)\n"
|
||||
" -v, --version print version info and exit\n"
|
||||
" -V, --version-full print full version info and exit\n"
|
||||
" -?, --help show this help\n"
|
||||
, progname);
|
||||
"\n"
|
||||
"Defaults paths:\n"
|
||||
" config file: %s/%s\n"
|
||||
" configdir : %s\n"
|
||||
" logdir : %s\n"
|
||||
" cachedir : %s\n"
|
||||
" libdir : %s\n"
|
||||
" datadir : %s\n"
|
||||
" execdir : %s\n"
|
||||
" language : %s\n"
|
||||
" piddir : %s\n"
|
||||
"\n"
|
||||
"If '--basedir' is provided then all other paths, including the default\n"
|
||||
"configuration file path, are defined relative to that. As an example,\n"
|
||||
"if '--basedir /path/maxscale' is specified, then, for instance, the log\n"
|
||||
"dir will be '/path/maxscale/var/log/maxscale', the config dir will be\n"
|
||||
"'/path/maxscale/etc' and the default config file will be\n"
|
||||
"'/path/maxscale/etc/maxscale.cnf'.\n",
|
||||
progname,
|
||||
get_configdir(), default_cnf_fname,
|
||||
get_configdir(), get_logdir(), get_cachedir(), get_libdir(),
|
||||
get_datadir(), get_execdir(), get_langdir(), get_piddir());
|
||||
}
|
||||
|
||||
|
||||
@ -1135,6 +1153,61 @@ bool configure_signals(void)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the directories of MaxScale relative to a basedir
|
||||
*
|
||||
* @param basedir The base directory relative to which the other are set.
|
||||
*
|
||||
* @return True if the directories could be set, false otherwise.
|
||||
*/
|
||||
bool set_dirs(const char *basedir)
|
||||
{
|
||||
bool rv = true;
|
||||
char *path;
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, "var/" MXS_DEFAULT_LOG_SUBPATH, true, false)))
|
||||
{
|
||||
set_logdir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, "var/" MXS_DEFAULT_CACHE_SUBPATH, true, true)))
|
||||
{
|
||||
set_cachedir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, MXS_DEFAULT_LIB_SUBPATH, true, false)))
|
||||
{
|
||||
set_libdir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, MXS_DEFAULT_CONFIG_SUBPATH, true, false)))
|
||||
{
|
||||
set_configdir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, "var/" MXS_DEFAULT_DATA_SUBPATH, true, false)))
|
||||
{
|
||||
set_datadir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, MXS_DEFAULT_EXEC_SUBPATH, true, false)))
|
||||
{
|
||||
set_execdir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, "var/" MXS_DEFAULT_LANG_SUBPATH, true, false)))
|
||||
{
|
||||
set_langdir(path);
|
||||
}
|
||||
|
||||
if (rv && (rv = handle_path_arg(&path, basedir, "var/" MXS_DEFAULT_PID_SUBPATH, true, true)))
|
||||
{
|
||||
set_piddir(path);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @mainpage
|
||||
* The main entry point into MaxScale
|
||||
@ -1304,7 +1377,6 @@ int main(int argc, char **argv)
|
||||
}
|
||||
break;
|
||||
case 'L':
|
||||
|
||||
if (handle_path_arg(&tmp_path, optarg, NULL, true, false))
|
||||
{
|
||||
set_logdir(tmp_path);
|
||||
@ -1381,6 +1453,17 @@ int main(int argc, char **argv)
|
||||
succp = false;
|
||||
}
|
||||
break;
|
||||
case 'R':
|
||||
if (handle_path_arg(&tmp_path, optarg, NULL, true, false))
|
||||
{
|
||||
succp = set_dirs(tmp_path);
|
||||
free(tmp_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
succp = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'S':
|
||||
{
|
||||
@ -2252,7 +2335,7 @@ static int write_pid_file()
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool handle_path_arg(char** dest, char* path, char* arg, bool rd, bool wr)
|
||||
bool handle_path_arg(char** dest, const char* path, char* arg, bool rd, bool wr)
|
||||
{
|
||||
char pathbuffer[PATH_MAX + 2];
|
||||
char* errstr;
|
||||
|
||||
@ -584,7 +584,7 @@ return_succ:
|
||||
{
|
||||
/** This releases memory of all created objects */
|
||||
logmanager_done_nomutex();
|
||||
LOG_ERROR("MaxScale Log: Error, initialization failed.\n");
|
||||
fprintf(stderr, "* Error: Initializing the log manager failed.\n");
|
||||
}
|
||||
return succ;
|
||||
}
|
||||
@ -1693,7 +1693,7 @@ static bool logfile_open_file(filewriter_t* fw,
|
||||
|
||||
if (fw->fwr_file == NULL)
|
||||
{
|
||||
LOG_ERROR("MaxScale Log: Error, opening log file %s failed.\n", lf->lf_full_file_name);
|
||||
// Error logged by skygw_file_init to stderr.
|
||||
rv = false;
|
||||
}
|
||||
|
||||
@ -2125,9 +2125,6 @@ static void filewriter_done(filewriter_t* fw, bool write_footer)
|
||||
{
|
||||
case RUN:
|
||||
CHK_FILEWRITER(fw);
|
||||
case INIT:
|
||||
fw->fwr_logmes = NULL;
|
||||
fw->fwr_clientmes = NULL;
|
||||
if (log_config.use_stdout)
|
||||
{
|
||||
skygw_file_free(fw->fwr_file);
|
||||
@ -2141,7 +2138,11 @@ static void filewriter_done(filewriter_t* fw, bool write_footer)
|
||||
|
||||
skygw_file_close(fw->fwr_file);
|
||||
}
|
||||
case INIT:
|
||||
fw->fwr_logmes = NULL;
|
||||
fw->fwr_clientmes = NULL;
|
||||
fw->fwr_state = DONE;
|
||||
break;
|
||||
case DONE:
|
||||
case UNINIT:
|
||||
default:
|
||||
|
||||
@ -826,7 +826,9 @@ mon_status_changed(MONITOR_SERVERS* mon_srv)
|
||||
{
|
||||
/* Previous status is -1 if not yet set */
|
||||
return (mon_srv->mon_prev_status != -1
|
||||
&& mon_srv->mon_prev_status != mon_srv->server->status);
|
||||
&& mon_srv->mon_prev_status != mon_srv->server->status
|
||||
/** If the server is going into maintenance or coming out of it, don't trigger a state change */
|
||||
&& ((mon_srv->mon_prev_status | mon_srv->server->status) & SERVER_MAINT) == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -89,7 +89,6 @@ server_alloc(char *servname, char *protocol, unsigned short port)
|
||||
server->rlag = -2;
|
||||
server->master_id = -1;
|
||||
server->depth = -1;
|
||||
server->slaves = NULL;
|
||||
server->parameters = NULL;
|
||||
server->server_string = NULL;
|
||||
spinlock_init(&server->lock);
|
||||
|
||||
@ -384,6 +384,9 @@ int serviceStartAllPorts(SERVICE* service)
|
||||
(void*) service, retry_after);
|
||||
MXS_NOTICE("Failed to start service %s, retrying in %d seconds.",
|
||||
service->name, retry_after);
|
||||
|
||||
/** This will prevent MaxScale from shutting down if service start is retried later */
|
||||
listeners = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@ -50,12 +50,12 @@
|
||||
static int
|
||||
test1()
|
||||
{
|
||||
if (admin_verify("admin", "mariadb") == 0)
|
||||
if (admin_verify_inet_user("admin", "mariadb") == 0)
|
||||
{
|
||||
fprintf(stderr, "admin_verify: test 1.1 (default user) failed.\n");
|
||||
return 1;
|
||||
}
|
||||
if (admin_verify("bad", "user"))
|
||||
if (admin_verify_inet_user("bad", "user"))
|
||||
{
|
||||
fprintf(stderr, "admin_verify: test 1.2 (wrong user) failed.\n");
|
||||
return 1;
|
||||
@ -74,15 +74,15 @@ test1()
|
||||
static int
|
||||
test2()
|
||||
{
|
||||
char *err;
|
||||
const char *err;
|
||||
|
||||
if ((err = admin_add_user("user0")) != NULL)
|
||||
if ((err = admin_enable_linux_account("user0")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 2.1 (add user) failed, %s.\n", err);
|
||||
|
||||
return 1;
|
||||
}
|
||||
if (admin_add_user("user0") == NULL)
|
||||
if (admin_enable_linux_account("user0") == NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 2.2 (add user) failed, duplicate.\n");
|
||||
|
||||
@ -90,7 +90,7 @@ test2()
|
||||
}
|
||||
|
||||
/* Deleting the last user is not forbidden so we expect this to succeed */
|
||||
if ((err = admin_remove_user("user0")) != NULL)
|
||||
if ((err = admin_disable_linux_account("user0")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_remove_user: test 2.3 (add user) failed, %s.\n", err);
|
||||
|
||||
@ -98,7 +98,7 @@ test2()
|
||||
}
|
||||
|
||||
/* Add the user back, for test5. */
|
||||
if ((err = admin_add_user("user0")) != NULL)
|
||||
if ((err = admin_enable_linux_account("user0")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 2.4 (add user) failed, %s.\n", err);
|
||||
|
||||
@ -120,37 +120,37 @@ test2()
|
||||
static int
|
||||
test3()
|
||||
{
|
||||
char *err;
|
||||
const char *err;
|
||||
|
||||
if ((err = admin_add_user("user1")) != NULL)
|
||||
if ((err = admin_enable_linux_account("user1")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 3.1 (add user) failed, %s.\n", err);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (admin_search_user("user1") == 0)
|
||||
if (admin_linux_account_enabled("user1") == 0)
|
||||
{
|
||||
fprintf(stderr, "admin_search_user: test 3.2 (search user) failed.\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (admin_search_user("user2") != 0)
|
||||
if (admin_linux_account_enabled("user2") != 0)
|
||||
{
|
||||
fprintf(stderr, "admin_search_user: test 3.3 (search user) failed, unexpeted user found.\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((err = admin_remove_user("user1")) != NULL)
|
||||
if ((err = admin_disable_linux_account("user1")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_remove_user: test 3.4 (add user) failed, %s.\n", err);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (admin_search_user("user1"))
|
||||
if (admin_linux_account_enabled("user1"))
|
||||
{
|
||||
fprintf(stderr, "admin_search_user: test 3.5 (search user) failed - user was deleted.\n");
|
||||
|
||||
@ -173,13 +173,14 @@ test3()
|
||||
static int
|
||||
test4()
|
||||
{
|
||||
char *err, user[40], passwd[40];
|
||||
const char *err;
|
||||
char user[40], passwd[40];
|
||||
int i, n_users = 50;
|
||||
|
||||
for (i = 1; i < n_users; i++)
|
||||
{
|
||||
sprintf(user, "user%d", i);
|
||||
if ((err = admin_add_user(user)) != NULL)
|
||||
if ((err = admin_enable_linux_account(user)) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 4.1 (add user) failed, %s.\n", err);
|
||||
|
||||
@ -190,7 +191,7 @@ test4()
|
||||
for (i = 1; i < n_users; i++)
|
||||
{
|
||||
sprintf(user, "user%d", i);
|
||||
if (admin_search_user(user) == 0)
|
||||
if (admin_linux_account_enabled(user) == 0)
|
||||
{
|
||||
fprintf(stderr, "admin_search_user: test 4.2 (search user) failed.\n");
|
||||
|
||||
@ -201,7 +202,7 @@ test4()
|
||||
for (i = 1; i < n_users; i++)
|
||||
{
|
||||
sprintf(user, "user%d", i);
|
||||
if ((err = admin_remove_user(user)) != NULL)
|
||||
if ((err = admin_disable_linux_account(user)) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_remove_user: test 4.3 (add user) failed, %s.\n", err);
|
||||
|
||||
@ -221,16 +222,16 @@ test4()
|
||||
static int
|
||||
test5()
|
||||
{
|
||||
char *err;
|
||||
const char *err;
|
||||
|
||||
if ((err = admin_add_user("user")) != NULL)
|
||||
if ((err = admin_enable_linux_account("user")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_add_user: test 5.1 (add user) failed, %s.\n", err);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((err = admin_remove_user("user0")) != NULL)
|
||||
if ((err = admin_disable_linux_account("user0")) != NULL)
|
||||
{
|
||||
fprintf(stderr, "admin_remove_user: test 5.2 (add user) failed, %s.\n", err);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user