Skip to content

Commit fd57198

Browse files
VikramJayanthi17Vikram Jayanthibhrutledge
authored
Credential Logging (#685)
* Basic credential logging * Normalize log messages * Add tests Co-authored-by: Vikram Jayanthi <[email protected]> Co-authored-by: Brian Rutledge <[email protected]>
1 parent 2f3cf38 commit fd57198

File tree

5 files changed

+100
-21
lines changed

5 files changed

+100
-21
lines changed

tests/test_auth.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import logging
2+
13
import pytest
24

35
from twine import auth
46
from twine import exceptions
57
from twine import utils
68

7-
cred = auth.CredentialInput
8-
99

1010
@pytest.fixture
1111
def config() -> utils.RepositoryConfig:
@@ -20,7 +20,7 @@ def get_password(system, user):
2020

2121
monkeypatch.setattr(auth, "keyring", MockKeyring)
2222

23-
pw = auth.Resolver(config, cred("user")).password
23+
pw = auth.Resolver(config, auth.CredentialInput("user")).password
2424
assert pw == "user@system sekure pa55word"
2525

2626

@@ -32,37 +32,41 @@ def get_password(system, user):
3232

3333
monkeypatch.setattr(auth, "keyring", MockKeyring)
3434

35-
pw = auth.Resolver(config, cred("user")).password
35+
pw = auth.Resolver(config, auth.CredentialInput("user")).password
3636
assert pw == "entered pw"
3737

3838

3939
def test_no_password_defers_to_prompt(monkeypatch, entered_password, config):
4040
config.update(password=None)
41-
pw = auth.Resolver(config, cred("user")).password
41+
pw = auth.Resolver(config, auth.CredentialInput("user")).password
4242
assert pw == "entered pw"
4343

4444

4545
def test_empty_password_bypasses_prompt(monkeypatch, entered_password, config):
4646
config.update(password="")
47-
pw = auth.Resolver(config, cred("user")).password
47+
pw = auth.Resolver(config, auth.CredentialInput("user")).password
4848
assert pw == ""
4949

5050

5151
def test_no_username_non_interactive_aborts(config):
5252
with pytest.raises(exceptions.NonInteractive):
53-
auth.Private(config, cred("user")).password
53+
auth.Private(config, auth.CredentialInput("user")).password
5454

5555

5656
def test_no_password_non_interactive_aborts(config):
5757
with pytest.raises(exceptions.NonInteractive):
58-
auth.Private(config, cred("user")).password
58+
auth.Private(config, auth.CredentialInput("user")).password
5959

6060

61-
def test_get_username_and_password_keyring_overrides_prompt(monkeypatch, config):
61+
def test_get_username_and_password_keyring_overrides_prompt(
62+
monkeypatch, config, caplog
63+
):
64+
caplog.set_level(logging.INFO, "twine")
65+
6266
class MockKeyring:
6367
@staticmethod
6468
def get_credential(system, user):
65-
return cred(
69+
return auth.CredentialInput(
6670
"real_user", "real_user@{system} sekure pa55word".format(**locals())
6771
)
6872

@@ -75,10 +79,16 @@ def get_password(system, user):
7579

7680
monkeypatch.setattr(auth, "keyring", MockKeyring)
7781

78-
res = auth.Resolver(config, cred())
82+
res = auth.Resolver(config, auth.CredentialInput())
83+
7984
assert res.username == "real_user"
8085
assert res.password == "real_user@system sekure pa55word"
8186

87+
assert caplog.messages == [
88+
"username set from keyring",
89+
"password set from keyring",
90+
]
91+
8292

8393
@pytest.fixture
8494
def keyring_missing_get_credentials(monkeypatch):
@@ -94,21 +104,21 @@ def entered_username(monkeypatch):
94104
def test_get_username_keyring_missing_get_credentials_prompts(
95105
entered_username, keyring_missing_get_credentials, config
96106
):
97-
assert auth.Resolver(config, cred()).username == "entered user"
107+
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
98108

99109

100110
def test_get_username_keyring_missing_non_interactive_aborts(
101111
entered_username, keyring_missing_get_credentials, config
102112
):
103113
with pytest.raises(exceptions.NonInteractive):
104-
auth.Private(config, cred()).username
114+
auth.Private(config, auth.CredentialInput()).username
105115

106116

107117
def test_get_password_keyring_missing_non_interactive_aborts(
108118
entered_username, keyring_missing_get_credentials, config
109119
):
110120
with pytest.raises(exceptions.NonInteractive):
111-
auth.Private(config, cred("user")).password
121+
auth.Private(config, auth.CredentialInput("user")).password
112122

113123

114124
@pytest.fixture
@@ -138,7 +148,7 @@ def get_credential(system, username):
138148
def test_get_username_runtime_error_suppressed(
139149
entered_username, keyring_no_backends_get_credential, recwarn, config
140150
):
141-
assert auth.Resolver(config, cred()).username == "entered user"
151+
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
142152
assert len(recwarn) == 1
143153
warning = recwarn.pop(UserWarning)
144154
assert "fail!" in str(warning)
@@ -147,7 +157,7 @@ def test_get_username_runtime_error_suppressed(
147157
def test_get_password_runtime_error_suppressed(
148158
entered_password, keyring_no_backends, recwarn, config
149159
):
150-
assert auth.Resolver(config, cred("user")).password == "entered pw"
160+
assert auth.Resolver(config, auth.CredentialInput("user")).password == "entered pw"
151161
assert len(recwarn) == 1
152162
warning = recwarn.pop(UserWarning)
153163
assert "fail!" in str(warning)
@@ -162,4 +172,33 @@ def get_credential(system, username):
162172
return None
163173

164174
monkeypatch.setattr(auth, "keyring", FailKeyring())
165-
assert auth.Resolver(config, cred()).username == "entered user"
175+
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
176+
177+
178+
def test_logs_cli_values(caplog):
179+
caplog.set_level(logging.INFO, "twine")
180+
181+
res = auth.Resolver(config, auth.CredentialInput("username", "password"))
182+
183+
assert res.username == "username"
184+
assert res.password == "password"
185+
186+
assert caplog.messages == [
187+
"username set by command options",
188+
"password set by command options",
189+
]
190+
191+
192+
def test_logs_config_values(config, caplog):
193+
caplog.set_level(logging.INFO, "twine")
194+
195+
config.update(username="username", password="password")
196+
res = auth.Resolver(config, auth.CredentialInput())
197+
198+
assert res.username == "username"
199+
assert res.password == "password"
200+
201+
assert caplog.messages == [
202+
"username set from config file",
203+
"password set from config file",
204+
]

tests/test_repository.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import logging
1415
from contextlib import contextmanager
1516

1617
import pretend
@@ -338,3 +339,23 @@ def test_package_is_uploaded_incorrect_repo_url():
338339
repo.url = "https://bad.repo.com/legacy"
339340

340341
assert repo.package_is_uploaded(None) is False
342+
343+
344+
@pytest.mark.parametrize(
345+
"username, password, messages",
346+
[
347+
(None, None, ["username: <empty>", "password: <empty>"]),
348+
("", "", ["username: <empty>", "password: <empty>"]),
349+
("username", "password", ["username: username", "password: <hidden>"]),
350+
],
351+
)
352+
def test_logs_username_and_password(username, password, messages, caplog):
353+
caplog.set_level(logging.INFO, "twine")
354+
355+
repository.Repository(
356+
repository_url=utils.DEFAULT_REPOSITORY,
357+
username=username,
358+
password=password,
359+
)
360+
361+
assert caplog.messages == messages

twine/auth.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import getpass
3+
import logging
34
import warnings
45
from typing import Callable, Optional, Type, cast
56

@@ -8,6 +9,8 @@
89
from twine import exceptions
910
from twine import utils
1011

12+
logger = logging.getLogger(__name__)
13+
1114

1215
class CredentialInput:
1316
def __init__(
@@ -70,12 +73,20 @@ def get_password_from_keyring(self) -> Optional[str]:
7073
return None
7174

7275
def username_from_keyring_or_prompt(self) -> str:
73-
return self.get_username_from_keyring() or self.prompt("username", input)
76+
username = self.get_username_from_keyring()
77+
if username:
78+
logger.info("username set from keyring")
79+
return username
80+
81+
return self.prompt("username", input)
7482

7583
def password_from_keyring_or_prompt(self) -> str:
76-
return self.get_password_from_keyring() or self.prompt(
77-
"password", getpass.getpass
78-
)
84+
password = self.get_password_from_keyring()
85+
if password:
86+
logger.info("password set from keyring")
87+
return password
88+
89+
return self.prompt("password", getpass.getpass)
7990

8091
def prompt(self, what: str, how: Callable[..., str]) -> str:
8192
return how(f"Enter your {what}: ")

twine/repository.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import logging
1415
import sys
1516
from typing import Any, Dict, List, Optional, Set, Tuple, cast
1617

@@ -33,6 +34,8 @@
3334
TEST_WAREHOUSE = "https://test.pypi.org/"
3435
WAREHOUSE_WEB = "https://pypi.org/"
3536

37+
logger = logging.getLogger(__name__)
38+
3639

3740
class ProgressBar(tqdm.tqdm):
3841
def update_to(self, n: int) -> None:
@@ -61,6 +64,9 @@ def __init__(
6164
self.session.auth = (
6265
(username or "", password or "") if username or password else None
6366
)
67+
logger.info(f"username: {username if username else '<empty>'}")
68+
logger.info(f"password: <{'hidden' if password else 'empty'}>")
69+
6470
self.session.headers["User-Agent"] = self._make_user_agent_string()
6571
for scheme in ("http://", "https://"):
6672
self.session.mount(scheme, self._make_adapter_with_retries())

twine/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,10 @@ def get_userpass_value(
232232
:rtype: unicode
233233
"""
234234
if cli_value is not None:
235+
logger.info(f"{key} set by command options")
235236
return cli_value
236237
elif config.get(key) is not None:
238+
logger.info(f"{key} set from config file")
237239
return config[key]
238240
elif prompt_strategy:
239241
return prompt_strategy()

0 commit comments

Comments
 (0)