Commit 58db4247 by Arun Babu Neelicattu Committed by Bjorn Neergaard

authenticator: allow multiple repos w/ same netloc

Co-authored-by: Agni Sairent <agniczech@gmail.com>
Co-authored-by: Dos Moonen <darsstar@gmail.com>
parent 070ea6b4
...@@ -112,6 +112,20 @@ class Config: ...@@ -112,6 +112,20 @@ class Config:
def raw(self) -> dict[str, Any]: def raw(self) -> dict[str, Any]:
return self._config return self._config
@staticmethod
def _get_environment_repositories() -> dict[str, dict[str, str]]:
repositories = {}
pattern = re.compile(r"POETRY_REPOSITORIES_(?P<name>[A-Z_]+)_URL")
for env_key in os.environ.keys():
match = pattern.match(env_key)
if match:
repositories[match.group("name").lower().replace("_", "-")] = {
"url": os.environ[env_key]
}
return repositories
def get(self, setting_name: str, default: Any = None) -> Any: def get(self, setting_name: str, default: Any = None) -> Any:
""" """
Retrieve a setting value. Retrieve a setting value.
...@@ -121,6 +135,12 @@ class Config: ...@@ -121,6 +135,12 @@ class Config:
# Looking in the environment if the setting # Looking in the environment if the setting
# is set via a POETRY_* environment variable # is set via a POETRY_* environment variable
if self._use_environment: if self._use_environment:
if setting_name == "repositories":
# repositories setting is special for now
repositories = self._get_environment_repositories()
if repositories:
return repositories
env = "POETRY_" + "_".join(k.upper().replace("-", "_") for k in keys) env = "POETRY_" + "_".join(k.upper().replace("-", "_") for k in keys)
env_value = os.getenv(env) env_value = os.getenv(env)
if env_value is not None: if env_value is not None:
......
...@@ -69,14 +69,14 @@ class Publisher: ...@@ -69,14 +69,14 @@ class Publisher:
logger.debug( logger.debug(
f"Found authentication information for {repository_name}." f"Found authentication information for {repository_name}."
) )
username = auth["username"] username = auth.username
password = auth["password"] password = auth.password
resolved_client_cert = client_cert or get_client_cert( resolved_client_cert = client_cert or get_client_cert(
self._poetry.config, repository_name self._poetry.config, repository_name
) )
# Requesting missing credentials but only if there is not a client cert defined. # Requesting missing credentials but only if there is not a client cert defined.
if not resolved_client_cert: if not resolved_client_cert and hasattr(self._io, "ask"):
if username is None: if username is None:
username = self._io.ask("Username:") username = self._io.ask("Username:")
......
from __future__ import annotations from __future__ import annotations
import dataclasses
import logging import logging
from contextlib import suppress from contextlib import suppress
...@@ -22,6 +23,12 @@ class KeyRingError(Exception): ...@@ -22,6 +23,12 @@ class KeyRingError(Exception):
pass pass
@dataclasses.dataclass
class HTTPAuthCredential:
username: str | None = dataclasses.field(default=None)
password: str | None = dataclasses.field(default=None)
class KeyRing: class KeyRing:
def __init__(self, namespace: str) -> None: def __init__(self, namespace: str) -> None:
self._namespace = namespace self._namespace = namespace
...@@ -32,6 +39,25 @@ class KeyRing: ...@@ -32,6 +39,25 @@ class KeyRing:
def is_available(self) -> bool: def is_available(self) -> bool:
return self._is_available return self._is_available
def get_credential(
self, *names: str, username: str | None = None
) -> HTTPAuthCredential:
default = HTTPAuthCredential(username=username, password=None)
if not self.is_available():
return default
import keyring
for name in names:
credential = keyring.get_credential(name, username)
if credential:
return HTTPAuthCredential(
username=credential.username, password=credential.password
)
return default
def get_password(self, name: str, username: str) -> str | None: def get_password(self, name: str, username: str) -> str | None:
if not self.is_available(): if not self.is_available():
return None return None
......
...@@ -28,6 +28,11 @@ if TYPE_CHECKING: ...@@ -28,6 +28,11 @@ if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@pytest.fixture(autouse=True)
def _use_simple_keyring(with_simple_keyring: None) -> None:
pass
class MockRepository(LegacyRepository): class MockRepository(LegacyRepository):
FIXTURES = Path(__file__).parent / "fixtures" / "legacy" FIXTURES = Path(__file__).parent / "fixtures" / "legacy"
......
...@@ -22,6 +22,11 @@ if TYPE_CHECKING: ...@@ -22,6 +22,11 @@ if TYPE_CHECKING:
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@pytest.fixture(autouse=True)
def _use_simple_keyring(with_simple_keyring: None) -> None:
pass
class MockRepository(PyPiRepository): class MockRepository(PyPiRepository):
JSON_FIXTURES = Path(__file__).parent / "fixtures" / "pypi.org" / "json" JSON_FIXTURES = Path(__file__).parent / "fixtures" / "pypi.org" / "json"
......
from __future__ import annotations from __future__ import annotations
import base64
import re import re
import uuid import uuid
...@@ -286,19 +287,16 @@ def test_authenticator_request_retries_on_status_code( ...@@ -286,19 +287,16 @@ def test_authenticator_request_retries_on_status_code(
assert sleep.call_count == attempts assert sleep.call_count == attempts
@pytest.fixture
def environment_repository_credentials(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz")
def test_authenticator_uses_env_provided_credentials( def test_authenticator_uses_env_provided_credentials(
config: Config, config: Config,
environ: None, environ: None,
mock_remote: type[httpretty.httpretty], mock_remote: type[httpretty.httpretty],
http: type[httpretty.httpretty], http: type[httpretty.httpretty],
environment_repository_credentials: None, monkeypatch: MonkeyPatch,
): ):
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_USERNAME", "bar")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_PASSWORD", "baz")
config.merge({"repositories": {"foo": {"url": "https://foo.bar/simple/"}}}) config.merge({"repositories": {"foo": {"url": "https://foo.bar/simple/"}}})
authenticator = Authenticator(config, NullIO()) authenticator = Authenticator(config, NullIO())
...@@ -352,3 +350,177 @@ def test_authenticator_uses_certs_from_config_if_not_provided( ...@@ -352,3 +350,177 @@ def test_authenticator_uses_certs_from_config_if_not_provided(
assert Path(kwargs["verify"]) == Path(cert or configured_cert) assert Path(kwargs["verify"]) == Path(cert or configured_cert)
assert Path(kwargs["cert"]) == Path(client_cert or configured_client_cert) assert Path(kwargs["cert"]) == Path(client_cert or configured_client_cert)
def test_authenticator_uses_credentials_from_config_matched_by_url_path(
config: Config, mock_remote: None, http: type[httpretty.httpretty]
):
config.merge(
{
"repositories": {
"foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"},
"foo-beta": {"url": "https://foo.bar/beta/files/simple/"},
},
"http-basic": {
"foo-alpha": {"username": "bar", "password": "alpha"},
"foo-beta": {"username": "baz", "password": "beta"},
},
}
)
authenticator = Authenticator(config, NullIO())
authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b"bar:alpha").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
# Make request on second repository with the same netloc but different credentials
authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b"baz:beta").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
def test_authenticator_uses_credentials_from_config_with_at_sign_in_path(
config: Config, mock_remote: None, http: type[httpretty.httpretty]
):
config.merge(
{
"repositories": {
"foo": {"url": "https://foo.bar/beta/files/simple/"},
},
"http-basic": {
"foo": {"username": "bar", "password": "baz"},
},
}
)
authenticator = Authenticator(config, NullIO())
authenticator.request("get", "https://foo.bar/beta/files/simple/f@@-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b"bar:baz").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
def test_authenticator_falls_back_to_keyring_url_matched_by_path(
config: Config,
mock_remote: None,
http: type[httpretty.httpretty],
with_simple_keyring: None,
dummy_keyring: DummyBackend,
):
config.merge(
{
"repositories": {
"foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"},
"foo-beta": {"url": "https://foo.bar/beta/files/simple/"},
}
}
)
dummy_keyring.set_password(
"https://foo.bar/alpha/files/simple/", None, SimpleCredential(None, "bar")
)
dummy_keyring.set_password(
"https://foo.bar/beta/files/simple/", None, SimpleCredential(None, "baz")
)
authenticator = Authenticator(config, NullIO())
authenticator.request("get", "https://foo.bar/alpha/files/simple/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}"
authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b":baz").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
def test_authenticator_uses_env_provided_credentials_matched_by_url_path(
config: Config,
environ: None,
mock_remote: type[httpretty.httpretty],
http: type[httpretty.httpretty],
monkeypatch: MonkeyPatch,
):
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_USERNAME", "bar")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_ALPHA_PASSWORD", "alpha")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_BETA_USERNAME", "baz")
monkeypatch.setenv("POETRY_HTTP_BASIC_FOO_BETA_PASSWORD", "beta")
config.merge(
{
"repositories": {
"foo-alpha": {"url": "https://foo.bar/alpha/files/simple/"},
"foo-beta": {"url": "https://foo.bar/beta/files/simple/"},
}
}
)
authenticator = Authenticator(config, NullIO())
authenticator.request("get", "https://foo.bar/alpha/files/simple/foo-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b"bar:alpha").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
authenticator.request("get", "https://foo.bar/beta/files/simple/foo-0.1.0.tar.gz")
request = http.last_request()
basic_auth = base64.b64encode(b"baz:beta").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
def test_authenticator_azure_feed_guid_credentials(
config: Config,
mock_remote: None,
http: type[httpretty.httpretty],
with_simple_keyring: None,
dummy_keyring: DummyBackend,
):
config.merge(
{
"repositories": {
"alpha": {
"url": "https://foo.bar/org-alpha/_packaging/feed/pypi/simple/"
},
"beta": {
"url": "https://foo.bar/org-beta/_packaging/feed/pypi/simple/"
},
},
"http-basic": {
"alpha": {"username": "foo", "password": "bar"},
"beta": {"username": "baz", "password": "qux"},
},
}
)
authenticator = Authenticator(config, NullIO())
authenticator.request(
"get",
"https://foo.bar/org-alpha/_packaging/GUID/pypi/simple/a/1.0.0/a-1.0.0.whl",
)
request = http.last_request()
basic_auth = base64.b64encode(b"foo:bar").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
authenticator.request(
"get",
"https://foo.bar/org-beta/_packaging/GUID/pypi/simple/b/1.0.0/a-1.0.0.whl",
)
request = http.last_request()
basic_auth = base64.b64encode(b"baz:qux").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment