From d229b4af0e5dde64a73c95ef8f81bad2adb69e37 Mon Sep 17 00:00:00 2001 From: Nikita Malyavin Date: Thu, 29 Feb 2024 16:49:18 +0100 Subject: [PATCH] MDEV-23729 MDEV-32218 INFORMATION_SCHEMA table for user data * A new table INFORMATION_SCHEMA.USERS is introduced. * It stores auxiliary user data * An unprivileged user can access their own data, and that is the main difference with what mysql.global_priv provides * The fields are currently: USER, PASSWORD_ERRORS, PASSWORD_EXPIRATION_TIME * If password_errors is ignored for the user, PASSWORD_ERRORS is NULL * PASSWORD_EXPIRATION_TIME is a timestamp with exact point in time, calculated from password_last_changed and password_lifetime (i.e. days) stored for the user --- .../main/information_schema_stats.result | 83 +++++++++++++++++ mysql-test/main/information_schema_stats.test | 86 ++++++++++++++++++ sql/handler.h | 1 + sql/sql_acl.cc | 89 +++++++++++++++++++ sql/sql_acl.h | 1 + sql/sql_show.cc | 3 + 6 files changed, 263 insertions(+) diff --git a/mysql-test/main/information_schema_stats.result b/mysql-test/main/information_schema_stats.result index 69755f54faf..e455aaa79d6 100644 --- a/mysql-test/main/information_schema_stats.result +++ b/mysql-test/main/information_schema_stats.result @@ -83,3 +83,86 @@ TABLE_SCHEMA TABLE_NAME INDEX_NAME ROWS_READ QUERIES select * from information_schema.table_statistics where table_schema='test' and table_name='just_a_test'; TABLE_SCHEMA TABLE_NAME ROWS_READ ROWS_CHANGED ROWS_CHANGED_X_INDEXES ROWS_INSERTED ROWS_UPDATED ROWS_DELETED KEY_READ_HITS KEY_READ_MISSES set global userstat=@save_userstat; +# +# MDEV-23729 INFORMATION_SCHEMA Table info. about user locked due to +# max_password_errors +# +# MDEV-32218 message to notify end-user N-days prior the password get +# expired +# +set @old_max_password_errors=@@max_password_errors; +set global max_password_errors=2; +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'root'@'neo' 0 NULL +set timestamp= 123; +create user nice_user; +create user naughty_user identified by 'naughty_user_passwd'; +alter user naughty_user password expire interval 10 day; +select 3600*24; +3600*24 +86400 +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'naughty_user'@'%' 0 864123 +'nice_user'@'%' 0 NULL +'root'@HOSTNAME 0 NULL +alter user nice_user password expire interval 10 day; +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'naughty_user'@'%' 0 864123 +'nice_user'@'%' 0 864123 +'root'@HOSTNAME 0 NULL +connect(localhost,naughty_user,wrong_passwd,test,MASTER_PORT,MASTER_SOCKET); +connect con1, localhost, naughty_user, wrong_passwd; +ERROR 28000: Access denied for user 'naughty_user'@'localhost' (using password: YES) +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'naughty_user'@'%' 1 864123 +'nice_user'@'%' 0 864123 +'root'@HOSTNAME 0 NULL +connect(localhost,naughty_user,wrong_passwd,test,MASTER_PORT,MASTER_SOCKET); +connect con1, localhost, naughty_user, wrong_passwd; +ERROR 28000: Access denied for user 'naughty_user'@'localhost' (using password: YES) +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'naughty_user'@'%' 2 864123 +'nice_user'@'%' 0 864123 +'root'@HOSTNAME 0 NULL +# Show all users that are blocked due to max_password_errors reached. +select user from information_schema.users +where password_errors >= @@global.max_password_errors; +user +'naughty_user'@'%' +set global max_password_errors=3; +connect con1, localhost, naughty_user, naughty_user_passwd; +connection default; +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'mariadb.sys'@'localhost' 0 NULL +'naughty_user'@'%' 0 864123 +'nice_user'@'%' 0 864123 +'root'@HOSTNAME 0 NULL +disconnect con1; +# Test unprivileged output +connect con2, localhost, nice_user; +set timestamp= 123; +set password= password('nice_passwd'); +select * from information_schema.users; +USER PASSWORD_ERRORS PASSWORD_EXPIRATION_TIME +'nice_user'@'%' 0 864123 +# Delete user while some connection is still alive, then select. +connection default; +drop user nice_user; +connection con2; +select * from information_schema.users; +ERROR 0L000: The current user is invalid +disconnect con2; +connection default; +drop user naughty_user; +set global max_password_errors=@old_max_password_errors; diff --git a/mysql-test/main/information_schema_stats.test b/mysql-test/main/information_schema_stats.test index 7f8c4c3aece..4a575213a49 100644 --- a/mysql-test/main/information_schema_stats.test +++ b/mysql-test/main/information_schema_stats.test @@ -54,3 +54,89 @@ select * from information_schema.index_statistics where table_schema='test' and select * from information_schema.table_statistics where table_schema='test' and table_name='just_a_test'; set global userstat=@save_userstat; --enable_ps2_protocol + +--echo # +--echo # MDEV-23729 INFORMATION_SCHEMA Table info. about user locked due to +--echo # max_password_errors +--echo # +--echo # MDEV-32218 message to notify end-user N-days prior the password get +--echo # expired +--echo # + +set @old_max_password_errors=@@max_password_errors; +set global max_password_errors=2; + +select * from information_schema.users; + +let $hostname= `select concat('@\'', @@hostname, '\'')`; +# set the password_last_changed value +set timestamp= 123; + +create user nice_user; +create user naughty_user identified by 'naughty_user_passwd'; + +alter user naughty_user password expire interval 10 day; + +select 3600*24; +--replace_result $hostname @HOSTNAME +eval select * from information_schema.users; + +alter user nice_user password expire interval 10 day; +--replace_result $hostname @HOSTNAME +select * from information_schema.users; + +--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT +--error ER_ACCESS_DENIED_ERROR +connect(con1, localhost, naughty_user, wrong_passwd); + +--replace_result $hostname @HOSTNAME +select * from information_schema.users; + +--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT +--error ER_ACCESS_DENIED_ERROR +connect(con1, localhost, naughty_user, wrong_passwd); + +--replace_result $hostname @HOSTNAME +select * from information_schema.users; + + +--echo # Show all users that are blocked due to max_password_errors reached. +select user from information_schema.users +where password_errors >= @@global.max_password_errors; + + +set global max_password_errors=3; + +connect(con1, localhost, naughty_user, naughty_user_passwd); +connection default; + +--replace_result $hostname @HOSTNAME +select * from information_schema.users; +disconnect con1; + +--echo # Test unprivileged output + +connect(con2, localhost, nice_user); +set timestamp= 123; +# timestamp was normal at the login moment, so the password was expired +set password= password('nice_passwd'); + +--replace_result $hostname @HOSTNAME +select * from information_schema.users; + +--echo # Delete user while some connection is still alive, then select. +connection default; +drop user nice_user; +connection con2; +# and here you are, select from your table +--error ER_INVALID_CURRENT_USER +select * from information_schema.users; + +disconnect con2; +connection default; +drop user naughty_user; +set global max_password_errors=@old_max_password_errors; + +# +# End of 11.5 tests +# diff --git a/sql/handler.h b/sql/handler.h index e5f39436a4b..37b403c942c 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1110,6 +1110,7 @@ enum enum_schema_tables SCH_TABLE_NAMES, SCH_TABLE_PRIVILEGES, SCH_TRIGGERS, + SCH_USERS, SCH_USER_PRIVILEGES, SCH_VIEWS, SCH_ENUM_SIZE diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index ecd10f3a4fb..04661768c68 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -13017,6 +13017,95 @@ err: #endif } +namespace Show +{ + ST_FIELD_INFO users_fields_info[] = + { + Column("USER", Userhost(), NOT_NULL), + Column("PASSWORD_ERRORS", SLonglong(), NULLABLE), + Column("PASSWORD_EXPIRATION_TIME", SLonglong(), NULLABLE), + CEnd() + }; +}; + +static bool ignore_max_password_errors(const ACL_USER *acl_user); + +static int fill_users_schema_record(THD *thd, TABLE * table, ACL_USER *user) +{ + ulonglong lifetime= user->password_lifetime < 0 + ? default_password_lifetime + : user->password_lifetime; + + bool ignore_password_errors= ignore_max_password_errors(user); + bool ignore_expiration_date= lifetime == 0; + + /* Skip user if nothing to show */ + if (ignore_password_errors && ignore_expiration_date) + return 0; + + Grantee_str grantee(user->user, + Lex_cstring_strlen(safe_str(user->host.hostname))); + table->field[0]->store(grantee, strlen(grantee), system_charset_info); + if (ignore_password_errors) + { + table->field[1]->set_null(); + } + else + { + table->field[1]->set_notnull(); + table->field[1]->store(user->password_errors); + } + if (ignore_expiration_date) + { + table->field[2]->set_null(); + } + else + { + table->field[2]->set_notnull(); + table->field[2]->store(user->password_last_changed + + user->password_lifetime * 3600 * 24, true); + } + + return schema_table_store_record(thd, table); +} + +int fill_users_schema_table(THD *thd, TABLE_LIST *tables, COND *cond) +{ + int res= 0; +#ifndef NO_EMBEDDED_ACCESS_CHECKS + bool see_whole_table= check_access(thd, SELECT_ACL, "mysql", NULL, NULL, + true, true) == 0; + TABLE *table= tables->table; + + if (!see_whole_table) + { + Security_context *sctx= thd->security_ctx; + mysql_mutex_lock(&acl_cache->lock); + ACL_USER *cur_user= find_user_exact(Lex_cstring_strlen(sctx->priv_host), + Lex_cstring_strlen(sctx->priv_user)); + if (!cur_user) + { + mysql_mutex_unlock(&acl_cache->lock); + my_error(ER_INVALID_CURRENT_USER, MYF(0)); + return 1; + } + + res= fill_users_schema_record(thd, table, cur_user); + mysql_mutex_unlock(&acl_cache->lock); + return res; + } + + mysql_mutex_lock(&acl_cache->lock); + for (size_t i= 0; res == 0 && i < acl_users.elements; i++) + { + ACL_USER *user= dynamic_element(&acl_users, i, ACL_USER*); + res= fill_users_schema_record(thd, table, user); + } + mysql_mutex_unlock(&acl_cache->lock); +#endif + return res; +} + #ifndef NO_EMBEDDED_ACCESS_CHECKS /* diff --git a/sql/sql_acl.h b/sql/sql_acl.h index b23fcb45f5d..cc774263c91 100644 --- a/sql/sql_acl.h +++ b/sql/sql_acl.h @@ -150,6 +150,7 @@ bool check_routine_level_acl(THD *thd, privilege_t acl, const char *db, const char *name, const Sp_handler *sph); bool is_acl_user(const LEX_CSTRING &host, const LEX_CSTRING &user); +int fill_users_schema_table(THD *thd, TABLE_LIST *tables, COND *cond); int fill_schema_user_privileges(THD *thd, TABLE_LIST *tables, COND *cond); int fill_schema_schema_privileges(THD *thd, TABLE_LIST *tables, COND *cond); int fill_schema_table_privileges(THD *thd, TABLE_LIST *tables, COND *cond); diff --git a/sql/sql_show.cc b/sql/sql_show.cc index 48bc51c9cd4..aa494b90d36 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -10243,6 +10243,7 @@ ST_FIELD_INFO files_fields_info[]= CEnd() }; + extern ST_FIELD_INFO users_fields_info[]; }; // namespace Show @@ -10556,6 +10557,8 @@ ST_SCHEMA_TABLE schema_tables[]= {"TRIGGERS"_Lex_ident_i_s_table, Show::triggers_fields_info, 0, get_all_tables, make_old_format, get_schema_triggers_record, 5, 6, 0, OPEN_TRIGGER_ONLY|OPTIMIZE_I_S_TABLE}, + {"USERS"_Lex_ident_i_s_table, Show::users_fields_info, 0, fill_users_schema_table, + 0, 0, -1, -1, 0, 0}, {"USER_PRIVILEGES"_Lex_ident_i_s_table, Show::user_privileges_fields_info, 0, fill_schema_user_privileges, 0, 0, -1, -1, 0, 0},