diff --git a/mysql-test/main/deny_revoke_public.result b/mysql-test/main/deny_revoke_public.result new file mode 100644 index 0000000000000..a82947a61c4b1 --- /dev/null +++ b/mysql-test/main/deny_revoke_public.result @@ -0,0 +1,193 @@ +# +# MDEV-40656 REVOKE DENY FROM PUBLIC can lock itself out +# +# DENY TO PUBLIC denies everyone, including whoever tries to revoke +# it. Allowed anyway when the revoker can UPDATE mysql.global_priv, +# same as hand-editing it. Checked at every scope: global, db, table, +# column, routine. +# +CREATE TABLE test.t1 (id INT, data INT); +INSERT INTO test.t1 VALUES (1, 100); +CREATE PROCEDURE test.p1() BEGIN SELECT 1; END| +# +# Same session: no bypass needed, privileges aren't re-read yet +# +DENY SELECT ON test.t1 TO PUBLIC; +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; +# +# Table level +# +CREATE USER u_table@localhost; +GRANT SELECT ON test.t1 TO u_table@localhost; +DENY SELECT ON test.t1 TO PUBLIC; +connect con_table1, localhost, u_table,,test; +SELECT * FROM t1; +ERROR 42000: SELECT command denied to user 'u_table'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; +ERROR 42000: SELECT, GRANT command denied to user 'u_table'@'localhost' for table `test`.`t1` +disconnect con_table1; +connection default; +GRANT UPDATE ON mysql.global_priv TO u_table@localhost; +connect con_table2, localhost, u_table,,test; +SELECT * FROM t1; +ERROR 42000: SELECT command denied to user 'u_table'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; +SELECT * FROM t1; +id data +1 100 +disconnect con_table2; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_table@localhost; +DROP USER u_table@localhost; +# +# Column level +# +CREATE USER u_col@localhost; +GRANT SELECT ON test.t1 TO u_col@localhost; +DENY SELECT (data) ON test.t1 TO PUBLIC; +connect con_col1, localhost, u_col,,test; +SELECT data FROM t1; +ERROR 42000: SELECT command denied to user 'u_col'@'localhost' for column 'data' in table 't1' +REVOKE DENY SELECT (data) ON test.t1 FROM PUBLIC; +ERROR 42000: GRANT command denied to user 'u_col'@'localhost' for table `test`.`t1` +disconnect con_col1; +connection default; +GRANT UPDATE ON mysql.global_priv TO u_col@localhost; +connect con_col2, localhost, u_col,,test; +SELECT data FROM t1; +ERROR 42000: SELECT command denied to user 'u_col'@'localhost' for column 'data' in table 't1' +REVOKE DENY SELECT (data) ON test.t1 FROM PUBLIC; +SELECT data FROM t1; +data +100 +disconnect con_col2; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_col@localhost; +DROP USER u_col@localhost; +# +# DB level +# +CREATE USER u_db@localhost; +GRANT SELECT ON test.* TO u_db@localhost; +DENY SELECT ON test.* TO PUBLIC; +connect con_db1, localhost, u_db,,; +SELECT * FROM test.t1; +ERROR 42000: SELECT command denied to user 'u_db'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON test.* FROM PUBLIC; +ERROR 42000: Access denied for user 'u_db'@'localhost' to database 'test' +disconnect con_db1; +connection default; +GRANT UPDATE ON mysql.global_priv TO u_db@localhost; +connect con_db2, localhost, u_db,,; +SELECT * FROM test.t1; +ERROR 42000: SELECT command denied to user 'u_db'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON test.* FROM PUBLIC; +SELECT * FROM test.t1; +id data +1 100 +disconnect con_db2; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_db@localhost; +DROP USER u_db@localhost; +# +# Global level +# +CREATE USER u_global@localhost; +GRANT SELECT ON *.* TO u_global@localhost; +DENY SELECT ON *.* TO PUBLIC; +connect con_global1, localhost, u_global,,; +SELECT * FROM test.t1; +ERROR 42000: SELECT command denied to user 'u_global'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON *.* FROM PUBLIC; +ERROR 28000: Access denied for user 'u_global'@'localhost' (using password: NO) +disconnect con_global1; +connection default; +GRANT UPDATE ON mysql.global_priv TO u_global@localhost; +connect con_global2, localhost, u_global,,; +SELECT * FROM test.t1; +ERROR 42000: SELECT command denied to user 'u_global'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON *.* FROM PUBLIC; +disconnect con_global2; +connection default; +# master_access is cached at connect; needs a reconnect to take effect +connect con_global3, localhost, u_global,,; +SELECT * FROM test.t1; +id data +1 100 +disconnect con_global3; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_global@localhost; +DROP USER u_global@localhost; +# +# Routine level +# +CREATE USER u_proc@localhost; +GRANT EXECUTE ON PROCEDURE test.p1 TO u_proc@localhost; +DENY EXECUTE ON PROCEDURE test.p1 TO PUBLIC; +connect con_proc1, localhost, u_proc,,test; +CALL p1(); +ERROR 42000: execute command denied to user 'u_proc'@'localhost' for routine 'test.p1' +REVOKE DENY EXECUTE ON PROCEDURE test.p1 FROM PUBLIC; +ERROR 42000: execute command denied to user 'u_proc'@'localhost' for routine 'test.p1' +disconnect con_proc1; +connection default; +GRANT UPDATE ON mysql.global_priv TO u_proc@localhost; +connect con_proc2, localhost, u_proc,,test; +CALL p1(); +ERROR 42000: execute command denied to user 'u_proc'@'localhost' for routine 'test.p1' +REVOKE DENY EXECUTE ON PROCEDURE test.p1 FROM PUBLIC; +CALL p1(); +1 +1 +disconnect con_proc2; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_proc@localhost; +DROP USER u_proc@localhost; +# +# A role is not PUBLIC: no bypass, even with UPDATE on mysql.global_priv +# +CREATE ROLE r1; +CREATE USER u_role@localhost; +GRANT r1 TO u_role@localhost; +GRANT SELECT ON test.t1 TO r1; +DENY SELECT ON test.t1 TO r1; +GRANT UPDATE ON mysql.global_priv TO u_role@localhost; +connect con_role, localhost, u_role,,; +SET ROLE r1; +SELECT * FROM test.t1; +ERROR 42000: SELECT command denied to user 'u_role'@'localhost' for table `test`.`t1` +REVOKE DENY SELECT ON test.t1 FROM r1; +ERROR 42000: SELECT, GRANT command denied to user 'u_role'@'localhost' for table `test`.`t1` +disconnect con_role; +connection default; +REVOKE UPDATE ON mysql.global_priv FROM u_role@localhost; +REVOKE DENY SELECT ON test.t1 FROM r1; +DROP USER u_role@localhost; +DROP ROLE r1; +# +# Known limitation: denying the bypass's own privilege is unrecoverable +# +DENY UPDATE ON *.* TO PUBLIC; +disconnect default; +connect default, localhost, root,,; +# Should FAIL - root can't UPDATE anywhere either, including this table +UPDATE mysql.global_priv SET Priv=Priv WHERE User='PUBLIC'; +ERROR 42000: UPDATE command denied to user 'root'@'localhost' for table `mysql`.`global_priv` +# Should FAIL - no recovery via REVOKE DENY once UPDATE itself is denied +REVOKE DENY UPDATE ON *.* FROM PUBLIC; +ERROR 28000: Access denied for user 'root'@'localhost' (using password: NO) +# Recovery: DELETE was never denied +DELETE FROM mysql.global_priv WHERE User='PUBLIC'; +FLUSH PRIVILEGES; +disconnect default; +connect default, localhost, root,,; +# UPDATE works again after reconnecting +UPDATE mysql.global_priv SET Priv=Priv WHERE User='root' AND Host='localhost'; +# +# Cleanup +# +DROP PROCEDURE test.p1; +DROP TABLE test.t1; +DELETE FROM mysql.global_priv WHERE User='PUBLIC'; +FLUSH PRIVILEGES; +# End of 13.1 tests diff --git a/mysql-test/main/deny_revoke_public.test b/mysql-test/main/deny_revoke_public.test new file mode 100644 index 0000000000000..9df13f0a9fbed --- /dev/null +++ b/mysql-test/main/deny_revoke_public.test @@ -0,0 +1,227 @@ +--source include/not_embedded.inc + +--echo # +--echo # MDEV-40656 REVOKE DENY FROM PUBLIC can lock itself out +--echo # +--echo # DENY TO PUBLIC denies everyone, including whoever tries to revoke +--echo # it. Allowed anyway when the revoker can UPDATE mysql.global_priv, +--echo # same as hand-editing it. Checked at every scope: global, db, table, +--echo # column, routine. +--echo # + +CREATE TABLE test.t1 (id INT, data INT); +INSERT INTO test.t1 VALUES (1, 100); +DELIMITER |; +CREATE PROCEDURE test.p1() BEGIN SELECT 1; END| +DELIMITER ;| + +--echo # +--echo # Same session: no bypass needed, privileges aren't re-read yet +--echo # +DENY SELECT ON test.t1 TO PUBLIC; +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; + +--echo # +--echo # Table level +--echo # +CREATE USER u_table@localhost; +GRANT SELECT ON test.t1 TO u_table@localhost; +DENY SELECT ON test.t1 TO PUBLIC; + +connect (con_table1, localhost, u_table,,test); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM t1; +--error ER_TABLEACCESS_DENIED_ERROR +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; +disconnect con_table1; +connection default; + +GRANT UPDATE ON mysql.global_priv TO u_table@localhost; + +connect (con_table2, localhost, u_table,,test); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM t1; +REVOKE DENY SELECT ON test.t1 FROM PUBLIC; +SELECT * FROM t1; +disconnect con_table2; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_table@localhost; +DROP USER u_table@localhost; + +--echo # +--echo # Column level +--echo # +CREATE USER u_col@localhost; +GRANT SELECT ON test.t1 TO u_col@localhost; +DENY SELECT (data) ON test.t1 TO PUBLIC; + +connect (con_col1, localhost, u_col,,test); +--error ER_COLUMNACCESS_DENIED_ERROR +SELECT data FROM t1; +--error ER_TABLEACCESS_DENIED_ERROR +REVOKE DENY SELECT (data) ON test.t1 FROM PUBLIC; +disconnect con_col1; +connection default; + +GRANT UPDATE ON mysql.global_priv TO u_col@localhost; + +connect (con_col2, localhost, u_col,,test); +--error ER_COLUMNACCESS_DENIED_ERROR +SELECT data FROM t1; +REVOKE DENY SELECT (data) ON test.t1 FROM PUBLIC; +SELECT data FROM t1; +disconnect con_col2; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_col@localhost; +DROP USER u_col@localhost; + +--echo # +--echo # DB level +--echo # +CREATE USER u_db@localhost; +GRANT SELECT ON test.* TO u_db@localhost; +DENY SELECT ON test.* TO PUBLIC; + +connect (con_db1, localhost, u_db,,); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM test.t1; +--error ER_DBACCESS_DENIED_ERROR +REVOKE DENY SELECT ON test.* FROM PUBLIC; +disconnect con_db1; +connection default; + +GRANT UPDATE ON mysql.global_priv TO u_db@localhost; + +connect (con_db2, localhost, u_db,,); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM test.t1; +REVOKE DENY SELECT ON test.* FROM PUBLIC; +SELECT * FROM test.t1; +disconnect con_db2; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_db@localhost; +DROP USER u_db@localhost; + +--echo # +--echo # Global level +--echo # +CREATE USER u_global@localhost; +GRANT SELECT ON *.* TO u_global@localhost; +DENY SELECT ON *.* TO PUBLIC; + +connect (con_global1, localhost, u_global,,); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM test.t1; +--error ER_ACCESS_DENIED_ERROR +REVOKE DENY SELECT ON *.* FROM PUBLIC; +disconnect con_global1; +connection default; + +GRANT UPDATE ON mysql.global_priv TO u_global@localhost; + +connect (con_global2, localhost, u_global,,); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM test.t1; +REVOKE DENY SELECT ON *.* FROM PUBLIC; +disconnect con_global2; +connection default; + +--echo # master_access is cached at connect; needs a reconnect to take effect +connect (con_global3, localhost, u_global,,); +SELECT * FROM test.t1; +disconnect con_global3; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_global@localhost; +DROP USER u_global@localhost; + +--echo # +--echo # Routine level +--echo # +CREATE USER u_proc@localhost; +GRANT EXECUTE ON PROCEDURE test.p1 TO u_proc@localhost; +DENY EXECUTE ON PROCEDURE test.p1 TO PUBLIC; + +connect (con_proc1, localhost, u_proc,,test); +--error ER_PROCACCESS_DENIED_ERROR +CALL p1(); +--error ER_PROCACCESS_DENIED_ERROR +REVOKE DENY EXECUTE ON PROCEDURE test.p1 FROM PUBLIC; +disconnect con_proc1; +connection default; + +GRANT UPDATE ON mysql.global_priv TO u_proc@localhost; + +connect (con_proc2, localhost, u_proc,,test); +--error ER_PROCACCESS_DENIED_ERROR +CALL p1(); +REVOKE DENY EXECUTE ON PROCEDURE test.p1 FROM PUBLIC; +CALL p1(); +disconnect con_proc2; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_proc@localhost; +DROP USER u_proc@localhost; + +--echo # +--echo # A role is not PUBLIC: no bypass, even with UPDATE on mysql.global_priv +--echo # +CREATE ROLE r1; +CREATE USER u_role@localhost; +GRANT r1 TO u_role@localhost; +GRANT SELECT ON test.t1 TO r1; +DENY SELECT ON test.t1 TO r1; +GRANT UPDATE ON mysql.global_priv TO u_role@localhost; + +connect (con_role, localhost, u_role,,); +SET ROLE r1; +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM test.t1; +--error ER_TABLEACCESS_DENIED_ERROR +REVOKE DENY SELECT ON test.t1 FROM r1; +disconnect con_role; +connection default; + +REVOKE UPDATE ON mysql.global_priv FROM u_role@localhost; +REVOKE DENY SELECT ON test.t1 FROM r1; +DROP USER u_role@localhost; +DROP ROLE r1; + +--echo # +--echo # Known limitation: denying the bypass's own privilege is unrecoverable +--echo # +DENY UPDATE ON *.* TO PUBLIC; + +disconnect default; +connect (default, localhost, root,,); + +--echo # Should FAIL - root can't UPDATE anywhere either, including this table +--error ER_TABLEACCESS_DENIED_ERROR +UPDATE mysql.global_priv SET Priv=Priv WHERE User='PUBLIC'; + +--echo # Should FAIL - no recovery via REVOKE DENY once UPDATE itself is denied +--error ER_ACCESS_DENIED_ERROR +REVOKE DENY UPDATE ON *.* FROM PUBLIC; + +--echo # Recovery: DELETE was never denied +DELETE FROM mysql.global_priv WHERE User='PUBLIC'; +FLUSH PRIVILEGES; + +disconnect default; +connect (default, localhost, root,,); + +--echo # UPDATE works again after reconnecting +UPDATE mysql.global_priv SET Priv=Priv WHERE User='root' AND Host='localhost'; + +--echo # +--echo # Cleanup +--echo # +DROP PROCEDURE test.p1; +DROP TABLE test.t1; +DELETE FROM mysql.global_priv WHERE User='PUBLIC'; +FLUSH PRIVILEGES; + +--echo # End of 13.1 tests diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 1a2af68394868..ec260dcd3b0c9 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -14359,11 +14359,51 @@ bool Sql_cmd_grant_proxy::execute(THD *thd) } +/** + Allow REVOKE DENY ... FROM PUBLIC to bypass the privilege check when the + revoker holds UPDATE on mysql.global_priv (where DENYs are stored). + + DENY ... TO PUBLIC locks out everyone including root. But the revoker who + can modify the underlying table via UPDATE has sufficient power to undo it + directly, so the denied privilege itself is not required. + + @retval true revoker runs in REVOKE DENY FROM PUBLIC, + can UPDATE mysql.global_priv and shall skip other checks + @retval false normal check applies +*/ + +bool Sql_cmd_grant::should_bypass_revoke_deny(THD *thd) +{ + /* Is current command a REVOKE DENY ? */ + if (!is_revoke() || !m_deny) + return false; + + /* Is it for PUBLIC ? */ + List_iterator_fast it(thd->lex->users_list); + LEX_USER *user; + while ((user= it++)) + { + if (user->host.length || + !my_charset_utf8mb3_general1400_as_ci.streq(user->user, public_name)) + return false; + } + + /* Does current user have UPDATE grant for mysql.global_priv ? */ + TABLE_LIST tl; + tl.init_one_table(&MYSQL_SCHEMA_NAME, &MYSQL_TABLE_NAME[USER_TABLE], + NULL, TL_WRITE); + return !check_access(thd, UPDATE_ACL, tl.db.str, &tl.grant.privilege, + &tl.grant.m_internal, 0, 1) && + !check_grant(thd, UPDATE_ACL, &tl, FALSE, 1, TRUE); +} + + bool Sql_cmd_grant_object::grant_stage0_exact_object(THD *thd, TABLE_LIST *table) { privilege_t priv= m_object_privilege | m_column_privilege_total | GRANT_ACL; - if (check_access(thd, priv, table->db.str, + if (!should_bypass_revoke_deny(thd) && + check_access(thd, priv, table->db.str, &table->grant.privilege, &table->grant.m_internal, 0, 0)) return true; @@ -14376,8 +14416,9 @@ bool Sql_cmd_grant_table::execute_exact_table(THD *thd, TABLE_LIST *table) { LEX *lex= thd->lex; if (grant_stage0_exact_object(thd, table) || - check_grant(thd, m_object_privilege | m_column_privilege_total | GRANT_ACL, - lex->query_tables, FALSE, UINT_MAX, FALSE)) + (!should_bypass_revoke_deny(thd) && + check_grant(thd, m_object_privilege | m_column_privilege_total | GRANT_ACL, + lex->query_tables, FALSE, UINT_MAX, FALSE))) return true; /* Conditionally writes to binlog */ WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL); @@ -14409,7 +14450,8 @@ bool Sql_cmd_grant_sp::execute(THD *thd) } if (grant_stage0_exact_object(thd, table) || - check_grant_routine(thd, grants|GRANT_ACL, lex->query_tables, &m_sph, 0)) + (!should_bypass_revoke_deny(thd) && + check_grant_routine(thd, grants|GRANT_ACL, lex->query_tables, &m_sph, 0))) return true; /* Conditionally writes to binlog */ @@ -14432,7 +14474,8 @@ bool Sql_cmd_grant_table::execute_table_mask(THD *thd) LEX *lex= thd->lex; DBUG_ASSERT(lex->first_select_lex()->table_list.first == NULL); - if (check_access(thd, m_object_privilege | m_column_privilege_total | GRANT_ACL, + if (!should_bypass_revoke_deny(thd) && + check_access(thd, m_object_privilege | m_column_privilege_total | GRANT_ACL, m_db.str, NULL, NULL, 1, 0)) return true; diff --git a/sql/sql_acl.h b/sql/sql_acl.h index b5d1aba2461fc..753c52bc49106 100644 --- a/sql/sql_acl.h +++ b/sql/sql_acl.h @@ -336,6 +336,7 @@ class Sql_cmd_grant: public Sql_cmd void warn_hostname_requires_resolving(THD *thd, List &list); bool user_list_reset_mqh(THD *thd, List &list); void grant_stage0(THD *thd); + bool should_bypass_revoke_deny(THD *thd); #endif public: Sql_cmd_grant(enum_sql_command command)