Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: allow bool values for repo cert #5719

Merged
merged 1 commit into from
May 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,15 @@ for more information.

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

**Type**: string
**Type**: string | bool

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

This configuration can be set to `false`, if TLS certificate verification should be skipped for this
repository.

### `certificates.<name>.client-cert`:

**Type**: string
Expand Down
15 changes: 15 additions & 0 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,21 @@ poetry config certificates.foo.cert /path/to/ca.pem
poetry config certificates.foo.client-cert /path/to/client.pem
```

{{% note %}}
The value of `certificates.<repository>.cert` can be set to `false` if certificate verification is
required to be skipped. This is useful for cases where a package source with self-signed certificates
are used.

```bash
poetry config certificates.foo.cert false
```

{{% warning %}}
Disabling certificate verification is not recommended as it is does not conform to security
best practices.
{{% /warning %}}
{{% /note %}}

## Caches

Poetry employs multiple caches for package sources in order to improve user experience and avoid duplicate network
Expand Down
28 changes: 16 additions & 12 deletions src/poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import re

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import cast
Expand All @@ -11,7 +12,11 @@
from cleo.helpers import option

from poetry.config.config import PackageFilterPolicy
from poetry.config.config import boolean_normalizer
from poetry.config.config import boolean_validator
from poetry.config.config import int_normalizer
from poetry.console.commands.command import Command
from poetry.locations import DEFAULT_CACHE_DIR


if TYPE_CHECKING:
Expand Down Expand Up @@ -48,13 +53,6 @@ class ConfigCommand(Command):

@property
def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]:
from pathlib import Path

from poetry.config.config import boolean_normalizer
from poetry.config.config import boolean_validator
from poetry.config.config import int_normalizer
from poetry.locations import DEFAULT_CACHE_DIR

unique_config_values = {
"cache-dir": (
str,
Expand Down Expand Up @@ -275,20 +273,26 @@ def handle(self) -> int | None:
return 0

# handle certs
m = re.match(
r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")
)
m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key"))
if m:
repository = m.group(1)
key = m.group(2)

if self.option("unset"):
config.auth_config_source.remove_property(
f"certificates.{m.group(1)}.{m.group(2)}"
f"certificates.{repository}.{key}"
)

return 0

if len(values) == 1:
new_value: str | bool = values[0]

if key == "cert" and boolean_validator(values[0]):
new_value = boolean_normalizer(values[0])

config.auth_config_source.add_property(
f"certificates.{m.group(1)}.{m.group(2)}", values[0]
f"certificates.{repository}.{key}", new_value
)
else:
raise ValueError("You must pass exactly 1 value")
Expand Down
14 changes: 10 additions & 4 deletions src/poetry/installation/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,17 @@ def install(self, package: Package, update: bool = False) -> None:
args += ["--trusted-host", parsed.hostname]

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

if repository.client_cert:
args += ["--client-cert", str(repository.client_cert)]
if certificates.cert:
args += ["--cert", str(certificates.cert)]

if parsed.scheme == "https" and not certificates.verify:
assert parsed.hostname is not None
args += ["--trusted-host", parsed.hostname]

if certificates.client_cert:
args += ["--client-cert", str(certificates.client_cert)]

index_url = repository.authenticated_url

Expand Down
11 changes: 5 additions & 6 deletions src/poetry/publishing/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

from poetry.publishing.uploader import Uploader
from poetry.utils.authenticator import Authenticator
from poetry.utils.helpers import get_cert
from poetry.utils.helpers import get_client_cert


if TYPE_CHECKING:
Expand Down Expand Up @@ -72,9 +70,10 @@ def publish(
username = auth.username
password = auth.password

resolved_client_cert = client_cert or get_client_cert(
self._poetry.config, repository_name
)
certificates = self._authenticator.get_certs_for_repository(repository_name)
resolved_cert = cert or certificates.cert or certificates.verify
resolved_client_cert = client_cert or certificates.client_cert

# Requesting missing credentials but only if there is not a client cert defined.
if not resolved_client_cert and hasattr(self._io, "ask"):
if username is None:
Expand All @@ -96,7 +95,7 @@ def publish(

self._uploader.upload(
url,
cert=cert or get_cert(self._poetry.config, repository_name),
cert=resolved_cert,
client_cert=resolved_client_cert,
dry_run=dry_run,
skip_existing=skip_existing,
Expand Down
8 changes: 3 additions & 5 deletions src/poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import io

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

Expand All @@ -25,8 +26,6 @@


if TYPE_CHECKING:
from pathlib import Path

from cleo.io.null_io import NullIO

from poetry.poetry import Poetry
Expand Down Expand Up @@ -114,15 +113,14 @@ def get_auth(self) -> tuple[str, str] | None:
def upload(
self,
url: str,
cert: Path | None = None,
cert: Path | bool = True,
client_cert: Path | None = None,
dry_run: bool = False,
skip_existing: bool = False,
) -> None:
session = self.make_session()

if cert:
session.verify = str(cert)
session.verify = str(cert) if isinstance(cert, Path) else cert

if client_cert:
session.cert = str(client_cert)
Expand Down
15 changes: 3 additions & 12 deletions src/poetry/repositories/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
if TYPE_CHECKING:
from poetry.config.config import Config
from poetry.inspection.info import PackageInfo
from poetry.utils.authenticator import RepositoryCertificateConfig


class HTTPRepository(CachedRepository, ABC):
Expand Down Expand Up @@ -59,18 +60,8 @@ def url(self) -> str:
return self._url

@property
def cert(self) -> Path | None:
cert = self._authenticator.get_certs_for_url(self.url).get("verify")
if cert:
return Path(cert)
return None

@property
def client_cert(self) -> Path | None:
cert = self._authenticator.get_certs_for_url(self.url).get("cert")
if cert:
return Path(cert)
return None
def certificates(self) -> RepositoryCertificateConfig:
return self._authenticator.get_certs_for_url(self.url)

@property
def authenticated_url(self) -> str:
Expand Down
57 changes: 40 additions & 17 deletions src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import urllib.parse

from os.path import commonprefix
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

Expand All @@ -20,21 +21,42 @@

from poetry.config.config import Config
from poetry.exceptions import PoetryException
from poetry.utils.helpers import get_cert
from poetry.utils.helpers import get_client_cert
from poetry.utils.password_manager import HTTPAuthCredential
from poetry.utils.password_manager import PasswordManager


if TYPE_CHECKING:
from pathlib import Path

from cleo.io.io import IO


logger = logging.getLogger(__name__)


@dataclasses.dataclass(frozen=True)
class RepositoryCertificateConfig:
cert: Path | None = dataclasses.field(default=None)
client_cert: Path | None = dataclasses.field(default=None)
verify: bool = dataclasses.field(default=True)

@classmethod
def create(
cls, repository: str, config: Config | None
) -> RepositoryCertificateConfig:
config = config if config else Config.create()

verify: str | bool = config.get(
f"certificates.{repository}.verify",
config.get(f"certificates.{repository}.cert", True),
)
client_cert: str = config.get(f"certificates.{repository}.client-cert")

return cls(
cert=Path(verify) if isinstance(verify, str) else None,
client_cert=Path(client_cert) if client_cert else None,
verify=verify if isinstance(verify, bool) else True,
)


@dataclasses.dataclass
class AuthenticatorRepositoryConfig:
name: str
Expand All @@ -47,11 +69,8 @@ def __post_init__(self) -> None:
self.netloc = parsed_url.netloc
self.path = parsed_url.path

def certs(self, config: Config) -> dict[str, Path | None]:
return {
"cert": get_client_cert(config, self.name),
"verify": get_cert(config, self.name),
}
def certs(self, config: Config) -> RepositoryCertificateConfig:
return RepositoryCertificateConfig.create(self.name, config)

@property
def http_credential_keys(self) -> list[str]:
Expand Down Expand Up @@ -91,7 +110,7 @@ def __init__(
self._io = io
self._sessions_for_netloc: dict[str, requests.Session] = {}
self._credentials: dict[str, HTTPAuthCredential] = {}
self._certs: dict[str, dict[str, Path | None]] = {}
self._certs: dict[str, RepositoryCertificateConfig] = {}
self._configured_repositories: dict[
str, AuthenticatorRepositoryConfig
] | None = None
Expand Down Expand Up @@ -186,14 +205,13 @@ def request(
stream = kwargs.get("stream")

certs = self.get_certs_for_url(url)
verify = kwargs.get("verify") or certs.get("verify")
cert = kwargs.get("cert") or certs.get("cert")
verify = kwargs.get("verify") or certs.cert or certs.verify
cert = kwargs.get("cert") or certs.client_cert

if cert is not None:
cert = str(cert)

if verify is not None:
verify = str(verify)
verify = str(verify) if isinstance(verify, Path) else verify

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

def get_certs_for_repository(self, name: str) -> RepositoryCertificateConfig:
if name.lower() == "pypi" or name not in self.configured_repositories:
return RepositoryCertificateConfig()
return self.configured_repositories[name].certs(self._config)

@property
def configured_repositories(self) -> dict[str, AuthenticatorRepositoryConfig]:
if self._configured_repositories is None:
Expand All @@ -352,7 +375,7 @@ def add_repository(self, name: str, url: str) -> None:
self.configured_repositories[name] = AuthenticatorRepositoryConfig(name, url)
self.reset_credentials_cache()

def get_certs_for_url(self, url: str) -> dict[str, Path | None]:
def get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
if url not in self._certs:
self._certs[url] = self._get_certs_for_url(url)
return self._certs[url]
Expand Down Expand Up @@ -398,11 +421,11 @@ def _get_repository_config_for_url(

return candidates[0]

def _get_certs_for_url(self, url: str) -> dict[str, Path | None]:
def _get_certs_for_url(self, url: str) -> RepositoryCertificateConfig:
selected = self.get_repository_config_for_url(url)
if selected:
return selected.certs(config=self._config)
return {"cert": None, "verify": None}
return RepositoryCertificateConfig()


_authenticator: Authenticator | None = None
Expand Down
17 changes: 0 additions & 17 deletions src/poetry/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from poetry.core.packages.package import Package
from requests import Session

from poetry.config.config import Config
from poetry.utils.authenticator import Authenticator


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


def get_cert(config: Config, repository_name: str) -> Path | None:
cert = config.get(f"certificates.{repository_name}.cert")
if cert:
return Path(cert)
else:
return None


def get_client_cert(config: Config, repository_name: str) -> Path | None:
client_cert = config.get(f"certificates.{repository_name}.client-cert")
if client_cert:
return Path(client_cert)
else:
return None


def _on_rm_error(func: Callable[[str], None], path: str, exc_info: Exception) -> None:
if not os.path.exists(path):
return
Expand Down