Skip to content

Commit ac73268

Browse files
abnCeleborn2BeAlivemaayanbar13
committed
config: allow bool values for repo cert
This change allows certificates.<repo>.cert configuration to accept boolean values in addition to certificate paths. This allows for repositories to skip TLS certificate validation for cases where self-signed certificats are used by package sources. In addition to the above, the certificate configuration handling has now been delegated to a dedicated dataclass. Co-authored-by: Celeborn2BeAlive <[email protected]> Co-authored-by: Maayan Bar <[email protected]>
1 parent 6a6034e commit ac73268

File tree

14 files changed

+197
-107
lines changed

14 files changed

+197
-107
lines changed

docs/configuration.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,15 @@ for more information.
315315

316316
### `certificates.<name>.cert`:
317317

318-
**Type**: string
318+
**Type**: string | bool
319319

320320
Set custom certificate authority for repository `<name>`.
321321
See [Repositories - Configuring credentials - Custom certificate authority]({{< relref "repositories#custom-certificate-authority-and-mutual-tls-authentication" >}})
322322
for more information.
323323

324+
This configuration can be set to `false`, if TLS certificate verification should be skipped for this
325+
repository.
326+
324327
### `certificates.<name>.client-cert`:
325328

326329
**Type**: string

docs/repositories.md

