Skip to content

Commit 7a61bb7

Browse files
Merge pull request #1106 from rly/fix_change_password
Update set_password to work on MySQL 8
2 parents 9c12891 + acd6fdf commit 7a61bb7

File tree

3 files changed

+164
-2
lines changed

3 files changed

+164
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- Added - GitHub Actions workflow to manually release docs
66
- Changed - Update `datajoint/nginx` to `v0.2.6`
77
- Changed - Migrate docs from `https://docs.datajoint.org/python` to `https://datajoint.com/docs/core/datajoint-python`
8+
- Fixed - Updated set_password to work on MySQL 8 - PR [#1106](https://github.com/datajoint/datajoint-python/pull/1106)
9+
- Added - Missing tests for set_password - PR [#1106](https://github.com/datajoint/datajoint-python/pull/1106)
810

911
### 0.14.1 -- Jun 02, 2023
1012
- Fixed - Fix altering a part table that uses the "master" keyword - PR [#991](https://github.com/datajoint/datajoint-python/pull/991)

datajoint/admin.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pymysql
22
from getpass import getpass
3+
from packaging import version
34
from .connection import conn
45
from .settings import config
56
from .utils import user_choice
@@ -14,9 +15,16 @@ def set_password(new_password=None, connection=None, update_config=None):
1415
new_password = getpass("New password: ")
1516
confirm_password = getpass("Confirm password: ")
1617
if new_password != confirm_password:
17-
logger.warn("Failed to confirm the password! Aborting password change.")
18+
logger.warning("Failed to confirm the password! Aborting password change.")
1819
return
19-
connection.query("SET PASSWORD = PASSWORD('%s')" % new_password)
20+
21+
if version.parse(
22+
connection.query("select @@version;").fetchone()[0]
23+
) >= version.parse("5.7"):
24+
# SET PASSWORD is deprecated as of MySQL 5.7 and removed in 8+
25+
connection.query("ALTER USER user() IDENTIFIED BY '%s';" % new_password)
26+
else:
27+
connection.query("SET PASSWORD = PASSWORD('%s')" % new_password)
2028
logger.info("Password updated.")
2129

2230
if update_config or (

tests/test_admin.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""
2+
Collection of test cases to test admin module.
3+
"""
4+
5+
import datajoint as dj
6+
import os
7+
import pymysql
8+
import pytest
9+
10+
from . import CONN_INFO_ROOT
11+
12+
13+
@pytest.fixture()
14+
def user_alice() -> dict:
15+
# set up - reset config, log in as root, and create a new user alice
16+
# reset dj.config manually because its state may be changed by these tests
17+
if os.path.exists(dj.settings.LOCALCONFIG):
18+
os.remove(dj.settings.LOCALCONFIG)
19+
dj.config["database.password"] = os.getenv("DJ_PASS")
20+
root_conn = dj.conn(**CONN_INFO_ROOT, reset=True)
21+
new_credentials = dict(
22+
host=CONN_INFO_ROOT["host"],
23+
user="alice",
24+
password="oldpass",
25+
)
26+
root_conn.query(f"DROP USER IF EXISTS '{new_credentials['user']}'@'%%';")
27+
root_conn.query(
28+
f"CREATE USER '{new_credentials['user']}'@'%%' "
29+
f"IDENTIFIED BY '{new_credentials['password']}';"
30+
)
31+
32+
# test the connection
33+
dj.Connection(**new_credentials)
34+
35+
# return alice's credentials
36+
yield new_credentials
37+
38+
# tear down - delete the user and the local config file
39+
root_conn.query(f"DROP USER '{new_credentials['user']}'@'%%';")
40+
if os.path.exists(dj.settings.LOCALCONFIG):
41+
os.remove(dj.settings.LOCALCONFIG)
42+
43+
44+
def test_set_password_prompt_match(monkeypatch, user_alice: dict):
45+
"""
46+
Should be able to change the password using user prompt
47+
"""
48+
# reset the connection to use alice's credentials
49+
dj.conn(**user_alice, reset=True)
50+
51+
# prompts: new password / confirm password
52+
password_resp = iter(["newpass", "newpass"])
53+
# NOTE: because getpass.getpass is imported in datajoint.admin and used as
54+
# getpass in that module, we need to patch datajoint.admin.getpass
55+
# instead of getpass.getpass
56+
monkeypatch.setattr("datajoint.admin.getpass", lambda _: next(password_resp))
57+
58+
# respond no to prompt to update local config
59+
monkeypatch.setattr("builtins.input", lambda _: "no")
60+
61+
# reset password of user of current connection (alice)
62+
dj.set_password()
63+
64+
# should not be able to connect with old credentials
65+
with pytest.raises(pymysql.err.OperationalError):
66+
dj.Connection(**user_alice)
67+
68+
# should be able to connect with new credentials
69+
dj.Connection(host=user_alice["host"], user=user_alice["user"], password="newpass")
70+
71+
# check that local config is not updated
72+
assert dj.config["database.password"] == os.getenv("DJ_PASS")
73+
assert not os.path.exists(dj.settings.LOCALCONFIG)
74+
75+
76+
def test_set_password_prompt_mismatch(monkeypatch, user_alice: dict):
77+
"""
78+
Should not be able to change the password when passwords do not match
79+
"""
80+
# reset the connection to use alice's credentials
81+
dj.conn(**user_alice, reset=True)
82+
83+
# prompts: new password / confirm password
84+
password_resp = iter(["newpass", "wrong"])
85+
# NOTE: because getpass.getpass is imported in datajoint.admin and used as
86+
# getpass in that module, we need to patch datajoint.admin.getpass
87+
# instead of getpass.getpass
88+
monkeypatch.setattr("datajoint.admin.getpass", lambda _: next(password_resp))
89+
90+
# reset password of user of current connection (alice)
91+
# should be nop
92+
dj.set_password()
93+
94+
# should be able to connect with old credentials
95+
dj.Connection(**user_alice)
96+
97+
98+
def test_set_password_args(user_alice: dict):
99+
"""
100+
Should be able to change the password with an argument
101+
"""
102+
# reset the connection to use alice's credentials
103+
dj.conn(**user_alice, reset=True)
104+
105+
# reset password of user of current connection (alice)
106+
dj.set_password(new_password="newpass", update_config=False)
107+
108+
# should be able to connect with new credentials
109+
dj.Connection(host=user_alice["host"], user=user_alice["user"], password="newpass")
110+
111+
112+
def test_set_password_update_config(monkeypatch, user_alice: dict):
113+
"""
114+
Should be able to change the password and update local config
115+
"""
116+
# reset the connection to use alice's credentials
117+
dj.conn(**user_alice, reset=True)
118+
119+
# respond yes to prompt to update local config
120+
monkeypatch.setattr("builtins.input", lambda _: "yes")
121+
122+
# reset password of user of current connection (alice)
123+
dj.set_password(new_password="newpass")
124+
125+
# should be able to connect with new credentials
126+
dj.Connection(host=user_alice["host"], user=user_alice["user"], password="newpass")
127+
128+
# check that local config is updated
129+
# NOTE: the global config state is changed unless dj modules are reloaded
130+
# NOTE: this test is a bit unrealistic because the config user does not match
131+
# the user whose password is being updated, so the config credentials
132+
# will be invalid after update...
133+
assert dj.config["database.password"] == "newpass"
134+
assert os.path.exists(dj.settings.LOCALCONFIG)
135+
136+
137+
def test_set_password_conn(user_alice: dict):
138+
"""
139+
Should be able to change the password using a given connection
140+
"""
141+
# create a connection with alice's credentials
142+
conn_alice = dj.Connection(**user_alice)
143+
144+
# reset password of user of alice's connection (alice) and do not update config
145+
dj.set_password(new_password="newpass", connection=conn_alice, update_config=False)
146+
147+
# should be able to connect with new credentials
148+
dj.Connection(host=user_alice["host"], user=user_alice["user"], password="newpass")
149+
150+
# check that local config is not updated
151+
assert dj.config["database.password"] == os.getenv("DJ_PASS")
152+
assert not os.path.exists(dj.settings.LOCALCONFIG)

0 commit comments

Comments
 (0)