Commit 6c8ce999 authored by Robert Bindar's avatar Robert Bindar Committed by Sergei Golubchik

MDEV-13095 Implement User Account locking

Add server support for user account locking.
This patch extends the ALTER/CREATE USER statements for
denying a user's subsequent login attempts:
  ALTER USER
    user [, user2] ACCOUNT [LOCK | UNLOCK]
  CREATE USER
    user [, user2] ACCOUNT [LOCK | UNLOCK]
The SHOW CREATE USER statement was updated to display the
locking state of an user.

Closes #1006
parent d89cdfc2
create user user1@localhost;
create user user2@localhost;
#
# Only privileged users should be able to lock/unlock.
#
alter user user1@localhost account lock;
alter user user1@localhost account unlock;
create user user3@localhost account lock;
drop user user3@localhost;
connect con1,localhost,user1;
connection con1;
alter user user2@localhost account lock;
ERROR 42000: Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation
disconnect con1;
connection default;
#
# ALTER USER USER1 ACCOUNT LOCK should deny the connection of user1,
# but it should allow user2 to connect.
#
alter user user1@localhost account lock;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
connect con2,localhost,user2;
disconnect con2;
connection default;
alter user user1@localhost account unlock;
#
# Passing an incorrect user should return an error unless
# IF EXISTS is used
#
alter user inexistentUser@localhost account lock;
ERROR HY000: Operation ALTER USER failed for 'inexistentUser'@'localhost'
alter if exists user inexistentUser@localhost account lock;
Warnings:
Error 1133 Can't find any matching row in the user table
Note 1396 Operation ALTER USER failed for 'inexistentUser'@'localhost'
#
# Passing an existing user to CREATE should not be allowed
# and it should not change the locking state of the current user
#
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
create user user1@localhost account lock;
ERROR HY000: Operation CREATE USER failed for 'user1'@'localhost'
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
#
# Passing multiple users should lock them all
#
alter user user1@localhost, user2@localhost account lock;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
connect(localhost,user2,,test,MYSQL_PORT,MYSQL_SOCK);
connect con2,localhost,user2;
ERROR HY000: Access denied, this account is locked
alter user user1@localhost, user2@localhost account unlock;
#
# The locking state is preserved after acl reload
#
alter user user1@localhost account lock;
flush privileges;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
alter user user1@localhost account unlock;
#
# JSON functions on global_priv reflect the locking state of an account
#
alter user user1@localhost account lock;
select host, user, JSON_VALUE(Priv, '$.account_locked') from mysql.global_priv where user='user1';
host user JSON_VALUE(Priv, '$.account_locked')
localhost user1 1
alter user user1@localhost account unlock;
select host, user, JSON_VALUE(Priv, '$.account_locked') from mysql.global_priv where user='user1';
host user JSON_VALUE(Priv, '$.account_locked')
localhost user1 0
#
# SHOW CREATE USER correctly displays the locking state of an user
#
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
alter user user1@localhost account lock;
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost' ACCOUNT LOCK
alter user user1@localhost account unlock;
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
create user newuser@localhost account lock;
show create user newuser@localhost;
CREATE USER for newuser@localhost
CREATE USER 'newuser'@'localhost' ACCOUNT LOCK
drop user newuser@localhost;
#
# Users should be able to lock themselves
#
grant CREATE USER on *.* to user1@localhost;
connect con1,localhost,user1;
connection con1;
alter user user1@localhost account lock;
disconnect con1;
connection default;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
alter user user1@localhost account unlock;
#
# Users should be able to unlock themselves if the connections
# had been established before the accounts were locked
#
grant CREATE USER on *.* to user1@localhost;
connect con1,localhost,user1;
alter user user1@localhost account lock;
connection con1;
alter user user1@localhost account unlock;
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
disconnect con1;
connection default;
#
# COM_CHANGE_USER should return error if the destination
# account is locked
#
alter user user1@localhost account lock;
ERROR HY000: Access denied, this account is locked
drop user user1@localhost;
drop user user2@localhost;
#
# Test user account locking
#
--source include/not_embedded.inc
create user user1@localhost;
create user user2@localhost;
--echo #
--echo # Only privileged users should be able to lock/unlock.
--echo #
alter user user1@localhost account lock;
alter user user1@localhost account unlock;
create user user3@localhost account lock;
drop user user3@localhost;
connect(con1,localhost,user1);
connection con1;
--error ER_SPECIFIC_ACCESS_DENIED_ERROR
alter user user2@localhost account lock;
disconnect con1;
connection default;
--echo #
--echo # ALTER USER USER1 ACCOUNT LOCK should deny the connection of user1,
--echo # but it should allow user2 to connect.
--echo #
alter user user1@localhost account lock;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
connect(con2,localhost,user2);
disconnect con2;
connection default;
alter user user1@localhost account unlock;
--echo #
--echo # Passing an incorrect user should return an error unless
--echo # IF EXISTS is used
--echo #
--error ER_CANNOT_USER
alter user inexistentUser@localhost account lock;
alter if exists user inexistentUser@localhost account lock;
--echo #
--echo # Passing an existing user to CREATE should not be allowed
--echo # and it should not change the locking state of the current user
--echo #
show create user user1@localhost;
--error ER_CANNOT_USER
create user user1@localhost account lock;
show create user user1@localhost;
--echo #
--echo # Passing multiple users should lock them all
--echo #
alter user user1@localhost, user2@localhost account lock;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con2,localhost,user2);
alter user user1@localhost, user2@localhost account unlock;
--echo #
--echo # The locking state is preserved after acl reload
--echo #
alter user user1@localhost account lock;
flush privileges;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
alter user user1@localhost account unlock;
--echo #
--echo # JSON functions on global_priv reflect the locking state of an account
--echo #
alter user user1@localhost account lock;
select host, user, JSON_VALUE(Priv, '$.account_locked') from mysql.global_priv where user='user1';
alter user user1@localhost account unlock;
select host, user, JSON_VALUE(Priv, '$.account_locked') from mysql.global_priv where user='user1';
--echo #
--echo # SHOW CREATE USER correctly displays the locking state of an user
--echo #
show create user user1@localhost;
alter user user1@localhost account lock;
show create user user1@localhost;
alter user user1@localhost account unlock;
show create user user1@localhost;
create user newuser@localhost account lock;
show create user newuser@localhost;
drop user newuser@localhost;
--echo #
--echo # Users should be able to lock themselves
--echo #
grant CREATE USER on *.* to user1@localhost;
connect(con1,localhost,user1);
connection con1;
alter user user1@localhost account lock;
disconnect con1;
connection default;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
alter user user1@localhost account unlock;
--echo #
--echo # Users should be able to unlock themselves if the connections
--echo # had been established before the accounts were locked
--echo #
grant CREATE USER on *.* to user1@localhost;
connect(con1,localhost,user1);
alter user user1@localhost account lock;
connection con1;
alter user user1@localhost account unlock;
show create user user1@localhost;
disconnect con1;
connection default;
--echo #
--echo # COM_CHANGE_USER should return error if the destination
--echo # account is locked
--echo #
alter user user1@localhost account lock;
--error ER_ACCOUNT_HAS_BEEN_LOCKED
--change_user user1
drop user user1@localhost;
drop user user2@localhost;
......@@ -165,5 +165,26 @@ foo % Y mysql_native_password *E8D46CE25265E545D225A8A6F1BAF642FEBEE5CB
goo % Y mysql_native_password *F3A2A51A9B0F2BE2468926B4132313728C250DBF
ioo % Y mysql_old_password 7a8f886d28473e85
#
# Test account locking
#
create user user1@localhost account lock;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
flush privileges;
connect(localhost,user1,,test,MYSQL_PORT,MYSQL_SOCK);
connect con1,localhost,user1;
ERROR HY000: Access denied, this account is locked
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost' ACCOUNT LOCK
alter user user1@localhost account unlock;
connect con1,localhost,user1;
disconnect con1;
connection default;
show create user user1@localhost;
CREATE USER for user1@localhost
CREATE USER 'user1'@'localhost'
#
# Reset to final original state.
#
......@@ -88,6 +88,24 @@ select user, host, select_priv, plugin, authentication_string from mysql.user
where user like "%oo"
order by user;
--echo #
--echo # Test account locking
--echo #
create user user1@localhost account lock;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
flush privileges;
--replace_result $MASTER_MYPORT MYSQL_PORT $MASTER_MYSOCK MYSQL_SOCK
--error ER_ACCOUNT_HAS_BEEN_LOCKED
connect(con1,localhost,user1);
show create user user1@localhost;
alter user user1@localhost account unlock;
connect(con1,localhost,user1);
disconnect con1;
connection default;
show create user user1@localhost;
--echo #
--echo # Reset to final original state.
--echo #
......
......@@ -643,6 +643,7 @@ ALTER TABLE user ADD plugin char(64) CHARACTER SET latin1 DEFAULT '' NOT NULL,
ALTER TABLE user MODIFY plugin char(64) CHARACTER SET latin1 DEFAULT '' NOT NULL,
MODIFY authentication_string TEXT NOT NULL;
ALTER TABLE user ADD password_expired ENUM('N', 'Y') COLLATE utf8_general_ci DEFAULT 'N' NOT NULL;
ALTER TABLE user ADD account_locked enum('N', 'Y') COLLATE utf8_general_ci DEFAULT 'N' NOT NULL after password_expired;
ALTER TABLE user ADD is_role enum('N', 'Y') COLLATE utf8_general_ci DEFAULT 'N' NOT NULL;
ALTER TABLE user ADD default_role char(80) binary DEFAULT '' NOT NULL;
ALTER TABLE user ADD max_statement_time decimal(12,6) DEFAULT 0 NOT NULL;
......@@ -804,6 +805,7 @@ IF 'BASE TABLE' = (select table_type from information_schema.tables where table_
'max_statement_time', max_statement_time,
'plugin', if(plugin>'',plugin,if(length(password)=16,'mysql_old_password','mysql_native_password')),
'authentication_string', if(plugin>'' and authentication_string>'',authentication_string,password),
'account_locked', 'Y'=account_locked,
'default_role', default_role,
'is_role', 'Y'=is_role)) as Priv
FROM user;
......
......@@ -55,6 +55,7 @@ static SYMBOL symbols[] = {
{ ">>", SYM(SHIFT_RIGHT)},
{ "<=>", SYM(EQUAL_SYM)},
{ "ACCESSIBLE", SYM(ACCESSIBLE_SYM)},
{ "ACCOUNT", SYM(ACCOUNT_SYM)},
{ "ACTION", SYM(ACTION)},
{ "ADD", SYM(ADD)},
{ "ADMIN", SYM(ADMIN_SYM)},
......
......@@ -7933,3 +7933,6 @@ ER_BACKUP_UNKNOWN_STAGE
eng "Unknown backup stage: '%s'. Stage should be one of START, FLUSH, BLOCK_DDL, BLOCK_COMMIT or END"
ER_USER_IS_BLOCKED
eng "User is blocked because of too many credential errors; unblock with 'FLUSH PRIVILEGES'"
ER_ACCOUNT_HAS_BEEN_LOCKED
eng "Access denied, this account is locked"
rum "Acces refuzat, acest cont este blocat"
......@@ -152,6 +152,7 @@ class ACL_USER :public ACL_USER_BASE
LEX_CSTRING default_rolename;
struct AUTH { LEX_CSTRING plugin, auth_string, salt; } *auth;
uint nauth;
bool account_locked;
bool alloc_auth(MEM_ROOT *root, uint n)
{
......@@ -864,6 +865,8 @@ class User_table: public Grant_table_base
virtual int set_is_role (bool x) const = 0;
virtual const char* get_default_role (MEM_ROOT *root) const = 0;
virtual int set_default_role (const char *s, size_t l) const = 0;
virtual bool get_account_locked () const = 0;
virtual int set_account_locked (bool x) const = 0;
virtual ~User_table() {}
private:
......@@ -1123,7 +1126,22 @@ class User_table_tabular: public User_table
return f->store(s, l, system_charset_info);
else
return 1;
};
}
/* On a MariaDB 10.3 user table, the account locking accessors will try to
get the content of the max_statement_time column, but they will fail due
to the typecheck in get_field. */
bool get_account_locked () const
{
Field *f= get_field(end_priv_columns + 13, MYSQL_TYPE_ENUM);
return f ? f->val_int()-1 : 0;
}
int set_account_locked (bool x) const
{
if (Field *f= get_field(end_priv_columns + 13, MYSQL_TYPE_ENUM))
return f->store(x+1, 0);
else
return 1;
}
virtual ~User_table_tabular() {}
private:
......@@ -1416,6 +1434,10 @@ class User_table_json: public User_table
{ return get_str_value(root, "default_role"); }
int set_default_role (const char *s, size_t l) const
{ return set_str_value("default_role", s, l); }
bool get_account_locked () const
{ return get_bool_value("account_locked"); }
int set_account_locked (bool x) const
{ return set_bool_value("account_locked", x); }
~User_table_json() {}
private:
......@@ -2260,6 +2282,8 @@ static bool acl_load(THD *thd, const Grant_tables& tables)
my_init_dynamic_array(&user.role_grants, sizeof(ACL_ROLE *), 0, 8, MYF(0));
user.account_locked= user_table.get_account_locked();
if (is_role)
{
if (is_invalid_role_name(username))
......@@ -4327,6 +4351,13 @@ static int replace_user_table(THD *thd, const User_table &user_table,
mqh_used= (mqh_used || lex->mqh.questions || lex->mqh.updates ||
lex->mqh.conn_per_hour || lex->mqh.user_conn ||
lex->mqh.max_statement_time != 0.0);
if (lex->account_options.account_locked != ACCOUNTLOCK_UNSPECIFIED)
{
bool lock_value= lex->account_options.account_locked == ACCOUNTLOCK_LOCKED;
user_table.set_account_locked(lock_value);
new_acl_user.account_locked= lock_value;
}
}
if (old_row_exists)
......@@ -8780,6 +8811,9 @@ bool mysql_show_create_user(THD *thd, LEX_USER *lex_user)
add_user_parameters(&result, acl_user, false);
if (acl_user->account_locked)
result.append(STRING_WITH_LEN(" ACCOUNT LOCK"));
protocol->prepare_for_resend();
protocol->store(result.ptr(), result.length(), result.charset());
if (protocol->write())
......@@ -13641,6 +13675,12 @@ bool acl_authenticate(THD *thd, uint com_change_user_pkt_len)
DBUG_RETURN(1);
}
if (acl_user->account_locked) {
status_var_increment(denied_connections);
my_error(ER_ACCOUNT_HAS_BEEN_LOCKED, MYF(0));
DBUG_RETURN(1);
}
/*
Don't allow the user to connect if he has done too many queries.
As we are testing max_user_connections == 0 here, it means that we
......
......@@ -2939,6 +2939,27 @@ class Delete_plan : public Update_plan
Explain_delete* save_explain_delete_data(MEM_ROOT *mem_root, THD *thd);
};
enum account_lock_type
{
ACCOUNTLOCK_UNSPECIFIED,
ACCOUNTLOCK_LOCKED,
ACCOUNTLOCK_UNLOCKED
};
struct Account_options
{
Account_options()
: account_locked(ACCOUNTLOCK_UNSPECIFIED)
{ }
void reset()
{
account_locked= ACCOUNTLOCK_UNSPECIFIED;
}
account_lock_type account_locked;
};
class Query_arena_memroot;
/* The state of the lex parsing. This is saved in the THD struct */
......@@ -3030,6 +3051,9 @@ struct LEX: public Query_tables_list
*/
LEX_USER *definer;
/* Used in ALTER/CREATE user to store account locking options */
Account_options account_options;
Table_type table_type; /* Used for SHOW CREATE */
List<Key_part_spec> ref_list;
List<LEX_USER> users_list;
......
......@@ -1151,6 +1151,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize);
Non-reserved keywords
*/
%token <kwd> ACCOUNT_SYM /* MYSQL */
%token <kwd> ACTION /* SQL-2003-N */
%token <kwd> ADMIN_SYM /* SQL-2003-N */
%token <kwd> ADDDATE_SYM /* MYSQL-FUNC */
......@@ -2911,7 +2912,7 @@ create:
Lex->pop_select(); //main select
}
| create_or_replace USER_SYM opt_if_not_exists clear_privileges
grant_list opt_require_clause opt_resource_options
grant_list opt_require_clause opt_resource_options opt_account_locking
{
if (unlikely(Lex->set_command_with_check(SQLCOM_CREATE_USER,
$1 | $3)))
......@@ -3318,6 +3319,7 @@ clear_privileges:
lex->ssl_type= SSL_TYPE_NOT_SPECIFIED;
lex->ssl_cipher= lex->x509_subject= lex->x509_issuer= 0;
bzero((char *)&(lex->mqh),sizeof(lex->mqh));
lex->account_options.reset();
}
;
......@@ -7979,7 +7981,7 @@ alter:
} OPTIONS_SYM '(' server_options_list ')' { }
/* ALTER USER foo is allowed for MySQL compatibility. */
| ALTER opt_if_exists USER_SYM clear_privileges grant_list
opt_require_clause opt_resource_options
opt_require_clause opt_resource_options opt_account_locking
{
Lex->create_info.set($2);
Lex->sql_command= SQLCOM_ALTER_USER;
......@@ -8018,6 +8020,18 @@ alter:
}
;
opt_account_locking:
/* Nothing */ {}
| ACCOUNT_SYM LOCK_SYM
{
Lex->account_options.account_locked= ACCOUNTLOCK_LOCKED;
}
| ACCOUNT_SYM UNLOCK_SYM
{
Lex->account_options.account_locked= ACCOUNTLOCK_UNLOCKED;
}
;
ev_alter_on_schedule_completion:
/* empty */ { $$= 0;}
| ON SCHEDULE_SYM ev_schedule_time { $$= 1; }
......@@ -15855,6 +15869,7 @@ keyword_data_type:
*/
keyword_sp_var_and_label:
ACTION
| ACCOUNT_SYM
| ADDDATE_SYM
| ADMIN_SYM
| AFTER_SYM
......
......@@ -646,6 +646,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize);
Non-reserved keywords
*/
%token <kwd> ACCOUNT_SYM /* MYSQL */
%token <kwd> ACTION /* SQL-2003-N */
%token <kwd> ADMIN_SYM /* SQL-2003-N */
%token <kwd> ADDDATE_SYM /* MYSQL-FUNC */
......@@ -2417,7 +2418,7 @@ create:
Lex->pop_select(); //main select
}
| create_or_replace USER_SYM opt_if_not_exists clear_privileges
grant_list opt_require_clause opt_resource_options
grant_list opt_require_clause opt_resource_options opt_account_locking
{
if (unlikely(Lex->set_command_with_check(SQLCOM_CREATE_USER,
$1 | $3)))
......@@ -8009,7 +8010,7 @@ alter:
} OPTIONS_SYM '(' server_options_list ')' { }
/* ALTER USER foo is allowed for MySQL compatibility. */
| ALTER opt_if_exists USER_SYM clear_privileges grant_list
opt_require_clause opt_resource_options
opt_require_clause opt_resource_options opt_account_locking
{
Lex->create_info.set($2);
Lex->sql_command= SQLCOM_ALTER_USER;
......@@ -8048,6 +8049,18 @@ alter:
}
;
opt_account_locking:
/* Nothing */ {}
| ACCOUNT_SYM LOCK_SYM
{
Lex->account_options.account_locked= ACCOUNTLOCK_LOCKED;
}
| ACCOUNT_SYM UNLOCK_SYM
{
Lex->account_options.account_locked= ACCOUNTLOCK_UNLOCKED;
}
;
ev_alter_on_schedule_completion:
/* empty */ { $$= 0;}
| ON SCHEDULE_SYM ev_schedule_time { $$= 1; }
......@@ -15943,6 +15956,7 @@ keyword_data_type:
*/
keyword_sp_var_and_label:
ACTION
| ACCOUNT_SYM
| ADDDATE_SYM
| ADMIN_SYM
| AFTER_SYM
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment