Skip to content

Commit 819d6d4

Browse files
Add cert retrieval for requests (#5320)
* Add cert retrieval for requests The authenticator.py code already retrieves credentials from the config for every request based on url matching. This change makes the authenticator also retrieve certs from the config for each request based on url matching. Also includes unit tests. Co-authored-by: Tucker Beck <[email protected]>
1 parent f462b7f commit 819d6d4

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

src/poetry/utils/authenticator.py

+44-3
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66

77
from typing import TYPE_CHECKING
88
from typing import Any
9+
from typing import Iterator
910

1011
import requests
1112
import requests.auth
1213
import requests.exceptions
1314

1415
from poetry.exceptions import PoetryException
16+
from poetry.utils.helpers import get_cert
17+
from poetry.utils.helpers import get_client_cert
1518
from poetry.utils.password_manager import PasswordManager
1619

1720

1821
if TYPE_CHECKING:
22+
from pathlib import Path
23+
1924
from cleo.io.io import IO
2025

2126
from poetry.config.config import Config
@@ -30,6 +35,7 @@ def __init__(self, config: Config, io: IO | None = None) -> None:
3035
self._io = io
3136
self._session = None
3237
self._credentials = {}
38+
self._certs = {}
3339
self._password_manager = PasswordManager(self._config)
3440

3541
def _log(self, message: str, level: str = "debug") -> None:
@@ -61,8 +67,16 @@ def request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
6167

6268
proxies = kwargs.get("proxies", {})
6369
stream = kwargs.get("stream")
64-
verify = kwargs.get("verify")
65-
cert = kwargs.get("cert")
70+
71+
certs = self.get_certs_for_url(url)
72+
verify = kwargs.get("verify") or certs.get("verify")
73+
cert = kwargs.get("cert") or certs.get("cert")
74+
75+
if cert is not None:
76+
cert = str(cert)
77+
78+
if verify is not None:
79+
verify = str(verify)
6680

6781
settings = session.merge_environment_settings(
6882
prepared_request.url, proxies, stream, verify, cert
@@ -157,7 +171,7 @@ def _get_http_auth(self, name: str, netloc: str | None) -> dict[str, str] | None
157171
return auth
158172

159173
def _get_credentials_for_netloc(self, netloc: str) -> tuple[str | None, str | None]:
160-
for repository_name in self._config.get("repositories", []):
174+
for repository_name, _ in self._get_repository_netlocs():
161175
auth = self._get_http_auth(repository_name, netloc)
162176

163177
if auth is None:
@@ -167,6 +181,22 @@ def _get_credentials_for_netloc(self, netloc: str) -> tuple[str | None, str | No
167181

168182
return None, None
169183

184+
def get_certs_for_url(self, url: str) -> dict[str, Path | None]:
185+
parsed_url = urllib.parse.urlsplit(url)
186+
187+
netloc = parsed_url.netloc
188+
189+
return self._certs.setdefault(
190+
netloc,
191+
self._get_certs_for_netloc_from_config(netloc),
192+
)
193+
194+
def _get_repository_netlocs(self) -> Iterator[tuple[str, str]]:
195+
for repository_name in self._config.get("repositories", []):
196+
url = self._config.get(f"repositories.{repository_name}.url")
197+
parsed_url = urllib.parse.urlsplit(url)
198+
yield repository_name, parsed_url.netloc
199+
170200
def _get_credentials_for_netloc_from_keyring(
171201
self, url: str, netloc: str, username: str | None
172202
) -> dict[str, str] | None:
@@ -193,3 +223,14 @@ def _get_credentials_for_netloc_from_keyring(
193223
}
194224

195225
return None
226+
227+
def _get_certs_for_netloc_from_config(self, netloc: str) -> dict[str, Path | None]:
228+
certs = {"cert": None, "verify": None}
229+
230+
for repository_name, repository_netloc in self._get_repository_netlocs():
231+
if netloc == repository_netloc:
232+
certs["cert"] = get_client_cert(self._config, repository_name)
233+
certs["verify"] = get_cert(self._config, repository_name)
234+
break
235+
236+
return certs

tests/utils/test_authenticator.py

+44
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import uuid
55

66
from dataclasses import dataclass
7+
from pathlib import Path
78
from typing import TYPE_CHECKING
89
from typing import Any
910

@@ -306,3 +307,46 @@ def test_authenticator_uses_env_provided_credentials(
306307
request = http.last_request()
307308

308309
assert request.headers["Authorization"] == "Basic YmFyOmJheg=="
310+
311+
312+
@pytest.mark.parametrize(
313+
"cert,client_cert",
314+
[
315+
(None, None),
316+
(None, "path/to/provided/client-cert"),
317+
("/path/to/provided/cert", None),
318+
("/path/to/provided/cert", "path/to/provided/client-cert"),
319+
],
320+
)
321+
def test_authenticator_uses_certs_from_config_if_not_provided(
322+
config: Config,
323+
mock_remote: type[httpretty.httpretty],
324+
http: type[httpretty.httpretty],
325+
mocker: MockerFixture,
326+
cert: str | None,
327+
client_cert: str | None,
328+
):
329+
configured_cert = "/path/to/cert"
330+
configured_client_cert = "/path/to/client-cert"
331+
config.merge(
332+
{
333+
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
334+
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
335+
"certificates": {
336+
"foo": {"cert": configured_cert, "client-cert": configured_client_cert}
337+
},
338+
}
339+
)
340+
341+
authenticator = Authenticator(config, NullIO())
342+
session_send = mocker.patch.object(authenticator.session, "send")
343+
authenticator.request(
344+
"get",
345+
"https://foo.bar/files/foo-0.1.0.tar.gz",
346+
verify=cert,
347+
cert=client_cert,
348+
)
349+
kwargs = session_send.call_args[1]
350+
351+
assert Path(kwargs["verify"]) == Path(cert or configured_cert)
352+
assert Path(kwargs["cert"]) == Path(client_cert or configured_client_cert)

0 commit comments

Comments
 (0)