Skip to content

Commit c22afa9

Browse files
committed
BUG#36664998: Packets out of order error is raised while changing user in aio
This patch fixes the issue where OperationalError: Packets out of order is raised while trying to change logged-in user information inside an async connector session. Change-Id: Ibe99c57aefa596c67dd3fc997d9044071afa30c3
1 parent 6524f5a commit c22afa9

11 files changed

+117
-121
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ v9.0.0
1414
- WL#16350: Update dnspython version
1515
- WL#16318: Deprecate Cursors Prepared Raw and Named Tuple
1616
- WL#16283: Remove OpenTelemetry Bundled Installation
17+
- BUG#36664998: Packets out of order error is raised while changing user in aio
1718
- BUG#36611371: Update dnspython required versions to allow latest 2.6.1
1819
- BUG#36570707: Collation set on connect using C-Extension is ignored
1920
- BUG#36476195: Incorrect escaping in pure Python mode if sql_mode includes NO_BACKSLASH_ESCAPES

mysql-connector-python/lib/mysql/connector/abstracts.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,7 +2030,7 @@ def cmd_change_user(
20302030
username: str = "",
20312031
password: str = "",
20322032
database: str = "",
2033-
charset: int = 45,
2033+
charset: Optional[int] = None,
20342034
password1: str = "",
20352035
password2: str = "",
20362036
password3: str = "",
@@ -2041,7 +2041,8 @@ def cmd_change_user(
20412041
20422042
It also causes the specified database to become the default (current)
20432043
database. It is also possible to change the character set using the
2044-
charset argument.
2044+
charset argument. The character set passed during initial connection
2045+
is reused if no value of charset is passed via this method.
20452046
20462047
Args:
20472048
username: New account's username.

mysql-connector-python/lib/mysql/connector/aio/abstracts.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,6 +1557,51 @@ async def cmd_ping(self) -> OkPacketType:
15571557
with OK packet information.
15581558
"""
15591559

1560+
@abstractmethod
1561+
async def cmd_change_user(
1562+
self,
1563+
username: str = "",
1564+
password: str = "",
1565+
database: str = "",
1566+
charset: Optional[int] = None,
1567+
password1: str = "",
1568+
password2: str = "",
1569+
password3: str = "",
1570+
oci_config_file: str = "",
1571+
oci_config_profile: str = "",
1572+
) -> Optional[OkPacketType]:
1573+
"""Changes the current logged in user.
1574+
1575+
It also causes the specified database to become the default (current)
1576+
database. It is also possible to change the character set using the
1577+
charset argument. The character set passed during initial connection
1578+
is reused if no value of charset is passed via this method.
1579+
1580+
Args:
1581+
username: New account's username.
1582+
password: New account's password.
1583+
database: Database to become the default (current) database.
1584+
charset: Client charset (see [1]), only the lower 8-bits.
1585+
password1: New account's password factor 1 - it's used instead
1586+
of `password` if set (higher precedence).
1587+
password2: New account's password factor 2.
1588+
password3: New account's password factor 3.
1589+
oci_config_file: OCI configuration file location (path-like string).
1590+
oci_config_profile: OCI configuration profile location (path-like string).
1591+
1592+
Returns:
1593+
ok_packet: Dictionary containing the OK packet information.
1594+
1595+
Examples:
1596+
```
1597+
>>> cnx.cmd_change_user(username='', password='', database='', charset=33)
1598+
```
1599+
1600+
References:
1601+
[1]: https://dev.mysql.com/doc/dev/mysql-server/latest/\
1602+
page_protocol_basic_character_set.html#a_protocol_character_set
1603+
"""
1604+
15601605

15611606
class MySQLCursorAbstract(ABC):
15621607
"""Defines the MySQL cursor interface."""

mysql-connector-python/lib/mysql/connector/aio/connection.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ async def cmd_change_user(
13201320
self._password3 = password3
13211321

13221322
if self._password1 and password != self._password1:
1323-
password = self._password1
1323+
self._password = self._password1
13241324

13251325
await self.handle_unread_result()
13261326

@@ -1332,22 +1332,24 @@ async def cmd_change_user(
13321332

13331333
self._oci_config_profile = oci_config_profile
13341334

1335-
packet = self._protocol.make_auth(
1335+
ok_packet: bytes = await self._authenticator.authenticate(
1336+
sock=self._socket,
13361337
handshake=self._handshake,
13371338
username=self._user,
1338-
password=self._password,
1339+
password1=self._password,
1340+
password2=self._password2,
1341+
password3=self._password3,
13391342
database=self._database,
13401343
charset=self._charset.charset_id,
13411344
client_flags=self._client_flags,
13421345
ssl_enabled=self._ssl_active,
13431346
auth_plugin=self._auth_plugin,
13441347
conn_attrs=self._connection_attrs,
13451348
auth_plugin_class=self._auth_plugin_class,
1349+
oci_config_file=self._oci_config_file,
1350+
oci_config_profile=self._oci_config_profile,
1351+
is_change_user_request=True,
13461352
)
1347-
logger.debug("Protocol::HandshakeResponse packet: %s", packet)
1348-
await self._socket.write(packet[0])
1349-
1350-
ok_packet = self._handle_ok(await self._socket.read())
13511353

13521354
if not (self._client_flags & ClientFlag.CONNECT_WITH_DB) and database:
13531355
await self.cmd_init_db(database)

mysql-connector-python/lib/mysql/connector/connection.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ def _do_auth(
254254
password: Optional[str] = None,
255255
database: Optional[str] = None,
256256
client_flags: int = 0,
257-
charset: int = 45,
258257
ssl_options: Optional[Dict[str, Optional[Union[str, bool, List[str]]]]] = None,
259258
conn_attrs: Optional[Dict[str, str]] = None,
260259
) -> bool:
@@ -290,7 +289,7 @@ def _do_auth(
290289
self._socket,
291290
self.server_host,
292291
ssl_options,
293-
charset=charset,
292+
charset=self._charset_id,
294293
client_flags=client_flags,
295294
)
296295
self._ssl_active = True
@@ -303,7 +302,7 @@ def _do_auth(
303302
password2=self._password2,
304303
password3=self._password3,
305304
database=database,
306-
charset=charset,
305+
charset=self._charset_id,
307306
client_flags=client_flags,
308307
auth_plugin=self._auth_plugin,
309308
auth_plugin_class=self._auth_plugin_class,
@@ -366,7 +365,6 @@ def _open_connection(self) -> None:
366365
self._password,
367366
self._database,
368367
self._client_flags,
369-
self._charset_id,
370368
self._ssl,
371369
self._conn_attrs,
372370
)
@@ -1061,7 +1059,7 @@ def cmd_change_user(
10611059
self._password3 = password3
10621060

10631061
if self._password1 and password != self._password1:
1064-
password = self._password1
1062+
self._password = self._password1
10651063

10661064
self.handle_unread_result()
10671065

@@ -1077,7 +1075,7 @@ def cmd_change_user(
10771075
sock=self._socket,
10781076
handshake=self._handshake,
10791077
username=self._user,
1080-
password1=password,
1078+
password1=self._password,
10811079
password2=self._password2,
10821080
password3=self._password3,
10831081
database=database,

mysql-connector-python/lib/mysql/connector/connection_cext.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ def cmd_change_user(
907907
username: str = "",
908908
password: str = "",
909909
database: str = "",
910-
charset: int = 45,
910+
charset: Optional[int] = None,
911911
password1: str = "",
912912
password2: str = "",
913913
password3: str = "",
@@ -932,7 +932,14 @@ def cmd_change_user(
932932
msg=err.msg, errno=err.errno, sqlstate=err.sqlstate
933933
) from err
934934

935-
self._charset_id = charset
935+
# If charset isn't defined, we use the same charset ID defined previously,
936+
# otherwise, we run a verification and update the charset ID.
937+
if charset is not None:
938+
if not isinstance(charset, int):
939+
raise ValueError("charset must be an integer")
940+
if charset < 0:
941+
raise ValueError("charset should be either zero or a postive integer")
942+
self._charset_id = charset
936943
self._user = username # updating user accordingly
937944
self._post_connection()
938945

mysql-connector-python/lib/mysql/connector/django/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def adapt_datetime_with_timezone_support(value: datetime) -> StrOrBytes:
113113
)
114114
default_timezone = timezone.get_default_timezone()
115115
value = timezone.make_aware(value, default_timezone)
116-
value = value.astimezone(timezone.utc).replace( # pylint: disable=no-member
116+
# pylint: disable=no-member
117+
value = value.astimezone(timezone.utc).replace( # type: ignore[attr-defined]
117118
tzinfo=None
118119
)
119120
if HAVE_CEXT:

mysql-connector-python/tests/qa/test_qa_aio_connection_api.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,6 @@ async def test_collation_only(self):
172172
(charset, config["collation"]), (cnx.charset, cnx.collation)
173173
)
174174

175-
@unittest.skipIf(
176-
True,
177-
"Fixme: `cmd_change_user` isn't working - It's a bug!",
178-
)
179175
@tests.foreach_cnx_aio()
180176
async def test_cmd_change_user(self):
181177
"""Switch user and provide a different charset_id."""
@@ -202,6 +198,16 @@ async def test_cmd_change_user(self):
202198
self.assertEqual(exp_charset_before, self.cnx.charset)
203199
self.assertEqual(exp_collation_before, self.cnx.collation)
204200

201+
# do switch without setting charset and check that it matches 'exp_charset_id_before'
202+
await self.cnx.cmd_change_user(
203+
username=config["user"],
204+
password=config["password"],
205+
database=config["database"],
206+
)
207+
self.assertEqual(exp_charset_id_before, self.cnx.charset_id)
208+
self.assertEqual(exp_charset_before, self.cnx.charset)
209+
self.assertEqual(exp_collation_before, self.cnx.collation)
210+
205211
# do switch and check that it matches `exp_charset_id_after`
206212
await self.cnx.cmd_change_user(
207213
username=config["user"],

mysql-connector-python/tests/qa/test_qa_connection_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ def test_cmd_change_user(self):
190190
self.assertEqual(exp_charset_before, self.cnx.charset)
191191
self.assertEqual(exp_collation_before, self.cnx.collation)
192192

193+
# do switch without setting charset and check that it matches 'exp_charset_id_before'
194+
self.cnx.cmd_change_user(
195+
username=config["user"],
196+
password=config["password"],
197+
database=config["database"],
198+
)
199+
self.assertEqual(exp_charset_id_before, self.cnx.charset_id)
200+
self.assertEqual(exp_charset_before, self.cnx.charset)
201+
self.assertEqual(exp_collation_before, self.cnx.collation)
202+
193203
# do switch and check that it matches `exp_charset_id_after`
194204
self.cnx.cmd_change_user(
195205
username=config["user"],

mysql-connector-python/tests/test_aio_authentication.py

Lines changed: 7 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,81 +1183,6 @@ def setUp(self):
11831183
if self.skip_reason is not None:
11841184
self.skipTest(self.skip_reason)
11851185

1186-
# # @classmethod
1187-
# # def setUpClass(cls):
1188-
# async def asyncSetUp(self):
1189-
# config = tests.get_mysql_config()
1190-
# self.base_config = {
1191-
# "host": config["host"],
1192-
# "port": config["port"],
1193-
# "auth_plugin": "mysql_clear_password",
1194-
# }
1195-
# plugin_ext = "dll" if os.name == "nt" else "so"
1196-
# with mysql.connector.connection.MySQLConnection(**config) as cnx:
1197-
# try:
1198-
# cnx.cmd_query("UNINSTALL PLUGIN cleartext_plugin_server")
1199-
# except ProgrammingError:
1200-
# pass
1201-
# try:
1202-
# cnx.cmd_query(
1203-
# f"""
1204-
# INSTALL PLUGIN cleartext_plugin_server
1205-
# SONAME 'auth_test_plugin.{plugin_ext}'
1206-
# """
1207-
# )
1208-
# except DatabaseError:
1209-
# self.skip_reason = "Plugin cleartext_plugin_server not available"
1210-
# self.skipTest(self.skip_reason)
1211-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_1f}'")
1212-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_2f}'")
1213-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_3f}'")
1214-
# cnx.cmd_query(
1215-
# f"""
1216-
# CREATE USER '{self.user_1f}'
1217-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password1}'
1218-
# """
1219-
# )
1220-
# try:
1221-
# cnx.cmd_query(
1222-
# f"""
1223-
# CREATE USER '{self.user_2f}'
1224-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password1}'
1225-
# AND
1226-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password2}'
1227-
# """
1228-
# )
1229-
# cnx.cmd_query(
1230-
# f"""
1231-
# CREATE USER '{self.user_3f}'
1232-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password1}'
1233-
# AND
1234-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password2}'
1235-
# AND
1236-
# IDENTIFIED WITH cleartext_plugin_server BY '{self.password3}'
1237-
# """
1238-
# )
1239-
# except ProgrammingError:
1240-
# self.skip_reason = "Multi Factor Authentication not supported"
1241-
# self.skipTest(self.skip_reason)
1242-
# return
1243-
1244-
# # @classmethod
1245-
# # def tearDownClass(cls):
1246-
# async def asyncTearDown(self):
1247-
# config = tests.get_mysql_config()
1248-
# with mysql.connector.connection.MySQLConnection(**config) as cnx:
1249-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_1f}'")
1250-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_2f}'")
1251-
# cnx.cmd_query(f"DROP USER IF EXISTS '{self.user_3f}'")
1252-
# try:
1253-
# cnx.cmd_query("UNINSTALL PLUGIN cleartext_plugin_server")
1254-
# except ProgrammingError:
1255-
# pass
1256-
1257-
# async def asyncSetUp(self):
1258-
# if self.skip_reason is not None:
1259-
# self.skipTest(self.skip_reason)
1260-
12611186
async def _test_connection(self, cls, permutations, user):
12621187
"""Helper method for testing connection with MFA."""
12631188
LOGGER.debug("Running %d permutations...", len(permutations))
@@ -1324,11 +1249,9 @@ async def _test_change_user(self, cls, permutations, user):
13241249
res = await cnx.get_rows()
13251250
self.assertIsNotNone(res[0][0][0])
13261251
else:
1327-
self.assertRaises(
1328-
(DatabaseError, OperationalError, ProgrammingError),
1329-
cnx.cmd_change_user,
1330-
**kwargs,
1331-
)
1252+
with self.assertRaises(ProgrammingError):
1253+
cnx = cls(**config)
1254+
await cnx.cmd_change_user(**kwargs)
13321255

