diff --git a/mysql-test/main/lock_user.result b/mysql-test/main/lock_user.result new file mode 100644 index 00000000000..a8740e8ad37 --- /dev/null +++ b/mysql-test/main/lock_user.result @@ -0,0 +1,134 @@ +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; diff --git a/mysql-test/main/lock_user.test b/mysql-test/main/lock_user.test new file mode 100644 index 00000000000..366c34ecea8 --- /dev/null +++ b/mysql-test/main/lock_user.test @@ -0,0 +1,142 @@ +# +# 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; + diff --git a/mysql-test/main/system_mysql_db_507.result b/mysql-test/main/system_mysql_db_507.result index 1fa4af66719..bf4d3115da5 100644 --- a/mysql-test/main/system_mysql_db_507.result +++ b/mysql-test/main/system_mysql_db_507.result @@ -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. # diff --git a/mysql-test/main/system_mysql_db_507.test b/mysql-test/main/system_mysql_db_507.test index b57a2a09c8e..bb8163f6ebe 100644 --- a/mysql-test/main/system_mysql_db_507.test +++ b/mysql-test/main/system_mysql_db_507.test @@ -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 # diff --git a/scripts/mysql_system_tables_fix.sql b/scripts/mysql_system_tables_fix.sql index fe0b9c01600..381f5356575 100644 --- a/scripts/mysql_system_tables_fix.sql +++ b/scripts/mysql_system_tables_fix.sql @@ -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; diff --git a/sql/lex.h b/sql/lex.h index 5ffe07fa415..bcb085279b3 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -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)}, diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index e09afebe074..f80ae99ac50 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -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" diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index ad9e2b446ed..9b457963a5e 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -152,6 +152,7 @@ public: 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 diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 0fa1d96e626..d94e0b0fca5 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -2939,6 +2939,27 @@ public: 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 @@ public: */ 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 ref_list; List users_list; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 2077ea52557..52150fb0619 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1151,6 +1151,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); Non-reserved keywords */ +%token ACCOUNT_SYM /* MYSQL */ %token ACTION /* SQL-2003-N */ %token ADMIN_SYM /* SQL-2003-N */ %token 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 diff --git a/sql/sql_yacc_ora.yy b/sql/sql_yacc_ora.yy index 4e14be598ce..99762e6aeb8 100644 --- a/sql/sql_yacc_ora.yy +++ b/sql/sql_yacc_ora.yy @@ -646,6 +646,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); Non-reserved keywords */ +%token ACCOUNT_SYM /* MYSQL */ %token ACTION /* SQL-2003-N */ %token ADMIN_SYM /* SQL-2003-N */ %token 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