+15
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,21 @@ poetry config certificates.foo.cert /path/to/ca.pem
384384
poetry config certificates.foo.client-cert /path/to/client.pem
385385
```
386386

387+
{{% note %}}
388+
The value of `certificates.<repository>.cert` can be set to `false` if certificate verification is
389+
required to be skipped. This is useful for cases where a package source with self-signed certificates
390+
are used.
391+
392+
```bash
393+
poetry config certificates.foo.cert false
394+
```
395+
396+
{{% warning %}}
397+
Disabling certificate verification is not recommended as it is does not conform to security
398+
best practices.
399+
{{% /warning %}}
400+
{{% /note %}}
401+
387402
## Caches
388403

389404
Poetry employs multiple caches for package sources in order to improve user experience and avoid duplicate network

src/poetry/console/commands/config.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import re
55

6+
from pathlib import Path
67
from typing import TYPE_CHECKING
78
from typing import Any
89
from typing import cast
@@ -11,7 +12,11 @@
1112
from cleo.helpers import option
1213

1314
from poetry.config.config import PackageFilterPolicy
15+
from poetry.config.config import boolean_normalizer
16+
from poetry.config.config import boolean_validator
17+
from poetry.config.config import int_normalizer
1418
from poetry.console.commands.command import Command
19+
from poetry.locations import DEFAULT_CACHE_DIR
1520

1621

1722
if TYPE_CHECKING:
@@ -48,13 +53,6 @@ class ConfigCommand(Command):
4853

4954
@property
5055
def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]:
51-
from pathlib import Path
52-
53-
from poetry.config.config import boolean_normalizer
54-
from poetry.config.config import boolean_validator
55-
from poetry.config.config import int_normalizer
56-
from poetry.locations import DEFAULT_CACHE_DIR
57-
5856
unique_config_values = {
5957
"cache-dir": (
6058
str,
@@ -275,20 +273,26 @@ def handle(self) -> int | None:
275273
return 0
276274

277275
# handle certs
278-
m = re.match(
279-
r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")
280-
)
276+
m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key"))
281277
if m:
278+
repository = m.group(1)
279+
key = m.group(2)
280+
282281
if self.option("unset"):
283282
config.auth_config_source.remove_property(
284-
f"certificates.{m.group(1)}.{m.group(2)}"
283+
f"certificates.{repository}.{key}"
285284
)
286285

287286
return 0
288287

289288
if len(values) == 1:
289+
new_value: str | bool = values[0]
290+
291+
if key == "cert" and boolean_validator(values[0]):
292+
new_value = boolean_normalizer(values[0])
293+
290294
config.auth_config_source.add_property(
291-
f"certificates.{m.group(1)}.{m.group(2)}", values[0]
295+
f"certificates.{repository}.{key}", new_value
292296
)
293297
else:
294298
raise ValueError("You must pass exactly 1 value")

src/poetry/installation/pip_installer.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,17 @@ def install(self, package: Package, update: bool = False) -> None:
6363
args += ["--trusted-host", parsed.hostname]
6464

6565
if isinstance(repository, HTTPRepository):
66-
if repository.cert:
67-
args += ["--cert", str(repository.cert)]
66+
certificates = repository.certificates
6867

69-
if repository.client_cert:
70-
args += ["--client-cert", str(repository.client_cert)]
68+
if certificates.cert:
69+
args += ["--cert", str(certificates.cert)]
70+
71+
if parsed.scheme == "https" and not certificates.verify:
72+
assert parsed.hostname is not None
73+
args += ["--trusted-host", parsed.hostname]
74+
75+
if certificates.client_cert:
76+
args += ["--client-cert", str(certificates.client_cert)]
7177

7278
index_url = repository.authenticated_url
7379

src/poetry/publishing/publisher.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from poetry.publishing.uploader import Uploader
88
from poetry.utils.authenticator import Authenticator
9-
from poetry.utils.helpers import get_cert
10-
from poetry.utils.helpers import get_client_cert
119

1210

1311
if TYPE_CHECKING:
@@ -72,9 +70,10 @@ def publish(
7270
username = auth.username
7371
password = auth.password
7472

75-
resolved_client_cert = client_cert or get_client_cert(
76-
self._poetry.config, repository_name
77-
)
73+
certificates = self._authenticator.get_certs_for_repository(repository_name)
74+
resolved_cert = cert or certificates.cert or certificates.verify
75+
resolved_client_cert = client_cert or certificates.client_cert
76+
7877
# Requesting missing credentials but only if there is not a client cert defined.
7978
if not resolved_client_cert and hasattr(self._io, "ask"):
8079
if username is None:
@@ -96,7 +95,7 @@ def publish(
9695

9796
self._uploader.upload(
9897
url,
99-
cert=cert or get_cert(self._poetry.config, repository_name),
98+
cert=resolved_cert,
10099
client_cert=resolved_client_cert,
101100
dry_run=dry_run,
102101
skip_existing=skip_existing,

src/poetry/publishing/uploader.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hashlib
44
import io
55

6+
from pathlib import Path
67
from typing import TYPE_CHECKING
78
from typing import Any
89

@@ -25,8 +26,6 @@
2526

2627

2728
if TYPE_CHECKING:
28-
from pathlib import Path
29-
3029
from cleo.io.null_io import NullIO
3130

3231
from poetry.poetry import Poetry
@@ -114,15 +113,14 @@ def get_auth(self) -> tuple[str, str] | None:
114113
def upload(
115114
self,
116115
url: str,
117-
cert: Path | None = None,
116+
cert: Path | bool = True,
118117
client_cert: Path | None = None,
119118
dry_run: bool = False,
120119
skip_existing: bool = False,
121120
) -> None:
122121
session = self.make_session()
123122

124-
if cert:
125-
session.verify = str(cert)
123+
session.verify = str(cert) if isinstance(cert, Path) else cert
126124

127125
if client_cert:
128126
session.cert = str(client_cert)

src/poetry/repositories/http.py

+3-12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
if TYPE_CHECKING:
3232
from poetry.config.config import Config
3333
from poetry.inspection.info import PackageInfo
34+
from poetry.utils.authenticator import RepositoryCertificateConfig
3435

3536

3637
class HTTPRepository(CachedRepository, ABC):
@@ -59,18 +60,8 @@ def url(self) -> str:
5960
return self._url
6061

6162
@property
62-
def cert(self) -> Path | None:
63-
cert = self._authenticator.get_certs_for_url(self.url).get("verify")
64-
if cert:
65-
return Path(cert)
66-
return None
67-
68-
@property
69-
def client_cert(self) -> Path | None:
70-
cert = self._authenticator.get_certs_for_url(self.url).get("cert")
71-
if cert:
72-
return Path(cert)
73-
return None
63+
def certificates(self) -> RepositoryCertificateConfig:
64+
return self._authenticator.get_certs_for_url(self.url)
7465

7566
@property
7667
def authenticated_url(self) -> str:

src/poetry/utils/authenticator.py

+40-17
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import urllib.parse
99

1010
from os.path import commonprefix
11+
from pathlib import Path
1112
from typing import TYPE_CHECKING
1213
from typing import Any
1314

@@ -20,21 +21,42 @@
2021

2122
from poetry.config.config import Config
2223
from poetry.exceptions import PoetryException
23-
from poetry.utils.helpers import get_cert
24-
from poetry.utils.helpers import get_client_cert
2524
from poetry.utils.password_manager import HTTPAuthCredential
2625
from poetry.utils.password_manager import PasswordManager
2726

2827

2928
if TYPE_CHECKING:
30-
from pathlib import Path
31-
3229
from cleo.io.io import IO
3330

3431

3532
logger = logging.getLogger(__name__)
3633

3734

35+
@dataclasses.dataclass(frozen=True)
36+
class RepositoryCertificateConfig:
37+
cert: Path | None = dataclasses.field(default=None)
38+
client_cert: Path | None = dataclasses.field(default=None)
39+
verify: bool = dataclasses.field(default=True)
40+
41+
@classmethod
42+
def create(
43+
cls, repository: str, config: Config | None
44+
) -> RepositoryCertificateConfig:
45+
config = config if config else Config.create()
46+
47+
verify: str | bool = config.get(
48+
f"certificates.{repository}.verify",
49+
config.get(f"certificates.{repository}.cert", True),
50+
)
51+
client_cert: str = config.get(f"certificates.{repository}.client-cert")
52+
53+
return cls(
54+
cert=Path(verify) if isinstance(verify, str) else None,
55+
client_cert=Path(client_cert) if client_cert else None,
56+
verify=verify if isinstance(verify, bool) else True,
57+
)
58+
59+
3860
@dataclasses.dataclass
3961
class AuthenticatorRepositoryConfig:
4062
name: str
@@ -47,11 +69,8 @@ def __post_init__(self) -> None:
4769
self.netloc = parsed_url.netloc
4870
self.path = parsed_url.path
4971

50-
def certs(self, config: Config) -> dict[str, Path | None]:
51-
return {
52-
"cert": get_client_cert(config, self.name),
53-
"verify": get_cert(config, self.name),
54-
}
72+
def certs(self, config: Config) -> RepositoryCertificateConfig:
73+
return RepositoryCertificateConfig.create(self.name, config)
5574

5675
@property
5776
def http_credential_keys(self) -> list[str]:
@@ -91,7 +110,7 @@ def __init__(
91110
self._io = io
92111
self._sessions_for_netloc: dict[str, requests.Session] = {}
93112
self._credentials: dict[str, HTTPAuthCredential] = {}
94-
self._certs: dict[str, dict[str, Path | None]] = {}
113+
self._certs: dict[str, RepositoryCertificateConfig] = {}
95114
self._configured_repositories: dict[
96115
str, AuthenticatorRepositoryConfig
97116
] | None = None
@@ -186,14 +205,13 @@ def request(
186205
stream = kwargs.get("stream")
187206

188207
certs = self.get_certs_for_url(url)
189-
verify = kwargs.get("verify") or certs.get("verify")
190-
cert = kwargs.get("cert") or certs.get("cert")
208+
verify = kwargs.get("verify") or certs.cert or certs.verify
209+
cert = kwargs.get("cert") or certs.client_cert
191210

192211
if cert is not None:
193212
cert = str(cert)
194213

195-
if verify is not None:
196-
verify = str(verify)
214+
verify = str(verify) if isinstance(verify, Path) else verify
197215

198216
settings = session.merge_environment_settings( # type: ignore[no-untyped-call]
199217
prepared_request.url, proxies, stream, verify, cert
@@ -332,6 +350,11 @@ def get_http_auth(
332350
repository=repository, username=username
333351
)
334352

353+
def get_certs_for_repository(self, name: str) -> RepositoryCertificateConfig:
354+
if name.lower() == "pypi" or name not in self.configured_repositories:
355+
return RepositoryCertificateConfig()
356+
return self.configured_repositories[name].certs(self._config)
357+
335358
@property
336359
def configured_repositories(self) -> dict[str, AuthenticatorRepositoryConfig]:
337360
if self._configured_repositories is None:
@@ -352,7 +375,7 @@ def add_repository(self, name: str, url: str) -> None:
352375
self.configured_repositories[name] = AuthenticatorRepositoryConfig(name, url)
353376
self.reset_credentials_cache()
354377

355-
def get_certs_for_url(self, url: str) -> dict[str, Path | None]:
378+
def get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
356379
if url not in self._certs:
357380
self._certs[url] = self._get_certs_for_url(url)
358381
return self._certs[url]
@@ -398,11 +421,11 @@ def _get_repository_config_for_url(
398421

399422
return candidates[0]
400423

401-
def _get_certs_for_url(self, url: str) -> dict[str, Path | None]:
424+
def _get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
402425
selected = self.get_repository_config_for_url(url)
403426
if selected:
404427
return selected.certs(config=self._config)
405-
return {"cert": None, "verify": None}
428+
return RepositoryCertificateConfig()
406429

407430

408431
_authenticator: Authenticator | None = None

src/poetry/utils/helpers.py

-17
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from poetry.core.packages.package import Package
1919
from requests import Session
2020

21-
from poetry.config.config import Config
2221
from poetry.utils.authenticator import Authenticator
2322

2423

@@ -33,22 +32,6 @@ def module_name(name: str) -> str:
3332
return canonicalize_name(name).replace(".", "_").replace("-", "_")
3433

3534

36-
def get_cert(config: Config, repository_name: str) -> Path | None:
37-
cert = config.get(f"certificates.{repository_name}.cert")
38-
if cert:
39-
return Path(cert)
40-
else:
41-
return None
42-
43-
44-
def get_client_cert(config: Config, repository_name: str) -> Path | None:
45-
client_cert = config.get(f"certificates.{repository_name}.client-cert")
46-
if client_cert:
47-
return Path(client_cert)
48-
else:
49-
return None
50-
51-
5235
def _on_rm_error(func: Callable[[str], None], path: str, exc_info: Exception) -> None:
5336
if not os.path.exists(path):
5437
return

0 commit comments

Comments
 (0)