From 0c22ca5adabd203c0533ebfcc30b07e7583002c4 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Sat, 16 Nov 2024 22:35:08 +0100 Subject: [PATCH] Ignore http credentials with empty usernames See-also: jaraco/keyring#668 jaraco/keyring#687 --- docs/repositories.md | 11 +++++++++ src/poetry/utils/authenticator.py | 4 +--- src/poetry/utils/password_manager.py | 35 ++++++++-------------------- tests/utils/test_authenticator.py | 5 ++-- tests/utils/test_password_manager.py | 23 +++++++++--------- 5 files changed, 36 insertions(+), 42 deletions(-) diff --git a/docs/repositories.md b/docs/repositories.md index 31122036dfe..499c5ec59e2 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -511,6 +511,17 @@ You can prevent this by adding double dashes to prevent any following argument f poetry config -- http-basic.pypi myUsername -myPasswordStartingWithDash ``` +{{% note %}} +In some cases like that of [Gemfury](https://gemfury.com/help/errors/repo-url-password/) repositories, it might be +required to set an empty password. This is supported by Poetry. + +```bash +poetry config http-basic.foo "" +``` + +**Note:** Usernames cannot be empty. Attempting to use an empty username can result in an unpredictable failure. +{{% /note %}} + ## Certificates ### Custom certificate authority and mutual TLS authentication diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index f19ee99b18e..b51a44c0c59 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -81,9 +81,7 @@ def get_http_credentials( self, password_manager: PasswordManager ) -> HTTPAuthCredential: # try with the repository name via the password manager - credential = HTTPAuthCredential( - **(password_manager.get_http_auth(self.name) or {}) - ) + credential = password_manager.get_http_auth(self.name) if credential.password is not None: return credential diff --git a/src/poetry/utils/password_manager.py b/src/poetry/utils/password_manager.py index f40ac4d1355..fd218524248 100644 --- a/src/poetry/utils/password_manager.py +++ b/src/poetry/utils/password_manager.py @@ -192,29 +192,17 @@ def delete_pypi_token(self, repo_name: str) -> None: self.keyring.delete_password(repo_name, "__token__") - def get_http_auth(self, repo_name: str) -> dict[str, str | None] | None: + def get_http_auth(self, repo_name: str) -> HTTPAuthCredential: username = self._config.get(f"http-basic.{repo_name}.username") password = self._config.get(f"http-basic.{repo_name}.password") - # we only return None if both values are None or "" - # password can be None at this stage with the username "" - if (username is password is None) or (username == password == ""): - return None - - if not password: - if self.use_keyring: - password = self.keyring.get_password(repo_name, username) - elif not username: - # at this tage if username is "" or None, auth is invalid - return None + if not username: + return HTTPAuthCredential() - if not username and not password: - return None + if password is None and self.use_keyring: + password = self.keyring.get_password(repo_name, username) - return { - "username": username or "", - "password": password or "", - } + return HTTPAuthCredential(username=username, password=password) def set_http_password(self, repo_name: str, username: str, password: str) -> None: auth = {"username": username} @@ -229,15 +217,12 @@ def set_http_password(self, repo_name: str, username: str, password: str) -> Non def delete_http_password(self, repo_name: str) -> None: auth = self.get_http_auth(repo_name) - if not auth: - return - username = auth.get("username") - if username is None: + if auth.username is None: return with suppress(PoetryKeyringError): - self.keyring.delete_password(repo_name, username) + self.keyring.delete_password(repo_name, auth.username) self._config.auth_config_source.remove_property(f"http-basic.{repo_name}") @@ -246,5 +231,5 @@ def get_credential( ) -> HTTPAuthCredential: if self.use_keyring: return self.keyring.get_credential(*names, username=username) - else: - return HTTPAuthCredential(username=username, password=None) + + return HTTPAuthCredential(username=username, password=None) diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index bd25702e296..12d91ab62e7 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -153,7 +153,7 @@ def test_authenticator_uses_empty_strings_as_default_password( assert request.headers["Authorization"] == f"Basic {basic_auth}" -def test_authenticator_uses_empty_strings_as_default_username( +def test_authenticator_ignores_empty_strings_as_default_username( config: Config, mock_remote: None, repo: dict[str, dict[str, str]], @@ -170,8 +170,7 @@ def test_authenticator_uses_empty_strings_as_default_username( authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz") request = http.last_request() - basic_auth = base64.b64encode(b":bar").decode() - assert request.headers["Authorization"] == f"Basic {basic_auth}" + assert request.headers["Authorization"] is None def test_authenticator_falls_back_to_keyring_url( diff --git a/tests/utils/test_password_manager.py b/tests/utils/test_password_manager.py index 00058e46513..def300e8e64 100644 --- a/tests/utils/test_password_manager.py +++ b/tests/utils/test_password_manager.py @@ -8,6 +8,7 @@ import pytest +from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager from poetry.utils.password_manager import PoetryKeyring from poetry.utils.password_manager import PoetryKeyringError @@ -40,7 +41,7 @@ def test_set_http_password( ("username", "password", "is_valid"), [ ("bar", "baz", True), - ("", "baz", True), + ("", "baz", False), ("bar", "", True), ("", "", False), ], @@ -62,10 +63,10 @@ def test_get_http_auth( if is_valid: assert auth is not None - assert auth["username"] == username - assert auth["password"] == password + assert auth.username == username + assert auth.password == password else: - assert auth is None + assert auth.username is auth.password is None def test_delete_http_password( @@ -134,7 +135,7 @@ def test_set_http_password_with_unavailable_backend( ("username", "password", "is_valid"), [ ("bar", "baz", True), - ("", "baz", True), + ("", "baz", False), ("bar", "", True), ("", "", False), ], @@ -156,10 +157,10 @@ def test_get_http_auth_with_unavailable_backend( if is_valid: assert auth is not None - assert auth["username"] == username - assert auth["password"] == password + assert auth.username == username + assert auth.password == password else: - assert auth is None + assert auth.username is auth.password is None def test_delete_http_password_with_unavailable_backend( @@ -304,7 +305,7 @@ def test_get_http_auth_from_environment_variables( manager = PasswordManager(config) auth = manager.get_http_auth("foo") - assert auth == {"username": "bar", "password": "baz"} + assert auth == HTTPAuthCredential(username="bar", password="baz") def test_get_http_auth_does_not_call_keyring_when_credentials_in_environment_variables( @@ -317,7 +318,7 @@ def test_get_http_auth_does_not_call_keyring_when_credentials_in_environment_var manager.keyring = MagicMock() auth = manager.get_http_auth("foo") - assert auth == {"username": "bar", "password": "baz"} + assert auth == HTTPAuthCredential(username="bar", password="baz") manager.keyring.get_password.assert_not_called() @@ -335,7 +336,7 @@ def test_get_http_auth_does_not_call_keyring_when_password_in_environment_variab manager.keyring = MagicMock() auth = manager.get_http_auth("foo") - assert auth == {"username": "bar", "password": "baz"} + assert auth == HTTPAuthCredential(username="bar", password="baz") manager.keyring.get_password.assert_not_called()