Skip to content

Commit 98d6753

Browse files
committed
feat: add do command to update the authentication plugin of MySQL users to caching_sha2_password
1 parent f8a47d7 commit 98d6753

File tree

6 files changed

+195
-2
lines changed

6 files changed

+195
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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)

docs/local.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ By default, only the tables in the openedx database are changed. For upgrading t
168168

169169
tutor local do convert-mysql-utf8mb4-charset --database=discovery
170170

171+
.. _update_mysql_authentication_plugin:
172+
173+
Updating the authentication plugin of MySQL users
174+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
175+
176+
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.
177+
178+
Tutor makes it easy do so with this handy command::
179+
180+
tutor local do update-mysql-authentication-plugin all
181+
182+
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::
183+
184+
tutor local do update-mysql-authentication-plugin discovery ecommerce
185+
186+
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::
187+
188+
MYUSER_MYSQL_USERNAME
189+
MYUSER_MYSQL_PASSWORD
190+
171191
Running arbitrary ``manage.py`` commands
172192
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
173193

docs/troubleshooting.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,10 @@ NPM Dependency Conflict When overriding ``@edx/frontend-component-header`` or ``
216216
----------------------------------------------------------------------------------------------------------------
217217

218218
The detailed steps are mentioned in `tutor-mfe <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#npm-dependency-conflict-when-overriding-edxfrontend-component-header-or-edxfrontend-component-footer>`__ documentation.
219+
220+
"Plugin 'mysql_native_password' is not loaded"
221+
----------------------------------------------
222+
223+
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.
224+
225+
The handy :ref:`update-mysql-authentication-plugin <update_mysql_authentication_plugin>` do command in tutor can be used to fix this issue.

tests/commands/test_jobs.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,36 @@ def test_convert_mysql_utf8mb4_charset_exclude_tables(self) -> None:
165165
self.assertIn("NOT", dc_args[-1])
166166
self.assertIn("course", dc_args[-1])
167167
self.assertIn("auth", dc_args[-1])
168+
169+
def test_update_mysql_authentication_plugin_all_users(self) -> None:
170+
with temporary_root() as root:
171+
self.invoke_in_root(root, ["config", "save"])
172+
with patch("tutor.utils.docker_compose") as mock_docker_compose:
173+
result = self.invoke_in_root(
174+
root,
175+
["local", "do", "update-mysql-authentication-plugin", "all"],
176+
)
177+
dc_args, _dc_kwargs = mock_docker_compose.call_args
178+
179+
self.assertIsNone(result.exception)
180+
self.assertEqual(0, result.exit_code)
181+
self.assertIn("lms-job", dc_args)
182+
self.assertIn("caching_sha2_password", dc_args[-1])
183+
self.assertIn("openedx", dc_args[-1])
184+
self.assertIn("root", dc_args[-1])
185+
186+
def test_update_mysql_authentication_plugin_one_user(self) -> None:
187+
with temporary_root() as root:
188+
self.invoke_in_root(root, ["config", "save"])
189+
with patch("tutor.utils.docker_compose") as mock_docker_compose:
190+
result = self.invoke_in_root(
191+
root,
192+
["local", "do", "update-mysql-authentication-plugin", "openedx"],
193+
)
194+
dc_args, _dc_kwargs = mock_docker_compose.call_args
195+
196+
self.assertIsNone(result.exception)
197+
self.assertEqual(0, result.exit_code)
198+
self.assertIn("lms-job", dc_args)
199+
self.assertIn("caching_sha2_password", dc_args[-1])
200+
self.assertIn("openedx", dc_args[-1])

tutor/commands/jobs.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
from typing_extensions import ParamSpec
1313

1414
from tutor import config as tutor_config
15-
from tutor import env, fmt, hooks
15+
from tutor import env, fmt, hooks, plugins
1616
from tutor.commands.context import Context
17-
from tutor.commands.jobs_utils import get_mysql_change_charset_query
17+
from tutor.commands.jobs_utils import (
18+
get_mysql_change_authentication_plugin_query,
19+
get_mysql_change_charset_query,
20+
)
1821
from tutor.hooks import priorities
1922

2023

@@ -420,6 +423,60 @@ def generate_query_to_append(tables: list[str], exclude: bool = False) -> str:
420423
fmt.echo_info("MySQL charset and collation successfully upgraded")
421424

422425

