From 8e182f66150c2e3d964ea40facab5ec9bdb34c09 Mon Sep 17 00:00:00 2001 From: d9pouces Date: Mon, 5 Oct 2026 22:01:51 +0200 Subject: [PATCH] feat: add support for trusted-host in pip configuration for non-HTTPS repositories Signed-off-by: d9pouces --- id_configurator/modules/python.py | 14 ++++++ tests/test_python.py | 78 +++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/id_configurator/modules/python.py b/id_configurator/modules/python.py index 66b6d02..4081805 100644 --- a/id_configurator/modules/python.py +++ b/id_configurator/modules/python.py @@ -3,6 +3,8 @@ PythonRepository entries instead of pypi.org, and to trust the root certificates installed by the certificates module. - pip: system-wide `/etc/pip.conf` (`index-url`/`extra-index-url`/`cert`). + Any repository whose URL isn't HTTPS has its host added to + `trusted-host`, since pip otherwise refuses a plain HTTP index. - uv: system-wide `/etc/uv/uv.toml` (`[[index]]` entries, `native-tls`). - poetry: `/root/.config/pypoetry/config.toml` (`repositories.*`/`certificates.*`). Poetry has no global "replace PyPI" switch: a project's `pyproject.toml` @@ -23,6 +25,7 @@ from __future__ import annotations import json import logging +from urllib.parse import urlparse from id_configurator.config import PythonRepository from id_configurator.context import Context @@ -41,11 +44,22 @@ def _toml_string(value: str) -> str: return json.dumps(value) +def _trusted_host(repository: PythonRepository) -> str | None: + parsed = urlparse(repository.index_url) + return None if parsed.scheme.lower() == "https" else parsed.hostname + + def _write_pip_conf(context: Context, repositories: list[PythonRepository]) -> None: lines = ["[global]", f"index-url = {repositories[0].index_url}"] if len(repositories) > 1: lines.append("extra-index-url =") lines.extend(f" {repository.index_url}" for repository in repositories[1:]) + trusted_hosts = dict.fromkeys( + host for repository in repositories if (host := _trusted_host(repository)) + ) + if trusted_hosts: + lines.append("trusted-host =") + lines.extend(f" {host}" for host in trusted_hosts) lines.append(f"cert = {_CA_BUNDLE}") context.write_file(_PIP_CONF, ("\n".join(lines) + "\n").encode("utf-8")) logger.info("configured pip to use %s", repositories[0].index_url) diff --git a/tests/test_python.py b/tests/test_python.py index 6721237..0747633 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -76,6 +76,84 @@ def test_apply_single_repository(tmp_path) -> None: assert not (dst / "usr/lib").exists() +def test_apply_adds_trusted_host_for_non_https_repository(tmp_path) -> None: + data = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "configurations": { + "python": { + "repositories": [ + {"name": "interdiode", "index-url": "http://example.invalid/simple"} + ] + } + }, + } + config = Configuration.model_validate(data) + dst = tmp_path / "dst" + context = Context(config=config, dst=dst, src=None) + + PythonModule().apply(context) + + pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8") + assert pip_conf == ( + "[global]\n" + "index-url = http://example.invalid/simple\n" + "trusted-host =\n" + " example.invalid\n" + "cert = /etc/ssl/certs/ca-certificates.crt\n" + ) + + +def test_apply_adds_trusted_host_only_for_non_https_among_mixed_repositories( + tmp_path, +) -> None: + data = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "configurations": { + "python": { + "repositories": [ + { + "name": "interdiode", + "index-url": "https://example.invalid/simple", + }, + { + "name": "insecure", + "index-url": "http://insecure.invalid/simple", + }, + ] + } + }, + } + config = Configuration.model_validate(data) + dst = tmp_path / "dst" + context = Context(config=config, dst=dst, src=None) + + PythonModule().apply(context) + + pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8") + assert pip_conf == ( + "[global]\n" + "index-url = https://example.invalid/simple\n" + "extra-index-url =\n" + " http://insecure.invalid/simple\n" + "trusted-host =\n" + " insecure.invalid\n" + "cert = /etc/ssl/certs/ca-certificates.crt\n" + ) + + +def test_apply_does_not_add_trusted_host_when_all_repositories_https(tmp_path) -> None: + config = Configuration.model_validate(ONE_REPO_CONFIG) + dst = tmp_path / "dst" + context = Context(config=config, dst=dst, src=None) + + PythonModule().apply(context) + + pip_conf = (dst / "etc/pip.conf").read_text(encoding="utf-8") + assert "trusted-host" not in pip_conf + + def test_apply_multiple_repositories(tmp_path) -> None: config = Configuration.model_validate(TWO_REPOS_CONFIG) dst = tmp_path / "dst"