diff --git a/mysql-test/r/all_persisted_variables.result b/mysql-test/r/all_persisted_variables.result index 6315f91f70cd..20799e67e2b5 100644 --- a/mysql-test/r/all_persisted_variables.result +++ b/mysql-test/r/all_persisted_variables.result @@ -47,7 +47,7 @@ include/assert.inc [Expect 500+ variables in the table. Due to open Bugs, we are # Test SET PERSIST -include/assert.inc [Expect 489 persisted variables in the table.] +include/assert.inc [Expect 490 persisted variables in the table.] ************************************************************ * 3. Restart server, it must preserve the persisted variable @@ -55,9 +55,9 @@ include/assert.inc [Expect 489 persisted variables in the table.] ************************************************************ # restart -include/assert.inc [Expect 489 persisted variables in persisted_variables table.] -include/assert.inc [Expect 489 persisted variables shown as PERSISTED in variables_info table.] -include/assert.inc [Expect 489 persisted variables with matching peristed and global values.] +include/assert.inc [Expect 490 persisted variables in persisted_variables table.] +include/assert.inc [Expect 490 persisted variables shown as PERSISTED in variables_info table.] +include/assert.inc [Expect 490 persisted variables with matching peristed and global values.] ************************************************************ * 4. Test RESET PERSIST IF EXISTS. Verify persisted variable diff --git a/mysql-test/r/mysqld--help-notwin.result b/mysql-test/r/mysqld--help-notwin.result index 07f04eb0c237..076e5bbf7a6a 100644 --- a/mysql-test/r/mysqld--help-notwin.result +++ b/mysql-test/r/mysqld--help-notwin.result @@ -1269,6 +1269,12 @@ The following options may be given as the first argument: Number of times the replication applier will retry a transaction in case it failed with a deadlock or other transient error, before it gives up and stops. + --replica-translate-deprecated-priv + Rewrite replicated SET_USER_ID to + SET_ANY_DEFINER,ALLOW_NONEXISTENT_DEFINER in the replica + SQL applier. Disabled by default, does not affect + user-issued GRANT/REVOKE, and can be changed only while + the replica SQL thread is stopped. --replica-type-conversions=name Set of type conversions that may be used by the replication applier thread for row events. Allowed values @@ -2082,6 +2088,7 @@ replica-preserve-commit-order TRUE replica-skip-errors (No default value) replica-sql-verify-checksum TRUE replica-transaction-retries 10 +replica-translate-deprecated-priv FALSE replica-type-conversions replicate-same-server-id FALSE replication-optimize-for-static-plugin-config FALSE diff --git a/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result b/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result index 9a9801fcd8c0..8eb1b5fa14f7 100644 --- a/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result +++ b/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result @@ -46,7 +46,7 @@ INSERT INTO aliases(name) VALUES ('slave_parallel_workers'), ('slave_pending_jobs_size_max'), ('pseudo_slave_mode'), ('skip_slave_start'); -include/assert.inc [Expect 113 variables in the table.] +include/assert.inc [Expect 114 variables in the table.] # Test SET PERSIST_ONLY SET PERSIST_ONLY binlog_cache_size = @@GLOBAL.binlog_cache_size; @@ -164,6 +164,7 @@ SET PERSIST_ONLY replica_preserve_commit_order = @@GLOBAL.replica_preserve_commi SET PERSIST_ONLY replica_skip_errors = @@GLOBAL.replica_skip_errors; SET PERSIST_ONLY replica_sql_verify_checksum = @@GLOBAL.replica_sql_verify_checksum; SET PERSIST_ONLY replica_transaction_retries = @@GLOBAL.replica_transaction_retries; +SET PERSIST_ONLY replica_translate_deprecated_priv = @@GLOBAL.replica_translate_deprecated_priv; SET PERSIST_ONLY replica_type_conversions = @@GLOBAL.replica_type_conversions; SET PERSIST_ONLY replication_optimize_for_static_plugin_config = @@GLOBAL.replication_optimize_for_static_plugin_config; SET PERSIST_ONLY replication_sender_observe_commit_only = @@GLOBAL.replication_sender_observe_commit_only; @@ -267,16 +268,16 @@ Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a futu Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a future release. SET PERSIST_ONLY sync_source_info = @@GLOBAL.sync_source_info; -include/assert.inc [Expect 100 persisted variables in persisted_variables table.] +include/assert.inc [Expect 101 persisted variables in persisted_variables table.] ############################################################ # 2. Restart server, it must preserve the persisted variable # settings. Verify persisted configuration. # restart -include/assert.inc [Expect 100 persisted variables in persisted_variables table.] -include/assert.inc [Expect 100 persisted variables shown as PERSISTED in variables_info table.] -include/assert.inc [Expect 100 persisted variables with matching persisted and global values.] +include/assert.inc [Expect 101 persisted variables in persisted_variables table.] +include/assert.inc [Expect 101 persisted variables shown as PERSISTED in variables_info table.] +include/assert.inc [Expect 101 persisted variables with matching persisted and global values.] ############################################################ # 3. Test RESET PERSIST. Verify persisted variable settings @@ -361,6 +362,7 @@ RESET PERSIST replica_preserve_commit_order; RESET PERSIST replica_skip_errors; RESET PERSIST replica_sql_verify_checksum; RESET PERSIST replica_transaction_retries; +RESET PERSIST replica_translate_deprecated_priv; RESET PERSIST replica_type_conversions; RESET PERSIST replication_optimize_for_static_plugin_config; RESET PERSIST replication_sender_observe_commit_only; diff --git a/mysql-test/suite/binlog_nogtid/r/binlog_persist_variables.result b/mysql-test/suite/binlog_nogtid/r/binlog_persist_variables.result index 94ce333d51f4..d9be508ee206 100644 --- a/mysql-test/suite/binlog_nogtid/r/binlog_persist_variables.result +++ b/mysql-test/suite/binlog_nogtid/r/binlog_persist_variables.result @@ -25,7 +25,7 @@ VARIABLE_NAME LIKE '%source%') AND (VARIABLE_NAME NOT LIKE 'innodb%') ORDER BY VARIABLE_NAME; -include/assert.inc [Expect 113 variables in the table.] +include/assert.inc [Expect 114 variables in the table.] # Test SET PERSIST SET PERSIST binlog_cache_size = @@GLOBAL.binlog_cache_size; @@ -145,6 +145,7 @@ SET PERSIST replica_skip_errors = @@GLOBAL.replica_skip_errors; ERROR HY000: Variable 'replica_skip_errors' is a read only variable SET PERSIST replica_sql_verify_checksum = @@GLOBAL.replica_sql_verify_checksum; SET PERSIST replica_transaction_retries = @@GLOBAL.replica_transaction_retries; +SET PERSIST replica_translate_deprecated_priv = @@GLOBAL.replica_translate_deprecated_priv; SET PERSIST replica_type_conversions = @@GLOBAL.replica_type_conversions; SET PERSIST replication_optimize_for_static_plugin_config = @@GLOBAL.replication_optimize_for_static_plugin_config; SET PERSIST replication_sender_observe_commit_only = @@GLOBAL.replication_sender_observe_commit_only; @@ -245,16 +246,16 @@ Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a futu Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a future release. SET PERSIST sync_source_info = @@GLOBAL.sync_source_info; -include/assert.inc [Expect 88 persisted variables in persisted_variables table.] +include/assert.inc [Expect 89 persisted variables in persisted_variables table.] ############################################################ # 2. Restart server, it must preserve the persisted variable # settings. Verify persisted configuration. # restart -include/assert.inc [Expect 88 persisted variables in persisted_variables table.'] -include/assert.inc [Expect 88 persisted variables shown as PERSISTED in variables_info table.'] -include/assert.inc [Expect 88 persisted variables with matching persisted and global values.] +include/assert.inc [Expect 89 persisted variables in persisted_variables table.'] +include/assert.inc [Expect 89 persisted variables shown as PERSISTED in variables_info table.'] +include/assert.inc [Expect 89 persisted variables with matching persisted and global values.] ############################################################ # 3. Test RESET PERSIST IF EXISTS. Verify persisted variable @@ -377,6 +378,7 @@ Warnings: Warning 3615 Variable replica_skip_errors does not exist in persisted config file RESET PERSIST IF EXISTS replica_sql_verify_checksum; RESET PERSIST IF EXISTS replica_transaction_retries; +RESET PERSIST IF EXISTS replica_translate_deprecated_priv; RESET PERSIST IF EXISTS replica_type_conversions; RESET PERSIST IF EXISTS replication_optimize_for_static_plugin_config; RESET PERSIST IF EXISTS replication_sender_observe_commit_only; diff --git a/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test b/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test index 83006785b8a1..f97ab0816939 100644 --- a/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test +++ b/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test @@ -83,7 +83,7 @@ INSERT INTO aliases(name) VALUES # If this count differs, it means a variable has been added or removed. # In that case, this testcase needs to be updated accordingly. --echo ---let $expected = 113 +--let $expected = 114 --let $assert_text = Expect $expected variables in the table. --let $assert_cond = [SELECT COUNT(*) as count FROM rplvars, count, 1] = $expected --source include/assert.inc @@ -115,7 +115,7 @@ while ( $varid <= $countvars ) } --echo ---let $expected = 100 +--let $expected = 101 --let $assert_text = Expect $expected persisted variables in persisted_variables table. --let $assert_cond = [SELECT COUNT(*) as count FROM performance_schema.persisted_variables, count, 1] = $expected --source include/assert.inc diff --git a/mysql-test/suite/binlog_nogtid/t/binlog_persist_variables.test b/mysql-test/suite/binlog_nogtid/t/binlog_persist_variables.test index ecd8b1a3f6e9..903040eac84f 100644 --- a/mysql-test/suite/binlog_nogtid/t/binlog_persist_variables.test +++ b/mysql-test/suite/binlog_nogtid/t/binlog_persist_variables.test @@ -61,7 +61,7 @@ INSERT INTO rplvars (varname, varvalue) # If this count differs, it means a variable has been added or removed. # In that case, this testcase needs to be updated accordingly. --echo ---let $expected = 113 +--let $expected = 114 --let $assert_text = Expect $expected variables in the table. --let $assert_cond = [SELECT COUNT(*) as count FROM rplvars, count, 1] = $expected --source include/assert.inc @@ -84,7 +84,7 @@ while ( $varid <= $countvars ) } --echo ---let $expected = 88 +--let $expected = 89 --let $assert_text = Expect $expected persisted variables in persisted_variables table. --let $assert_cond = [SELECT COUNT(*) as count FROM performance_schema.persisted_variables, count, 1] = $expected --source include/assert.inc diff --git a/mysql-test/suite/rpl/r/rpl_grant_set_user_id_compat.result b/mysql-test/suite/rpl/r/rpl_grant_set_user_id_compat.result new file mode 100644 index 000000000000..d38ec3d31848 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_grant_set_user_id_compat.result @@ -0,0 +1,147 @@ +include/rpl/init_source_replica.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the connection metadata repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START REPLICA; see the 'START REPLICA Syntax' in the MySQL Manual for more information. +[connection master] +# +# Sanity: SET_USER_ID is (re-)registered on the source, but not on +# the replica (this is what makes the test cross-version-like). +# +include/assert_grep.inc ["SET_USER_ID should be defined"] +[connection slave] +include/assert_grep.inc ["SET_USER_ID should not be defined"] +CALL mtr.add_suppression( +"Replica applier rewrote removed dynamic privilege 'SET_USER_ID' into "); +# +# Sanity: the new gate variable exists and defaults to OFF (opt-in). +# Save the current value so we can restore it before deinit. +# +SET @save_translate_deprecated_priv = +@@global.replica_translate_deprecated_priv; +SELECT @@global.replica_translate_deprecated_priv; +@@global.replica_translate_deprecated_priv +0 +# +# R5. The grammar/parser is not touched by the fix: a user thread +# executing GRANT SET_USER_ID directly on the replica must still be +# rejected with ER_SYNTAX_ERROR. Verified here while the gate is +# still at its OFF default, on a regular user connection (not the +# SQL applier thread). +# +CREATE USER rpl_psuid@localhost; +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use +DROP USER rpl_psuid@localhost; +# +# Opt in to the rewrite for the R1..R6 tests below. The gate can +# only be changed while the replication SQL thread is stopped, so +# stop replication, flip it ON, and start again. +# +include/rpl/stop_replica.inc +SET @@global.replica_translate_deprecated_priv = ON; +include/rpl/start_replica.inc +# +# R1, R2. GRANT SET_USER_ID on the source is binlogged as-is; the +# replica rewrites it before applying and ends up with the two +# modern privileges granted. +# +[connection master] +CREATE USER rpl_psuid@localhost; +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost; +include/rpl/sync_to_replica.inc +[connection slave] +SHOW GRANTS FOR rpl_psuid@localhost; +Grants for rpl_psuid@localhost +GRANT USAGE ON *.* TO `rpl_psuid`@`localhost` +GRANT ALLOW_NONEXISTENT_DEFINER,SET_ANY_DEFINER ON *.* TO `rpl_psuid`@`localhost` +# +# R2 (GRANT OPTION preserved): the WITH GRANT OPTION flag carried by +# the source statement must apply to both translated privileges. +# +[connection master] +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost WITH GRANT OPTION; +include/rpl/sync_to_replica.inc +[connection slave] +SHOW GRANTS FOR rpl_psuid@localhost; +Grants for rpl_psuid@localhost +GRANT USAGE ON *.* TO `rpl_psuid`@`localhost` +GRANT ALLOW_NONEXISTENT_DEFINER,SET_ANY_DEFINER ON *.* TO `rpl_psuid`@`localhost` WITH GRANT OPTION +# +# R3. REVOKE SET_USER_ID coming from the source revokes both +# SET_ANY_DEFINER and ALLOW_NONEXISTENT_DEFINER on the replica. +# +[connection master] +REVOKE SET_USER_ID ON *.* FROM rpl_psuid@localhost; +include/rpl/sync_to_replica.inc +[connection slave] +SHOW GRANTS FOR rpl_psuid@localhost; +Grants for rpl_psuid@localhost +GRANT USAGE ON *.* TO `rpl_psuid`@`localhost` +# +# R4. A DB literally named "set_user_id" (backtick-quoted) is not a +# GRANT of the SET_USER_ID privilege; the table-level GRANT must +# round-trip unmodified. Use a static privilege (SELECT) to make +# this a regular table-scope GRANT, unaffected by the dynamic +# privilege rewrite. +# +[connection master] +CREATE DATABASE `set_user_id`; +CREATE TABLE `set_user_id`.t (i INT); +GRANT SELECT ON `set_user_id`.* TO rpl_psuid@localhost; +include/rpl/sync_to_replica.inc +[connection slave] +SHOW GRANTS FOR rpl_psuid@localhost; +Grants for rpl_psuid@localhost +GRANT USAGE ON *.* TO `rpl_psuid`@`localhost` +GRANT SELECT ON `set_user_id`.* TO `rpl_psuid`@`localhost` +# +# Cleanup of the ON-mode test artifacts +# +[connection master] +DROP DATABASE `set_user_id`; +DROP USER rpl_psuid@localhost; +include/rpl/sync_to_replica.inc +# +# R7. Turn the gate back OFF. The same kind of statement that just +# round-tripped successfully must now stop the SQL applier with +# ER_SYNTAX_ERROR, proving the rewrite is gated. The gate can only +# be changed while the replication SQL thread is stopped. +# +[connection slave] +include/rpl/stop_replica.inc +SET @@global.replica_translate_deprecated_priv = OFF; +include/rpl/start_replica.inc +CALL mtr.add_suppression( +"Worker .* failed executing transaction"); +CALL mtr.add_suppression( +"The replica coordinator and worker threads are stopped"); +CALL mtr.add_suppression( +"Replica SQL for channel.*Error 'You have an error in your SQL syntax"); +[connection master] +CREATE USER rpl_psuid_off@localhost; +GRANT SET_USER_ID ON *.* TO rpl_psuid_off@localhost; +[connection slave] +include/rpl/wait_for_applier_error.inc [errno=1149] +# +# Recover: the CREATE USER ran successfully on the replica (it +# preceded the failing GRANT in the relay log), so the user is +# still there even after the SQL thread stopped. Stop replication, +# drop the user on both sides, wipe the source's binlog so the +# replica is not asked to re-apply the same failing event, then +# restart replication. +# +include/rpl/stop_replica.inc +RESET REPLICA; +DROP USER rpl_psuid_off@localhost; +[connection master] +DROP USER rpl_psuid_off@localhost; +RESET BINARY LOGS AND GTIDS; +# +# Restore the gate to its saved (default) value while the SQL thread +# is still stopped, then resume replication. +# +[connection slave] +SET @@global.replica_translate_deprecated_priv = +@save_translate_deprecated_priv; +include/rpl/start_replica.inc +include/rpl/deinit.inc diff --git a/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat-master.opt b/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat-master.opt new file mode 100644 index 000000000000..a86bb70fc7cf --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat-master.opt @@ -0,0 +1 @@ +--loose-debug=+d,register_legacy_set_user_id_priv diff --git a/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat.test b/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat.test new file mode 100644 index 000000000000..d83cb79ad417 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_grant_set_user_id_compat.test @@ -0,0 +1,204 @@ +# ==== Purpose ==== +# +# Verify that the replica SQL applier accepts GRANT/REVOKE statements that +# reference the removed SET_USER_ID dynamic privilege, by rewriting them to +# SET_ANY_DEFINER + ALLOW_NONEXISTENT_DEFINER -- the same mapping used by +# the offline upgrade migration (mysql_system_tables_fix.sql). +# +# ==== Requirements ==== +# +# R1. The replica MUST NOT stop with ER_SYNTAX_ERROR when applying a +# GRANT/REVOKE statement that references SET_USER_ID coming from an +# older source server. +# R2. The replica MUST grant SET_ANY_DEFINER and ALLOW_NONEXISTENT_DEFINER +# in place of SET_USER_ID, with the original WITH GRANT OPTION preserved. +# R3. A REVOKE SET_USER_ID coming from the source MUST revoke both +# SET_ANY_DEFINER and ALLOW_NONEXISTENT_DEFINER on the replica. +# R4. A free-standing identifier SET_USER_ID inside a quoted DB/table name +# (e.g. `set_user_id`) MUST NOT be rewritten -- only the bare privilege +# keyword in the privilege list is. +# R5. The rewrite MUST run only on the replica SQL applier; a user thread +# executing GRANT SET_USER_ID directly MUST still fail with the +# original ER_SYNTAX_ERROR (the SQL grammar/mysql_grant are unchanged). +# R6. A warning describing the rewrite MUST be written to the replica's +# error log so the operator can see the compatibility translation. +# R7. The rewrite MUST be gated by the new global system variable +# @@global.replica_translate_deprecated_priv (OFF by default; opt-in). +# With the default value the SQL applier MUST fail with +# ER_SYNTAX_ERROR exactly as on a stock build, proving the gate +# defaults to disabled. Setting the variable to ON MUST enable the +# rewrite path tested by R1..R6. +# +# ==== Implementation ==== +# +# The source server is launched with --debug=+d,register_legacy_set_user_id_priv +# (see rpl_grant_set_user_id_compat-master.opt), which re-registers +# SET_USER_ID as a dynamic privilege only on the source. +# This is needed only for test (the other option would be pre-recording +# the binlog. +# The replica boots +# with the default privilege set (no SET_USER_ID), so any binlogged +# GRANT/REVOKE SET_USER_ID statement from the source would fail to apply +# without the replica-side rewrite under test. + +--source include/have_debug.inc +--source include/have_binlog_format_row.inc +--source include/rpl/init_source_replica.inc + +--echo # +--echo # Sanity: SET_USER_ID is (re-)registered on the source, but not on +--echo # the replica (this is what makes the test cross-version-like). +--echo # +--let $assert_privilege_name= SET_USER_ID +--let $assert_privilege_absent= OFF +--source include/assert_dynamic_priv.inc + +--source include/rpl/connection_replica.inc +--let $assert_privilege_name= SET_USER_ID +--let $assert_privilege_absent= ON +--source include/assert_dynamic_priv.inc + +CALL mtr.add_suppression( + "Replica applier rewrote removed dynamic privilege 'SET_USER_ID' into "); + +--echo # +--echo # Sanity: the new gate variable exists and defaults to OFF (opt-in). +--echo # Save the current value so we can restore it before deinit. +--echo # +SET @save_translate_deprecated_priv = + @@global.replica_translate_deprecated_priv; +SELECT @@global.replica_translate_deprecated_priv; + +--echo # +--echo # R5. The grammar/parser is not touched by the fix: a user thread +--echo # executing GRANT SET_USER_ID directly on the replica must still be +--echo # rejected with ER_SYNTAX_ERROR. Verified here while the gate is +--echo # still at its OFF default, on a regular user connection (not the +--echo # SQL applier thread). +--echo # +CREATE USER rpl_psuid@localhost; +--error ER_SYNTAX_ERROR +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost; +DROP USER rpl_psuid@localhost; + +--echo # +--echo # Opt in to the rewrite for the R1..R6 tests below. The gate can +--echo # only be changed while the replication SQL thread is stopped, so +--echo # stop replication, flip it ON, and start again. +--echo # +--source include/rpl/stop_replica.inc +SET @@global.replica_translate_deprecated_priv = ON; +--source include/rpl/start_replica.inc + +--echo # +--echo # R1, R2. GRANT SET_USER_ID on the source is binlogged as-is; the +--echo # replica rewrites it before applying and ends up with the two +--echo # modern privileges granted. +--echo # +--source include/rpl/connection_source.inc +CREATE USER rpl_psuid@localhost; +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost; +--source include/rpl/sync_to_replica.inc + +--source include/rpl/connection_replica.inc +SHOW GRANTS FOR rpl_psuid@localhost; + +--echo # +--echo # R2 (GRANT OPTION preserved): the WITH GRANT OPTION flag carried by +--echo # the source statement must apply to both translated privileges. +--echo # +--source include/rpl/connection_source.inc +GRANT SET_USER_ID ON *.* TO rpl_psuid@localhost WITH GRANT OPTION; +--source include/rpl/sync_to_replica.inc + +--source include/rpl/connection_replica.inc +SHOW GRANTS FOR rpl_psuid@localhost; + +--echo # +--echo # R3. REVOKE SET_USER_ID coming from the source revokes both +--echo # SET_ANY_DEFINER and ALLOW_NONEXISTENT_DEFINER on the replica. +--echo # +--source include/rpl/connection_source.inc +REVOKE SET_USER_ID ON *.* FROM rpl_psuid@localhost; +--source include/rpl/sync_to_replica.inc + +--source include/rpl/connection_replica.inc +SHOW GRANTS FOR rpl_psuid@localhost; + +--echo # +--echo # R4. A DB literally named "set_user_id" (backtick-quoted) is not a +--echo # GRANT of the SET_USER_ID privilege; the table-level GRANT must +--echo # round-trip unmodified. Use a static privilege (SELECT) to make +--echo # this a regular table-scope GRANT, unaffected by the dynamic +--echo # privilege rewrite. +--echo # +--source include/rpl/connection_source.inc +CREATE DATABASE `set_user_id`; +CREATE TABLE `set_user_id`.t (i INT); +GRANT SELECT ON `set_user_id`.* TO rpl_psuid@localhost; +--source include/rpl/sync_to_replica.inc + +--source include/rpl/connection_replica.inc +SHOW GRANTS FOR rpl_psuid@localhost; + +--echo # +--echo # Cleanup of the ON-mode test artifacts +--echo # +--source include/rpl/connection_source.inc +DROP DATABASE `set_user_id`; +DROP USER rpl_psuid@localhost; +--source include/rpl/sync_to_replica.inc + +--echo # +--echo # R7. Turn the gate back OFF. The same kind of statement that just +--echo # round-tripped successfully must now stop the SQL applier with +--echo # ER_SYNTAX_ERROR, proving the rewrite is gated. The gate can only +--echo # be changed while the replication SQL thread is stopped. +--echo # +--source include/rpl/connection_replica.inc +--source include/rpl/stop_replica.inc +SET @@global.replica_translate_deprecated_priv = OFF; +--source include/rpl/start_replica.inc + +CALL mtr.add_suppression( + "Worker .* failed executing transaction"); +CALL mtr.add_suppression( + "The replica coordinator and worker threads are stopped"); +CALL mtr.add_suppression( + "Replica SQL for channel.*Error 'You have an error in your SQL syntax"); + +--source include/rpl/connection_source.inc +CREATE USER rpl_psuid_off@localhost; +GRANT SET_USER_ID ON *.* TO rpl_psuid_off@localhost; + +--source include/rpl/connection_replica.inc +--let $slave_sql_errno= convert_error(ER_SYNTAX_ERROR) +--source include/rpl/wait_for_applier_error.inc + +--echo # +--echo # Recover: the CREATE USER ran successfully on the replica (it +--echo # preceded the failing GRANT in the relay log), so the user is +--echo # still there even after the SQL thread stopped. Stop replication, +--echo # drop the user on both sides, wipe the source's binlog so the +--echo # replica is not asked to re-apply the same failing event, then +--echo # restart replication. +--echo # +--let $rpl_only_running_threads= 1 +--source include/rpl/stop_replica.inc +RESET REPLICA; +DROP USER rpl_psuid_off@localhost; + +--source include/rpl/connection_source.inc +DROP USER rpl_psuid_off@localhost; +RESET BINARY LOGS AND GTIDS; + +--echo # +--echo # Restore the gate to its saved (default) value while the SQL thread +--echo # is still stopped, then resume replication. +--echo # +--source include/rpl/connection_replica.inc +SET @@global.replica_translate_deprecated_priv = + @save_translate_deprecated_priv; +--source include/rpl/start_replica.inc + +--source include/rpl/deinit.inc diff --git a/mysql-test/suite/sys_vars/r/all_vars.result b/mysql-test/suite/sys_vars/r/all_vars.result index e28e3b50e6a1..458c7b15de28 100644 --- a/mysql-test/suite/sys_vars/r/all_vars.result +++ b/mysql-test/suite/sys_vars/r/all_vars.result @@ -103,6 +103,8 @@ replica_skip_errors replica_skip_errors replica_sql_verify_checksum replica_sql_verify_checksum +replica_translate_deprecated_priv +replica_translate_deprecated_priv replica_type_conversions replica_type_conversions replication_optimize_for_static_plugin_config diff --git a/share/messages_to_error_log.txt b/share/messages_to_error_log.txt index 4062f0ac37fb..d882408ac3b2 100644 --- a/share/messages_to_error_log.txt +++ b/share/messages_to_error_log.txt @@ -13418,6 +13418,9 @@ ER_AUDIT_PARSE_SKIP_DISABLED_EVENT_SUBCLASS # start-error-number 48300 +ER_LOG_REPLICA_TRANSLATED_DEPRECATED_PRIVILEGE + eng "Replica applier rewrote removed dynamic privilege '%s' into '%s' before applying the query received from an older source server. Original query: %s" + # # End of Percona Server 8.4 error log messages # diff --git a/sql/auth/dynamic_privileges_impl.cc b/sql/auth/dynamic_privileges_impl.cc index 621d66f48e85..92f79a8db9a5 100644 --- a/sql/auth/dynamic_privileges_impl.cc +++ b/sql/auth/dynamic_privileges_impl.cc @@ -320,5 +320,15 @@ bool dynamic_privilege_init(void) { ret += service->register_privilege(STRING_WITH_LEN("TRANSACTION_GTID_TAG")); ret += service->register_privilege(STRING_WITH_LEN("OPTIMIZE_LOCAL_TABLE")); + /* + Test-only: re-register the removed SET_USER_ID dynamic privilege so the + server can act as an "old source" emitting GRANT/REVOKE SET_USER_ID into + the binlog. Used by mysql-test cases that exercise the replica-side + compatibility translation; never enabled in release builds. + */ + DBUG_EXECUTE_IF("register_legacy_set_user_id_priv", { + ret += service->register_privilege(STRING_WITH_LEN("SET_USER_ID")); + }); + return ret != 0; } diff --git a/sql/log_event.cc b/sql/log_event.cc index 089c0c429ab3..5de817570f16 100644 --- a/sql/log_event.cc +++ b/sql/log_event.cc @@ -40,7 +40,10 @@ #include #include #include +#include #include +#include +#include #include #include "base64.h" @@ -4404,11 +4407,145 @@ void Query_log_event::detach_temp_tables_worker(THD *thd_arg, rli->current_mts_submode->detach_temp_tables(thd_arg, rli, this); } +/* + The removed dynamic privilege we recognize, and the modern privileges + the rewrite produces in its place (same mapping as the offline upgrade + in mysql_system_tables_fix.sql). Declared once and reused for the + match, the replacement text, the warning log message, and to size the + output buffer in the rewrite helper below. +*/ +static constexpr std::string_view kLegacyPriv = "SET_USER_ID"; +static constexpr std::string_view kModernPrivs = + "SET_ANY_DEFINER,ALLOW_NONEXISTENT_DEFINER"; + +/* + Total function for the replica applier's legacy-privilege rewrite. + Operates on this event's own `query` / `q_len` and allocates on the + event's THD mem_root. + + If `query` is a GRANT/REVOKE statement that mentions SET_USER_ID as a + free-standing identifier, returns a {pointer, length} pair for a copy + with each such occurrence replaced by kModernPrivs, and logs + ER_LOG_REPLICA_TRANSLATED_DEPRECATED_PRIVILEGE so the operator can see + the compatibility translation. Match is ASCII-case-insensitive. + Backtick / '...' / "..." regions are passed through untouched so a DB + or table literally named SET_USER_ID is left alone. + + In every other case (statement is not GRANT/REVOKE, SET_USER_ID is + absent, all occurrences are quoted, or the rewrite buffer cannot be + allocated) returns {query, q_len} unchanged, so the caller can use the + returned (ptr, len) pair unconditionally with no nullptr or "did it + change" checks of its own. +*/ +std::pair +Query_log_event::rewrite_legacy_set_user_id_priv() const { + /* + Cheapest possible early-out. Any query that could mention SET_USER_ID + as a privilege must be at least as long as the shortest GRANT form + that the parser accepts: + + GRANT SET_USER_ID ON *.* TO a@b (31 bytes) + + The REVOKE form is at least 34 bytes, so the GRANT template defines + the absolute floor. Computed at compile time from the literal so + the constant stays in sync with the template that motivates it. + */ + static constexpr std::string_view kMinRewriteCandidate = + "GRANT SET_USER_ID ON *.* TO a@b"; + if (query == nullptr || q_len < kMinRewriteCandidate.size()) + return {query, q_len}; + + /* Fast prefix check: only GRANT/REVOKE statements are candidates. */ + static const std::regex prefix(R"(^\s*(?:GRANT|REVOKE)\b)", + std::regex_constants::icase); + if (!std::regex_search(query, query + q_len, prefix)) return {query, q_len}; + + /* + Cheap second filter: if SET_USER_ID does not appear as a whole word + anywhere in the query (quoted or not), no rewrite is possible and we + can skip the quote-aware scan below entirely. This is the common + case for replicated GRANT/REVOKE traffic, which usually does not + mention the removed privilege at all. + */ + static const std::regex any_set_user_id(R"(\bSET_USER_ID\b)", + std::regex_constants::icase); + if (!std::regex_search(query, query + q_len, any_set_user_id)) + return {query, q_len}; + + /* + Scan the query for two kinds of tokens: + 1. A whole quoted string -- captured in group 1 and copied verbatim, + so identifiers / literals like `set_user_id`, 'SET_USER_ID' or + "SET_USER_ID" are left untouched. + 2. The bare word SET_USER_ID -- not captured, replaced with the + modern privileges. + Listing the quoted forms first lets the engine swallow them whole, + so the SET_USER_ID alternative only fires on text that lives outside + any quotes. + */ + static const std::regex re( + R"((`(?:``|[^`])*`|'(?:''|\\.|[^'])*'|"(?:""|\\.|[^"])*")|)" + R"(\bSET_USER_ID\b)", + std::regex_constants::icase); + + /* + Reserve room for at least one replacement (the common case is exactly + one SET_USER_ID per GRANT/REVOKE). Additional matches still work -- + std::string will reallocate as needed -- but a single replacement + fits without any heap growth beyond this reserve. + */ + static constexpr size_t kReplacementGrowth = + kModernPrivs.size() - kLegacyPriv.size(); + std::string output; + output.reserve(q_len + kReplacementGrowth); + bool replaced = false; + const char *pos = query; + for (std::cregex_iterator it(query, query + q_len, re), end; it != end; + ++it) { + const auto &m = (*it)[0]; + output.append(pos, m.first); + if ((*it)[1].matched) { + output.append(m.first, m.second); // quoted region: keep as-is + } else { + output.append(kModernPrivs); + replaced = true; + } + pos = m.second; + } + if (!replaced) return {query, q_len}; + output.append(pos, query + q_len); + + char *new_query = static_cast(thd->alloc(output.size() + 1)); + if (new_query == nullptr) return {query, q_len}; + memcpy(new_query, output.data(), output.size()); + new_query[output.size()] = '\0'; + LogErr(WARNING_LEVEL, ER_LOG_REPLICA_TRANSLATED_DEPRECATED_PRIVILEGE, + kLegacyPriv.data(), kModernPrivs.data(), query); + return {new_query, output.size()}; +} + /* Query_log_event::do_apply_event() */ int Query_log_event::do_apply_event(Relay_log_info const *rli) { - return do_apply_event(rli, query, q_len); + /* + Optionally rewrite the legacy SET_USER_ID dynamic privilege (removed + in 8.2 by WL#15875) into its modern replacements before handing the + query to the parser, so a GRANT/REVOKE SET_USER_ID emitted by an + older source server does not stop the replica SQL applier with + ER_SYNTAX_ERROR. Gated by @@global.replica_translate_deprecated_priv + The rewrite runs only here, on the replica applier path; the SQL + grammar and mysql_grant() are unchanged, so user-issued and + BINLOG '...' statements that reference SET_USER_ID always fail with + the same error as before. + */ + const char *effective_query = query; + size_t effective_len = q_len; + if (unlikely(opt_replica_translate_deprecated_priv)) { + std::tie(effective_query, effective_len) = + rewrite_legacy_set_user_id_priv(); + } + return do_apply_event(rli, effective_query, effective_len); } /* diff --git a/sql/log_event.h b/sql/log_event.h index 0b2fae609f95..20be4246f641 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -43,6 +43,7 @@ #include #include #include +#include #include "my_aes.h" #include "m_string.h" // native_strncasecmp @@ -1424,6 +1425,8 @@ class Query_log_event : public virtual mysql::binlog::event::Query_event, int do_apply_event(Relay_log_info const *rli, const char *query_arg, size_t q_len_arg); + + std::pair rewrite_legacy_set_user_id_priv() const; #endif /* MYSQL_SERVER */ /* If true, the event always be applied by slave SQL thread or be printed by diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 2b8af30c4e92..a1e83792d257 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -1380,6 +1380,7 @@ ulonglong replica_type_conversions_options; ulong opt_mts_replica_parallel_workers; ulonglong opt_mts_pending_jobs_size_max; bool opt_replica_preserve_commit_order; +bool opt_replica_translate_deprecated_priv; #ifndef NDEBUG uint replica_rows_last_search_algorithm_used; #endif diff --git a/sql/mysqld.h b/sql/mysqld.h index e05dc6356c0e..e9008773b2ad 100644 --- a/sql/mysqld.h +++ b/sql/mysqld.h @@ -213,6 +213,7 @@ extern bool lower_case_file_system; extern bool opt_require_secure_transport; extern bool opt_replica_preserve_commit_order; +extern bool opt_replica_translate_deprecated_priv; #ifndef NDEBUG extern uint replica_rows_last_search_algorithm_used; diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index b4f3d51a8215..0ffa9be7a73b 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -4249,6 +4249,17 @@ static Sys_var_bool Sys_replica_preserve_commit_order( static Sys_var_deprecated_alias Sys_slave_preserve_commit_order( "slave_preserve_commit_order", Sys_replica_preserve_commit_order); +static Sys_var_bool Sys_replica_translate_deprecated_priv( + "replica_translate_deprecated_priv", + "Rewrite replicated SET_USER_ID to " + "SET_ANY_DEFINER,ALLOW_NONEXISTENT_DEFINER in the replica SQL " + "applier. Disabled by default, does not affect user-issued " + "GRANT/REVOKE, and can be changed only while the replica SQL thread " + "is stopped.", + GLOBAL_VAR(opt_replica_translate_deprecated_priv), CMD_LINE(OPT_ARG), + DEFAULT(false), NO_MUTEX_GUARD, NOT_IN_BINLOG, + ON_CHECK(check_slave_stopped), ON_UPDATE(nullptr)); + bool Sys_var_charptr::global_update(THD *, set_var *var) { char *new_val, *ptr = var->save_result.string_value.str; const size_t len = var->save_result.string_value.length;