426+
@click.command(
427+
short_help="Update the authentication plugin of mysql users to caching_sha2_password.",
428+
help=(
429+
"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."
430+
),
431+
)
432+
@click.argument(
433+
"users",
434+
nargs=-1,
435+
)
436+
@click.pass_obj
437+
def update_mysql_authentication_plugin(
438+
context: Context, users: tuple[str]
439+
) -> t.Iterable[tuple[str, str]]:
440+
"""
441+
Update the authentication plugin of MySQL users from mysql_native_password to caching_sha2_password
442+
Handy command utilized when upgrading to v8.4 of MySQL which deprecates mysql_native_password
443+
"""
444+
445+
config = tutor_config.load(context.root)
446+
447+
if not config["RUN_MYSQL"]:
448+
fmt.echo_info(
449+
f"You are not running MySQL (RUN_MYSQL=False). It is your "
450+
f"responsibility to update the authentication plugin of mysql users."
451+
)
452+
return
453+
454+
if not users:
455+
fmt.echo_error(
456+
f"Please specify a list of users to update the authentication plugin of.\n"
457+
f"Or, specify 'all' to update all database users."
458+
)
459+
return
460+
461+
update_all = "all" in users
462+
users_to_update = list(plugins.iter_loaded()) if update_all else users
463+
464+
query = get_mysql_change_authentication_plugin_query(
465+
config, users_to_update, update_all
466+
)
467+
468+
# In case there is no user to update the authentication plugin of
469+
if not query:
470+
return
471+
472+
mysql_command = (
473+
"mysql --user={{ MYSQL_ROOT_USERNAME }} --password={{ MYSQL_ROOT_PASSWORD }} --host={{ MYSQL_HOST }} --port={{ MYSQL_PORT }} --database={{ OPENEDX_MYSQL_DATABASE }} --show-warnings "
474+
+ shlex.join(["-e", query])
475+
)
476+
477+
yield ("lms", mysql_command)
478+
479+
423480
def add_job_commands(do_command_group: click.Group) -> None:
424481
"""
425482
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:
503560
print_edx_platform_setting,
504561
settheme,
505562
sqlshell,
563+
update_mysql_authentication_plugin,
506564
]
507565
)

tutor/commands/jobs_utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
44
Methods:
55
- `get_mysql_change_charset_query`: Generates MySQL queries to upgrade the charset and collation of columns, tables, and databases.
6+
- `get_mysql_change_authentication_plugin_query`: Generates MySQL queries to update the authentication plugin for MySQL users.
67
"""
78

9+
from typing import Sequence
10+
11+
from tutor import fmt
12+
from tutor.types import Config, ConfigValue
13+
814

915
def get_mysql_change_charset_query(
1016
database: str,
@@ -131,3 +137,71 @@ def get_mysql_change_charset_query(
131137
CALL UpdateColumns();
132138
CALL UpdateTables();
133139
"""
140+
141+
142+
def get_mysql_change_authentication_plugin_query(
143+
config: Config, users: Sequence[str], all_users: bool
144+
) -> str:
145+
"""
146+
Generates MySQL queries to update the authentication plugin for MySQL users.
147+
148+
This method constructs queries to change the authentication plugin to
149+
`caching_sha2_password`. User credentials must be provided in the tutor
150+
configuration under the keys `<user>_MYSQL_USERNAME` and `<user>_MYSQL_PASSWORD`.
151+
152+
Args:
153+
config (Config): Tutor configuration object
154+
users (List[str]): List of specific MySQL users to update.
155+
all_users (bool): Flag indicating whether to include ROOT and OPENEDX users
156+
in addition to those specified in the `users` list.
157+
158+
Returns:
159+
str: A string containing the SQL queries to execute.
160+
161+
Raises:
162+
TutorError: If any user in the `users` list does not have corresponding
163+
username or password entries in the configuration.
164+
"""
165+
166+
host = "%"
167+
query = ""
168+
169+
def generate_mysql_authentication_plugin_update_query(
170+
username: ConfigValue, password: ConfigValue, host: str
171+
) -> str:
172+
fmt.echo_info(
173+
f"Authentication plugin of user {username} will be updated to caching_sha2_password"
174+
)
175+
return f"ALTER USER IF EXISTS '{username}'@'{host}' IDENTIFIED with caching_sha2_password BY '{password}';"
176+
177+
def generate_user_queries(users: Sequence[str]) -> str:
178+
query = ""
179+
for user in users:
180+
user_uppercase = user.upper()
181+
if not (
182+
f"{user_uppercase}_MYSQL_USERNAME" in config
183+
and f"{user_uppercase}_MYSQL_PASSWORD" in config
184+
):
185+
fmt.echo_alert(
186+
f"Username or Password for User {user} not found in config. Skipping update process for User {user}."
187+
)
188+
continue
189+
190+
query += generate_mysql_authentication_plugin_update_query(
191+
config[f"{user_uppercase}_MYSQL_USERNAME"],
192+
config[f"{user_uppercase}_MYSQL_PASSWORD"],
193+
host,
194+
)
195+
return query
196+
197+
if not all_users:
198+
return generate_user_queries(users)
199+
200+
query += generate_mysql_authentication_plugin_update_query(
201+
config["MYSQL_ROOT_USERNAME"], config["MYSQL_ROOT_PASSWORD"], host
202+
)
203+
query += generate_mysql_authentication_plugin_update_query(
204+
config["OPENEDX_MYSQL_USERNAME"], config["OPENEDX_MYSQL_PASSWORD"], host
205+
)
206+
207+
return query + generate_user_queries(users)

0 commit comments

Comments
 (0)