From 500a7a2e91cea96b58d4521ba138ba1ce60ef58b Mon Sep 17 00:00:00 2001 From: Anway Durge Date: Sun, 12 Jul 2026 14:24:25 +0530 Subject: [PATCH] MDEV-31535: Add privilege-based fast path for SHOW DATABASES listing For users without global database-listing privileges, build the database list from in-memory ACL tables instead of scanning the data directory when grants are exact-name only. Mirror acl_get_all3()/check_grant_db() grantee handling (user, active role, PUBLIC), scope the optimization to SCHEMATA, preserve find_files() ordering, and add MTR coverage. --- mysql-test/main/mdev_31535.result | 142 ++++++++++++++++++++++++++++++ mysql-test/main/mdev_31535.test | 137 ++++++++++++++++++++++++++++ sql/sql_acl.cc | 90 ++++++++++++++++++- sql/sql_acl.h | 1 + sql/sql_show.cc | 28 +++++- 5 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 mysql-test/main/mdev_31535.result create mode 100644 mysql-test/main/mdev_31535.test diff --git a/mysql-test/main/mdev_31535.result b/mysql-test/main/mdev_31535.result new file mode 100644 index 0000000000000..54ac342221178 --- /dev/null +++ b/mysql-test/main/mdev_31535.result @@ -0,0 +1,142 @@ +# +# MDEV-31535: Optimize directory listing for SHOW DATABASES / +# INFORMATION_SCHEMA.SCHEMATA by using a privilege-based lookup +# (get_acl_databases_for_user) instead of a full filesystem scan, +# for users without global database-listing privileges. +# +drop database if exists mdev31535a; +drop database if exists mdev31535b; +drop database if exists mdev31535pub; +drop database if exists mdev31535roledb; +drop database if exists mdev31535_wild_a; +drop database if exists mdev31535_wild_b; +create database mdev31535a; +create database mdev31535b; +create table mdev31535b.t1 (a int); +create database mdev31535_wild_a; +create database mdev31535_wild_b; +# +# Case 1: user with an explicit mysql.db grant on exactly one database +# (no wildcard metacharacter in the name) is resolved by the fast path +# and sees only that database plus information_schema. +# +create user mdev31535_u1@localhost; +grant select on mdev31535a.* to mdev31535_u1@localhost; +connect con1,localhost,mdev31535_u1,,; +connection con1; +show databases; +Database +information_schema +mdev31535a +select schema_name from information_schema.schemata; +schema_name +information_schema +mdev31535a +connection default; +disconnect con1; +# +# Case 2: user with only a table-level grant (no mysql.db row) still +# sees that database, via the column_priv_hash pass of the fast path. +# +create user mdev31535_u2@localhost; +grant select on mdev31535b.t1 to mdev31535_u2@localhost; +connect con2,localhost,mdev31535_u2,,; +connection con2; +show databases; +Database +information_schema +mdev31535b +select schema_name from information_schema.schemata; +schema_name +information_schema +mdev31535b +connection default; +disconnect con2; +# +# Case 3: user whose db grant contains an SQL wildcard ('_' or '%') can +# match an unknown set of on-disk databases, so the fast path declines +# and falls back to the legacy find_files() scan. +# +create user mdev31535_u3@localhost; +grant select on `mdev31535\_wild_%`.* to mdev31535_u3@localhost; +connect con3,localhost,mdev31535_u3,,; +connection con3; +show databases; +Database +information_schema +mdev31535_wild_a +mdev31535_wild_b +connection default; +disconnect con3; +# +# Case 4: a database granted to a role that is the user's default role +# must be visible, alongside the user's own database. This mirrors +# acl_get_all3()/check_grant_db(), which also consider the active role. +# +create role mdev31535role; +create database mdev31535roledb; +grant select on mdev31535roledb.* to mdev31535role; +create user mdev31535_u4@localhost; +grant select on mdev31535a.* to mdev31535_u4@localhost; +grant mdev31535role to mdev31535_u4@localhost; +set default role mdev31535role for mdev31535_u4@localhost; +connect con4,localhost,mdev31535_u4,,; +connection con4; +select current_role; +current_role +mdev31535role +show databases; +Database +information_schema +mdev31535a +mdev31535roledb +select schema_name from information_schema.schemata; +schema_name +information_schema +mdev31535a +mdev31535roledb +connection default; +disconnect con4; +# +# Case 5: a database granted via the PUBLIC role must be visible to any +# user through the fast path, exactly as the legacy per-database ACL +# filter (acl_get_all3) would admit it. Before this fix the fast path +# ignored PUBLIC grants and hid such databases. +# +create user mdev31535_u5@localhost; +create database mdev31535pub; +grant select on mdev31535a.* to mdev31535_u5@localhost; +grant select on mdev31535pub.* to public; +connect con5,localhost,mdev31535_u5,,; +connection con5; +show databases; +Database +information_schema +mdev31535a +mdev31535pub +select schema_name from information_schema.schemata; +schema_name +information_schema +mdev31535a +mdev31535pub +connection default; +disconnect con5; +revoke select on mdev31535pub.* from public; +drop role mdev31535role; +drop user mdev31535_u1@localhost; +drop user mdev31535_u2@localhost; +drop user mdev31535_u3@localhost; +drop user mdev31535_u4@localhost; +drop user mdev31535_u5@localhost; +drop database mdev31535a; +drop database mdev31535b; +drop database mdev31535pub; +drop database mdev31535roledb; +drop database mdev31535_wild_a; +drop database mdev31535_wild_b; +# Granting to PUBLIC auto-creates the PUBLIC role row in mysql.global_priv +# on the minimal test datadir; DROP ROLE PUBLIC is not permitted, so +# remove the leftover role directly to keep server state unchanged. +delete from mysql.global_priv where User='PUBLIC' and Host=''; +flush privileges; +# End of MDEV-31535 tests diff --git a/mysql-test/main/mdev_31535.test b/mysql-test/main/mdev_31535.test new file mode 100644 index 0000000000000..3d1e733a956b2 --- /dev/null +++ b/mysql-test/main/mdev_31535.test @@ -0,0 +1,137 @@ +--source include/not_embedded.inc + +--echo # +--echo # MDEV-31535: Optimize directory listing for SHOW DATABASES / +--echo # INFORMATION_SCHEMA.SCHEMATA by using a privilege-based lookup +--echo # (get_acl_databases_for_user) instead of a full filesystem scan, +--echo # for users without global database-listing privileges. +--echo # + +--disable_warnings +drop database if exists mdev31535a; +drop database if exists mdev31535b; +drop database if exists mdev31535pub; +drop database if exists mdev31535roledb; +drop database if exists mdev31535_wild_a; +drop database if exists mdev31535_wild_b; +--enable_warnings + +create database mdev31535a; +create database mdev31535b; +create table mdev31535b.t1 (a int); +create database mdev31535_wild_a; +create database mdev31535_wild_b; + +--echo # +--echo # Case 1: user with an explicit mysql.db grant on exactly one database +--echo # (no wildcard metacharacter in the name) is resolved by the fast path +--echo # and sees only that database plus information_schema. +--echo # +create user mdev31535_u1@localhost; +grant select on mdev31535a.* to mdev31535_u1@localhost; + +connect (con1,localhost,mdev31535_u1,,); +connection con1; +--sorted_result +show databases; +--sorted_result +select schema_name from information_schema.schemata; +connection default; +disconnect con1; + +--echo # +--echo # Case 2: user with only a table-level grant (no mysql.db row) still +--echo # sees that database, via the column_priv_hash pass of the fast path. +--echo # +create user mdev31535_u2@localhost; +grant select on mdev31535b.t1 to mdev31535_u2@localhost; + +connect (con2,localhost,mdev31535_u2,,); +connection con2; +--sorted_result +show databases; +--sorted_result +select schema_name from information_schema.schemata; +connection default; +disconnect con2; + +--echo # +--echo # Case 3: user whose db grant contains an SQL wildcard ('_' or '%') can +--echo # match an unknown set of on-disk databases, so the fast path declines +--echo # and falls back to the legacy find_files() scan. +--echo # +create user mdev31535_u3@localhost; +grant select on `mdev31535\_wild_%`.* to mdev31535_u3@localhost; + +connect (con3,localhost,mdev31535_u3,,); +connection con3; +--sorted_result +show databases; +connection default; +disconnect con3; + +--echo # +--echo # Case 4: a database granted to a role that is the user's default role +--echo # must be visible, alongside the user's own database. This mirrors +--echo # acl_get_all3()/check_grant_db(), which also consider the active role. +--echo # +create role mdev31535role; +create database mdev31535roledb; +grant select on mdev31535roledb.* to mdev31535role; +create user mdev31535_u4@localhost; +grant select on mdev31535a.* to mdev31535_u4@localhost; +grant mdev31535role to mdev31535_u4@localhost; +set default role mdev31535role for mdev31535_u4@localhost; + +connect (con4,localhost,mdev31535_u4,,); +connection con4; +select current_role; +--sorted_result +show databases; +--sorted_result +select schema_name from information_schema.schemata; +connection default; +disconnect con4; + +--echo # +--echo # Case 5: a database granted via the PUBLIC role must be visible to any +--echo # user through the fast path, exactly as the legacy per-database ACL +--echo # filter (acl_get_all3) would admit it. Before this fix the fast path +--echo # ignored PUBLIC grants and hid such databases. +--echo # +create user mdev31535_u5@localhost; +create database mdev31535pub; +grant select on mdev31535a.* to mdev31535_u5@localhost; +grant select on mdev31535pub.* to public; + +connect (con5,localhost,mdev31535_u5,,); +connection con5; +--sorted_result +show databases; +--sorted_result +select schema_name from information_schema.schemata; +connection default; +disconnect con5; + +revoke select on mdev31535pub.* from public; + +drop role mdev31535role; +drop user mdev31535_u1@localhost; +drop user mdev31535_u2@localhost; +drop user mdev31535_u3@localhost; +drop user mdev31535_u4@localhost; +drop user mdev31535_u5@localhost; +drop database mdev31535a; +drop database mdev31535b; +drop database mdev31535pub; +drop database mdev31535roledb; +drop database mdev31535_wild_a; +drop database mdev31535_wild_b; + +--echo # Granting to PUBLIC auto-creates the PUBLIC role row in mysql.global_priv +--echo # on the minimal test datadir; DROP ROLE PUBLIC is not permitted, so +--echo # remove the leftover role directly to keep server state unchanged. +delete from mysql.global_priv where User='PUBLIC' and Host=''; +flush privileges; + +--echo # End of MDEV-31535 tests diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 0d8640a112a7c..3d60d399b96d5 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -3885,7 +3885,6 @@ privilege_t acl_get_all3(Security_context *sctx, const char *db, return access; } - /* Check if there are any possible matching entries for this host @@ -5555,6 +5554,95 @@ class GRANT_TABLE :public GRANT_NAME } }; +/* + Collect, from the in-memory privilege tables, the exact database names the + current user is allowed to see in SHOW DATABASES / I_S.SCHEMATA, so the caller + can skip a full data-directory scan (find_files()). +*/ +bool get_acl_databases_for_user(THD *thd, Dynamic_array *files) +{ + Security_context *sctx= thd->security_ctx; + const char *host= sctx->host; + const char *ip= sctx->ip; + const char *user= sctx->priv_user; + const char *role= sctx->priv_role; + const size_t base= files->elements(); + + auto append_db= [&](const char *db) -> bool + { + LEX_CSTRING *db_name= (LEX_CSTRING*) thd->alloc(sizeof(LEX_CSTRING)); + if (!db_name) + return true; + db_name->length= strlen(db); + db_name->str= thd->strmake(db, db_name->length); + for (size_t j= 0; j < files->elements(); j++) + if (!strcmp(files->at(j)->str, db_name->str)) + return false; + return files->append_val(db_name); + }; + + mysql_mutex_lock(&acl_cache->lock); + + for (size_t i= 0; i < acl_dbs.elements(); i++) + { + ACL_DB *acl_db= &acl_dbs.at(i); + if (acl_db->access == NO_ACL) + continue; + + bool applies; + if (!strcmp(acl_db->user, user)) + applies= compare_hostname(&acl_db->host, host, ip); + else if (role[0] && !strcmp(acl_db->user, role)) + applies= true; + else if (acl_public && !strcmp(acl_db->user, public_name.str)) + applies= true; + else + applies= false; + if (!applies) + continue; + + if (acl_db->db && strpbrk(acl_db->db, "%_")) + { + mysql_mutex_unlock(&acl_cache->lock); + files->elements(base); + return true; + } + if (acl_db->db && append_db(acl_db->db)) + { + mysql_mutex_unlock(&acl_cache->lock); + files->elements(base); + return true; + } + } + + for (uint i= 0; i < column_priv_hash.records; i++) + { + GRANT_TABLE *grant= (GRANT_TABLE*) my_hash_element(&column_priv_hash, i); + if (grant->privs == NO_ACL && grant->cols == NO_ACL) + continue; + + bool applies; + if (!strcmp(grant->user, user)) + applies= compare_hostname(&grant->host, host, ip); + else if (role[0] && !strcmp(grant->user, role)) + applies= true; + else + applies= false; + if (!applies) + continue; + + if (grant->db && append_db(grant->db)) + { + mysql_mutex_unlock(&acl_cache->lock); + files->elements(base); + return true; + } + } + + mysql_mutex_unlock(&acl_cache->lock); + return false; +} + privilege_t GRANT_INFO::all_privilege() { diff --git a/sql/sql_acl.h b/sql/sql_acl.h index 6688242f48f2a..4e33136a8837c 100644 --- a/sql/sql_acl.h +++ b/sql/sql_acl.h @@ -80,6 +80,7 @@ bool acl_reload(THD *thd); void acl_free(bool end=0); privilege_t acl_get_all3(Security_context *sctx, const char *db, bool db_is_patern); +bool get_acl_databases_for_user(THD *thd, Dynamic_array *files); bool acl_authenticate(THD *thd, uint com_change_user_pkt_len); bool acl_getroot(Security_context *sctx, const char *user, const char *host, const char *ip, const char *db); diff --git a/sql/sql_show.cc b/sql/sql_show.cc index c506e2ffe4220..69faede8dbd74 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -4459,7 +4459,8 @@ enum enum_schema_tables get_schema_table_idx(ST_SCHEMA_TABLE *schema_table) */ static int make_db_list(THD *thd, Dynamic_array *files, - LOOKUP_FIELD_VALUES *lookup_field_vals) + LOOKUP_FIELD_VALUES *lookup_field_vals, + bool allow_acl_fast_path) { if (lookup_field_vals->wild_db_value) { @@ -4515,10 +4516,29 @@ static int make_db_list(THD *thd, Dynamic_array *files, */ if (files->append_val(&INFORMATION_SCHEMA_NAME)) return 1; + + /* + Fast path: for SHOW DATABASES / I_S.SCHEMATA + */ + if (allow_acl_fast_path && + !(thd->security_ctx->master_access & (DB_ACLS | SHOW_DB_ACL))) + { + if (!get_acl_databases_for_user(thd, files)) + { + Discovered_table_list tl(thd, files, &null_clex_str); + if (is_show_command(thd)) + tl.sort(); +#ifndef DBUG_OFF + else + tl.sort_desc(); +#endif + return 0; + } + } + return find_files(thd, files, 0, mysql_data_home, &null_clex_str); } - struct st_add_schema_table { Dynamic_array *files; @@ -5506,7 +5526,7 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) } } - if (make_db_list(thd, &db_names, &plan->lookup_field_vals)) + if (make_db_list(thd, &db_names, &plan->lookup_field_vals, false)) goto err; for (size_t i=0; i < db_names.elements(); i++) @@ -5687,7 +5707,7 @@ int fill_schema_schemata(THD *thd, TABLE_LIST *tables, COND *cond) DBUG_PRINT("INDEX VALUES",("db_name: %s table_name: %s", lookup_field_vals.db_value.str, lookup_field_vals.table_value.str)); - if (make_db_list(thd, &db_names, &lookup_field_vals)) + if (make_db_list(thd, &db_names, &lookup_field_vals, true)) DBUG_RETURN(1); /*