From 98d6753afbd1b5485961582acc5b0e5fffa39681 Mon Sep 17 00:00:00 2001 From: Danyal-Faheem Date: Fri, 29 Nov 2024 20:09:41 +0500 Subject: [PATCH] feat: add do command to update the authentication plugin of MySQL users to caching_sha2_password --- ...heem_mysql_authentication_plugin_change.md | 1 + docs/local.rst | 20 +++++ docs/troubleshooting.rst | 7 ++ tests/commands/test_jobs.py | 33 +++++++++ tutor/commands/jobs.py | 62 +++++++++++++++- tutor/commands/jobs_utils.py | 74 +++++++++++++++++++ 6 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 changelog.d/20240718_171945_danyal.faheem_mysql_authentication_plugin_change.md diff --git a/changelog.d/20240718_171945_danyal.faheem_mysql_authentication_plugin_change.md b/changelog.d/20240718_171945_danyal.faheem_mysql_authentication_plugin_change.md new file mode 100644 index 0000000000..907712997b --- /dev/null +++ b/changelog.d/20240718_171945_danyal.faheem_mysql_authentication_plugin_change.md @@ -0,0 +1 @@ +- [Improvement] Add a do command to update the authentication plugin of existing MySQL users from mysql_native_password to caching_sha2_password for compatibility with MySQL v8.4.0 and above. (by @Danyal-Faheem) diff --git a/docs/local.rst b/docs/local.rst index 997c61ffa2..aeef996070 100644 --- a/docs/local.rst +++ b/docs/local.rst @@ -168,6 +168,26 @@ By default, only the tables in the openedx database are changed. For upgrading t tutor local do convert-mysql-utf8mb4-charset --database=discovery +.. _update_mysql_authentication_plugin: + +Updating the authentication plugin of MySQL users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As of MySQL v8.4.0, the ``mysql_native_password`` authentication plugin has been deprecated. Users created with this authentication plugin should ideally be updated to use the latest ``caching_sha2_password`` authentication plugin. + +Tutor makes it easy do so with this handy command:: + + tutor local do update-mysql-authentication-plugin all + +The above command will update all the database users created by Tutor. If you only want to update the authentication plugin of specific users, you can use the ``--users`` option. This option takes comma seperated names of users to upgrade:: + + tutor local do update-mysql-authentication-plugin discovery ecommerce + +For this command, Tutor expects specific entries in the configuration for the mysql username and password of a database user. For example, if you are trying to update the user ``myuser``, the following case sensitive entries need to be present in the configuration:: + + MYUSER_MYSQL_USERNAME + MYUSER_MYSQL_PASSWORD + Running arbitrary ``manage.py`` commands ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 06e60fa583..718b94f777 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -216,3 +216,10 @@ NPM Dependency Conflict When overriding ``@edx/frontend-component-header`` or `` ---------------------------------------------------------------------------------------------------------------- The detailed steps are mentioned in `tutor-mfe `__ documentation. + +"Plugin 'mysql_native_password' is not loaded" +---------------------------------------------- + +This issue can occur when Tutor is upgraded from v15 (Olive) or earlier to v18 (Redwood) because the users created in Tutor v15 utilize the mysql_native_password authentication plugin by default. This plugin has been deprecated as of MySQL v8.4.0 which is the default MySQL server used in Tutor v18. + +The handy :ref:`update-mysql-authentication-plugin ` do command in tutor can be used to fix this issue. diff --git a/tests/commands/test_jobs.py b/tests/commands/test_jobs.py index d519629c83..c8554cb86d 100644 --- a/tests/commands/test_jobs.py +++ b/tests/commands/test_jobs.py @@ -165,3 +165,36 @@ def test_convert_mysql_utf8mb4_charset_exclude_tables(self) -> None: self.assertIn("NOT", dc_args[-1]) self.assertIn("course", dc_args[-1]) self.assertIn("auth", dc_args[-1]) + + def test_update_mysql_authentication_plugin_all_users(self) -> None: + with temporary_root() as root: + self.invoke_in_root(root, ["config", "save"]) + with patch("tutor.utils.docker_compose") as mock_docker_compose: + result = self.invoke_in_root( + root, + ["local", "do", "update-mysql-authentication-plugin", "all"], + ) + dc_args, _dc_kwargs = mock_docker_compose.call_args + + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) + self.assertIn("lms-job", dc_args) + self.assertIn("caching_sha2_password", dc_args[-1]) + self.assertIn("openedx", dc_args[-1]) + self.assertIn("root", dc_args[-1]) + + def test_update_mysql_authentication_plugin_one_user(self) -> None: + with temporary_root() as root: + self.invoke_in_root(root, ["config", "save"]) + with patch("tutor.utils.docker_compose") as mock_docker_compose: + result = self.invoke_in_root( + root, + ["local", "do", "update-mysql-authentication-plugin", "openedx"], + ) + dc_args, _dc_kwargs = mock_docker_compose.call_args + + self.assertIsNone(result.exception) + self.assertEqual(0, result.exit_code) + self.assertIn("lms-job", dc_args) + self.assertIn("caching_sha2_password", dc_args[-1]) + self.assertIn("openedx", dc_args[-1]) diff --git a/tutor/commands/jobs.py b/tutor/commands/jobs.py index 21fbbe5c43..991c48acd5 100644 --- a/tutor/commands/jobs.py +++ b/tutor/commands/jobs.py @@ -12,9 +12,12 @@ from typing_extensions import ParamSpec from tutor import config as tutor_config -from tutor import env, fmt, hooks +from tutor import env, fmt, hooks, plugins from tutor.commands.context import Context -from tutor.commands.jobs_utils import get_mysql_change_charset_query +from tutor.commands.jobs_utils import ( + get_mysql_change_authentication_plugin_query, + get_mysql_change_charset_query, +) from tutor.hooks import priorities @@ -420,6 +423,60 @@ def generate_query_to_append(tables: list[str], exclude: bool = False) -> str: fmt.echo_info("MySQL charset and collation successfully upgraded") +@click.command( + short_help="Update the authentication plugin of mysql users to caching_sha2_password.", + help=( + "Update the authentication plugin of mysql users to caching_sha2_password from mysql_native_password. You can specify either specific users to update or all to update all users." + ), +) +@click.argument( + "users", + nargs=-1, +) +@click.pass_obj +def update_mysql_authentication_plugin( + context: Context, users: tuple[str] +) -> t.Iterable[tuple[str, str]]: + """ + Update the authentication plugin of MySQL users from mysql_native_password to caching_sha2_password + Handy command utilized when upgrading to v8.4 of MySQL which deprecates mysql_native_password + """ + + config = tutor_config.load(context.root) + + if not config["RUN_MYSQL"]: + fmt.echo_info( + f"You are not running MySQL (RUN_MYSQL=False). It is your " + f"responsibility to update the authentication plugin of mysql users." + ) + return + + if not users: + fmt.echo_error( + f"Please specify a list of users to update the authentication plugin of.\n" + f"Or, specify 'all' to update all database users." + ) + return + + update_all = "all" in users + users_to_update = list(plugins.iter_loaded()) if update_all else users + + query = get_mysql_change_authentication_plugin_query( + config, users_to_update, update_all + ) + + # In case there is no user to update the authentication plugin of + if not query: + return + + mysql_command = ( + "mysql --user={{ MYSQL_ROOT_USERNAME }} --password={{ MYSQL_ROOT_PASSWORD }} --host={{ MYSQL_HOST }} --port={{ MYSQL_PORT }} --database={{ OPENEDX_MYSQL_DATABASE }} --show-warnings " + + shlex.join(["-e", query]) + ) + + yield ("lms", mysql_command) + + def add_job_commands(do_command_group: click.Group) -> None: """ This is meant to be called with the `local/dev/k8s do` group commands, to add the @@ -503,5 +560,6 @@ def do_callback(service_commands: t.Iterable[tuple[str, str]]) -> None: print_edx_platform_setting, settheme, sqlshell, + update_mysql_authentication_plugin, ] ) diff --git a/tutor/commands/jobs_utils.py b/tutor/commands/jobs_utils.py index e0d7dbbb0d..a4c171d93e 100644 --- a/tutor/commands/jobs_utils.py +++ b/tutor/commands/jobs_utils.py @@ -3,8 +3,14 @@ Methods: - `get_mysql_change_charset_query`: Generates MySQL queries to upgrade the charset and collation of columns, tables, and databases. +- `get_mysql_change_authentication_plugin_query`: Generates MySQL queries to update the authentication plugin for MySQL users. """ +from typing import Sequence + +from tutor import fmt +from tutor.types import Config, ConfigValue + def get_mysql_change_charset_query( database: str, @@ -131,3 +137,71 @@ def get_mysql_change_charset_query( CALL UpdateColumns(); CALL UpdateTables(); """ + + +def get_mysql_change_authentication_plugin_query( + config: Config, users: Sequence[str], all_users: bool +) -> str: + """ + Generates MySQL queries to update the authentication plugin for MySQL users. + + This method constructs queries to change the authentication plugin to + `caching_sha2_password`. User credentials must be provided in the tutor + configuration under the keys `_MYSQL_USERNAME` and `_MYSQL_PASSWORD`. + + Args: + config (Config): Tutor configuration object + users (List[str]): List of specific MySQL users to update. + all_users (bool): Flag indicating whether to include ROOT and OPENEDX users + in addition to those specified in the `users` list. + + Returns: + str: A string containing the SQL queries to execute. + + Raises: + TutorError: If any user in the `users` list does not have corresponding + username or password entries in the configuration. + """ + + host = "%" + query = "" + + def generate_mysql_authentication_plugin_update_query( + username: ConfigValue, password: ConfigValue, host: str + ) -> str: + fmt.echo_info( + f"Authentication plugin of user {username} will be updated to caching_sha2_password" + ) + return f"ALTER USER IF EXISTS '{username}'@'{host}' IDENTIFIED with caching_sha2_password BY '{password}';" + + def generate_user_queries(users: Sequence[str]) -> str: + query = "" + for user in users: + user_uppercase = user.upper() + if not ( + f"{user_uppercase}_MYSQL_USERNAME" in config + and f"{user_uppercase}_MYSQL_PASSWORD" in config + ): + fmt.echo_alert( + f"Username or Password for User {user} not found in config. Skipping update process for User {user}." + ) + continue + + query += generate_mysql_authentication_plugin_update_query( + config[f"{user_uppercase}_MYSQL_USERNAME"], + config[f"{user_uppercase}_MYSQL_PASSWORD"], + host, + ) + return query + + if not all_users: + return generate_user_queries(users) + + query += generate_mysql_authentication_plugin_update_query( + config["MYSQL_ROOT_USERNAME"], config["MYSQL_ROOT_PASSWORD"], host + ) + query += generate_mysql_authentication_plugin_update_query( + config["OPENEDX_MYSQL_USERNAME"], config["OPENEDX_MYSQL_PASSWORD"], host + ) + + return query + generate_user_queries(users)