mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: update APT module to support inline ASCII-armored GPG keys in Signed-By field
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
FROM debian:trixie-slim AS target
|
||||
FROM interdiode/id-configurator:latest AS configuration
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=target / /src/
|
||||
COPY --from=generated-config Apt.yaml /config.yaml
|
||||
RUN id-configurator apply --config /config.yaml --src /src --dst /dst
|
||||
|
||||
+22
-77
@@ -1,14 +1,13 @@
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from id_configurator.config import Configuration
|
||||
from id_configurator.context import Context
|
||||
from id_configurator.modules.apt import AptModule
|
||||
|
||||
KEY_CONTENT = "fake-armored-gpg-key"
|
||||
KEYRING_BYTES = b"fake-dearmored-keyring-bytes"
|
||||
KEY_CONTENT = (
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
"\n"
|
||||
"mQENBGqh08IBCAC0\n"
|
||||
"-----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
)
|
||||
|
||||
SOURCE_CONTENT = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
@@ -52,16 +51,6 @@ def make_config_with(source_extra: dict, name: str = "interdiode") -> Configurat
|
||||
return Configuration.model_validate(data)
|
||||
|
||||
|
||||
def _mock_gpg_dearmor_success(monkeypatch, output: bytes = KEYRING_BYTES) -> MagicMock:
|
||||
mock_run = MagicMock(
|
||||
return_value=subprocess.CompletedProcess(
|
||||
args=[], returncode=0, stdout=output, stderr=b""
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run)
|
||||
return mock_run
|
||||
|
||||
|
||||
def test_is_relevant_false_without_src(tmp_path) -> None:
|
||||
context = Context(config=make_config(), dst=tmp_path / "dst", src=None)
|
||||
assert AptModule().is_relevant(context) is False
|
||||
@@ -81,10 +70,7 @@ def test_is_relevant_true_with_apt_dir(tmp_path) -> None:
|
||||
assert AptModule().is_relevant(context) is True
|
||||
|
||||
|
||||
def test_apply_disables_existing_sources_and_adds_configured_ones(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
apt_dir = src / "etc/apt"
|
||||
sources_list_d = apt_dir / "sources.list.d"
|
||||
@@ -133,22 +119,21 @@ def test_apply_disables_existing_sources_and_adds_configured_ones(
|
||||
|
||||
assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists()
|
||||
|
||||
keyring = (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes()
|
||||
assert keyring == KEYRING_BYTES
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args.kwargs["input"] == KEY_CONTENT.encode("utf-8")
|
||||
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert configured_source == (
|
||||
"Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n"
|
||||
"Signed-By: /etc/apt/keyrings/interdiode.gpg\nEnabled: yes\n"
|
||||
"Signed-By:\n"
|
||||
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
" .\n"
|
||||
" mQENBGqh08IBCAC0\n"
|
||||
" -----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
"Enabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_without_existing_top_level_sources_list(tmp_path, monkeypatch) -> None:
|
||||
_mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_without_existing_top_level_sources_list(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
(src / "etc/apt").mkdir(parents=True)
|
||||
dst = tmp_path / "dst"
|
||||
@@ -160,8 +145,7 @@ def test_apply_without_existing_top_level_sources_list(tmp_path, monkeypatch) ->
|
||||
assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists()
|
||||
|
||||
|
||||
def test_apply_uses_signed_by_file_when_no_key_content(tmp_path, monkeypatch) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_uses_signed_by_file_when_no_key_content(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"signed-by-file": "/etc/apt/keyrings/existing.gpg"}),
|
||||
@@ -171,8 +155,6 @@ def test_apply_uses_signed_by_file_when_no_key_content(tmp_path, monkeypatch) ->
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
mock_run.assert_not_called()
|
||||
assert not (dst / "etc/apt/keyrings").exists()
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -182,10 +164,7 @@ def test_apply_uses_signed_by_file_when_no_key_content(tmp_path, monkeypatch) ->
|
||||
)
|
||||
|
||||
|
||||
def test_apply_signed_by_takes_precedence_over_signed_by_file(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_signed_by_takes_precedence_over_signed_by_file(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with(
|
||||
@@ -200,25 +179,19 @@ def test_apply_signed_by_takes_precedence_over_signed_by_file(
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
mock_run.assert_called_once()
|
||||
assert (dst / "etc/apt/keyrings/interdiode.gpg").read_bytes() == KEYRING_BYTES
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "Signed-By: /etc/apt/keyrings/interdiode.gpg\n" in configured_source
|
||||
assert "Signed-By:\n -----BEGIN PGP PUBLIC KEY BLOCK-----" in configured_source
|
||||
assert "existing.gpg" not in configured_source
|
||||
|
||||
|
||||
def test_apply_without_signed_by_writes_no_keyring_or_field(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_without_signed_by_writes_no_field(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config_with({}), dst=dst, src=None)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
mock_run.assert_not_called()
|
||||
assert not (dst / "etc/apt/keyrings").exists()
|
||||
configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -227,8 +200,7 @@ def test_apply_without_signed_by_writes_no_keyring_or_field(
|
||||
)
|
||||
|
||||
|
||||
def test_apply_sanitizes_source_name_with_slash(tmp_path, monkeypatch) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
def test_apply_sanitizes_source_name_with_slash(tmp_path) -> None:
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"signed-by": KEY_CONTENT}, name="../../evil"),
|
||||
@@ -238,33 +210,6 @@ def test_apply_sanitizes_source_name_with_slash(tmp_path, monkeypatch) -> None:
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
keyring = dst / "etc/apt/keyrings/.._.._evil.gpg"
|
||||
assert keyring.read_bytes() == KEYRING_BYTES
|
||||
source_file = dst / "etc/apt/sources.list.d/.._.._evil.sources"
|
||||
assert "Signed-By: /etc/apt/keyrings/.._.._evil.gpg" in source_file.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_resolve_signed_by_raises_when_gpg_not_on_path(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("id_configurator.modules.apt.shutil.which", lambda _: None)
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(), dst=dst, src=None)
|
||||
|
||||
with pytest.raises(RuntimeError, match="gpg is not available"):
|
||||
AptModule().apply(context)
|
||||
|
||||
|
||||
def test_resolve_signed_by_raises_on_invalid_key(tmp_path, monkeypatch) -> None:
|
||||
mock_run = MagicMock(
|
||||
return_value=subprocess.CompletedProcess(
|
||||
args=[], returncode=2, stdout=b"", stderr=b"gpg: no valid data found"
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr("id_configurator.modules.apt.subprocess.run", mock_run)
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(), dst=dst, src=None)
|
||||
|
||||
with pytest.raises(RuntimeError, match="invalid GPG key for apt source interdiode"):
|
||||
AptModule().apply(context)
|
||||
assert source_file.exists()
|
||||
assert not (dst / "etc/apt/sources.list.d/evil.sources").exists()
|
||||
|
||||
@@ -226,3 +226,39 @@ def test_apt_source_to_deb822_full() -> None:
|
||||
"Description: InterDiode APT repository",
|
||||
"Enabled: no",
|
||||
]
|
||||
|
||||
|
||||
def test_apt_source_to_deb822_folds_inline_signed_by_key() -> None:
|
||||
source = AptSource(
|
||||
name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"]
|
||||
)
|
||||
armored_key = (
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
"\n"
|
||||
"mQENBGqh08IBCAC0\n"
|
||||
"-----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
)
|
||||
rendered = source.to_deb822(signed_by_armored_key=armored_key)
|
||||
assert rendered == (
|
||||
"Types: deb\n"
|
||||
"URIs: http://example.invalid/debian\n"
|
||||
"Suites: stable\n"
|
||||
"Signed-By:\n"
|
||||
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
" .\n"
|
||||
" mQENBGqh08IBCAC0\n"
|
||||
" -----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
"Enabled: yes\n"
|
||||
)
|
||||
|
||||
|
||||
def test_apt_source_to_deb822_signed_by_armored_key_takes_precedence() -> None:
|
||||
source = AptSource(
|
||||
name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"]
|
||||
)
|
||||
rendered = source.to_deb822(
|
||||
signed_by_armored_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\nabc\n-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
signed_by_path="/etc/apt/keyrings/unused.gpg",
|
||||
)
|
||||
assert "Signed-By:\n" in rendered
|
||||
assert "unused.gpg" not in rendered
|
||||
|
||||
Reference in New Issue
Block a user