13331256
@foreach_cnx_aio()
13341257
async def test_user_1f(self):
@@ -1337,6 +1260,7 @@ async def test_user_1f(self):
13371260
for perm in itertools.product([True, False, None], repeat=4):
13381261
permutations.append((perm, perm[1] or (perm[0] and perm[1] is None)))
13391262
await self._test_connection(self.cnx.__class__, permutations, self.user_1f)
1263+
await self._test_change_user(self.cnx.__class__, permutations, self.user_1f)
13401264

13411265
@foreach_cnx_aio()
13421266
async def test_user_2f(self):
@@ -1350,9 +1274,7 @@ async def test_user_2f(self):
13501274
)
13511275
)
13521276
await self._test_connection(self.cnx.__class__, permutations, self.user_2f)
1353-
# The cmd_change_user() tests are temporarily disabled due to server
1354-
# BUG#33110621
1355-
# self._test_change_user(self.cnx.__class__, permutations, self.user_2f)
1277+
await self._test_change_user(self.cnx.__class__, permutations, self.user_2f)
13561278

13571279
@foreach_cnx_aio()
13581280
async def test_user_3f(self):
@@ -1368,9 +1290,7 @@ async def test_user_3f(self):
13681290
)
13691291
)
13701292
await self._test_connection(self.cnx.__class__, permutations, self.user_3f)
1371-
# The cmd_change_user() tests are temporarily disabled due to server
1372-
# BUG#33110621
1373-
# self._test_change_user(self.cnx.__class__, permutations, self.user_2f)
1293+
await self._test_change_user(self.cnx.__class__, permutations, self.user_3f)
13741294

13751295

13761296
@unittest.skipIf(
@@ -1386,8 +1306,7 @@ class MySQLWebAuthnAuthPluginTests(tests.MySQLConnectorTests):
13861306
async def test_invalid_webauthn_callback(self):
13871307
"""Test invalid 'webauthn_callback' option."""
13881308

1389-
def my_callback():
1390-
...
1309+
def my_callback(): ...
13911310

13921311
test_cases = (
13931312
"abc", # No callable named 'abc'

0 commit comments

Comments
 (0)