Skip to content

Commit

Permalink
Ignore http credentials with empty usernames
Browse files Browse the repository at this point in the history
  • Loading branch information
abn committed Nov 17, 2024
1 parent a923878 commit 0c22ca5
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 42 deletions.
11 changes: 11 additions & 0 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TOKEN> ""
```

**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
Expand Down
4 changes: 1 addition & 3 deletions src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 10 additions & 25 deletions src/poetry/utils/password_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}")

Expand All @@ -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)
5 changes: 2 additions & 3 deletions tests/utils/test_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand All @@ -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(
Expand Down
23 changes: 12 additions & 11 deletions tests/utils/test_password_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,7 +41,7 @@ def test_set_http_password(
("username", "password", "is_valid"),
[
("bar", "baz", True),
("", "baz", True),
("", "baz", False),
("bar", "", True),
("", "", False),
],
Expand All @@ -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(
Expand Down Expand Up @@ -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),
],
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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()


Expand All @@ -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()


Expand Down

0 comments on commit 0c22ca5

Please sign in to comment.