Skip to content

Commit 623ab2a

Browse files
committed
dovecot: security backports, new versioning scheme
- Fix dovecot package download URLs for new [release](https://github.com/chatmail/dovecot/releases/tag/upstream%2F2.3.21%2Bdfsg1-3%2Bchatmail2) with - debian-security backport for 12 CVEs - distro-specific suffix (+deb{release}u1), simplified primary URL path: remove codename subdirectory - Use VERSION_ID from os-release as deb_release instead of codename mapping, reorder hash-dict to match Github release page - Remove redundant parsing/validation, let function validate against hash dict - Update test expectations
1 parent 6b87244 commit 623ab2a

2 files changed

Lines changed: 54 additions & 66 deletions

File tree

‎cmdeploy/src/cmdeploy/dovecot/deployer.py‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@
1414
is_in_container,
1515
)
1616

17-
DOVECOT_ARCHIVE_VERSION = "2.3.21+dfsg1-3"
17+
DOVECOT_ARCHIVE_VERSION = "2.3.21+dfsg1-3+chatmail2"
1818
DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_ARCHIVE_VERSION}"
1919

2020
DOVECOT_SHA256 = {
21-
("amd64", "bookworm", "core"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
22-
("arm64", "bookworm", "core"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
23-
("amd64", "bookworm", "imapd"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
24-
("arm64", "bookworm", "imapd"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
25-
("amd64", "bookworm", "lmtpd"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
26-
("arm64", "bookworm", "lmtpd"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
27-
("amd64", "trixie", "core"): "406d3781ed81e0913c472077dcf62cb1106e3855983efa6e44ddf43b4b0c9be1",
28-
("arm64", "trixie", "core"): "c75b0d9df11a77d07ebd8522920380c167fa47330ddefebe10575d99d0ecdf7f",
29-
("amd64", "trixie", "imapd"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
30-
("arm64", "trixie", "imapd"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
31-
("amd64", "trixie", "lmtpd"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
32-
("arm64", "trixie", "lmtpd"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
21+
("amd64", 12, "core"): "ac3977264d9b9a6fcec53fd3f5cdd2a79ca8aa0324de530c07e535008540826e",
22+
("arm64", 12, "core"): "21626c9c9b52cbdcf1a17b5c09e3c4043e69aa371bf83cc2fcb3b7ddaecdc109",
23+
("amd64", 13, "core"): "47c242ef23c17e700ac19d52d82c9fdb2ebd757d8beb3a7f6781d2de59f87bd0",
24+
("arm64", 13, "core"): "c14c53f112c875f698c4cb6e5870c605cd0a9dd98d35a66e94ceb1827f8020a3",
25+
("amd64", 12, "imapd"): "92a7ab5fc7dc32886a0c34404f919f1335d397b48c467e0c1ef77e56978f60ea",
26+
("arm64", 12, "imapd"): "9369fd566fec4df109ef23debf34ea0417ae85beb29cbe7de619d4d1f31b120c",
27+
("amd64", 13, "imapd"): "e38cc1266455f937ed62f971ea859c47e1a99247841ed0ad946963b524cfdbc5",
28+
("arm64", 13, "imapd"): "11d97dabf23171b37f8b1335dfdb81d408f8b95391aea6d4066aecc9fde01dfe",
29+
("amd64", 12, "lmtpd"): "dc3de473789969f7dd3504ac8783da5e42a446d2d7a305a4e9d7081a6dfe71ab",
30+
("arm64", 12, "lmtpd"): "ae2cbd6c5c43f6d8e2172997b055448f4c79238e2f99cd9ab9200a7d9f548908",
31+
("amd64", 13, "lmtpd"): "833b243e28c7baff141ecf37456e310f5d836e7944a3b9f2fe5074adf0d6a418",
32+
("arm64", 13, "lmtpd"): "55af47a121ba7e23966b20ddaab2dff7feba4b34677864e045e31a702afa180d",
3333
}
3434

3535

@@ -43,13 +43,12 @@ def __init__(self, config, disable_mail):
4343

4444
def install(self):
4545
arch = host.get_fact(Arch)
46-
codename = (host.get_fact(Command, "grep '^VERSION_CODENAME=' /etc/os-release | cut -d= -f2") or "").strip()
47-
if codename not in {key[1] for key in DOVECOT_SHA256}:
48-
raise ValueError(f"Unsupported Debian codename: {codename!r}")
46+
version_line = (host.get_fact(Command, "grep '^VERSION_ID=' /etc/os-release") or "").strip()
47+
deb_release = int(version_line.split("=", 1)[1].strip('"'))
4948
with blocked_service_startup():
5049
debs = []
5150
for pkg in ("core", "imapd", "lmtpd"):
52-
deb, changed = _download_dovecot_package(pkg, arch, codename)
51+
deb, changed = _download_dovecot_package(pkg, arch, deb_release)
5352
self.need_restart |= changed
5453
if deb:
5554
debs.append(deb)
@@ -105,29 +104,32 @@ def _pick_url(primary, fallback):
105104
return fallback
106105

107106

108-
def _download_dovecot_package(package: str, arch: str, codename: str) -> tuple[str | None, bool]:
107+
def _download_dovecot_package(package: str, arch: str, deb_release: int) -> tuple[str | None, bool]:
109108
"""Download a dovecot .deb if needed, return (path, changed)."""
110109
arch = "amd64" if arch == "x86_64" else arch
111110
arch = "arm64" if arch == "aarch64" else arch
112111

113112
pkg_name = f"dovecot-{package}"
114-
sha256 = DOVECOT_SHA256.get((arch, codename, package))
115-
if sha256 is None:
113+
if (arch, deb_release, package) not in DOVECOT_SHA256:
116114
op = apt.packages(packages=[pkg_name])
117115
return None, bool(getattr(op, "changed", False))
116+
sha256 = DOVECOT_SHA256[(arch, deb_release, package)]
118117

119118
installed_versions = host.get_fact(DebPackages).get(pkg_name, [])
120119
if DOVECOT_PACKAGE_VERSION in installed_versions:
121120
return None, False
122121

123-
url_version = DOVECOT_ARCHIVE_VERSION.replace("+", "%2B")
124-
deb_base = f"{pkg_name}_{url_version}_{arch}.deb"
125-
primary_url = f"https://download.delta.chat/dovecot/{codename}/{url_version}/{deb_base}"
126-
upstream_version = DOVECOT_ARCHIVE_VERSION.rsplit("-", 1)[0].replace("+", "%2B")
127-
fallback_deb = f"{pkg_name}_{url_version}_{arch}_{codename}.deb"
128-
fallback_url = f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{upstream_version}/{fallback_deb}"
122+
# Primary URL: flat structure with distro suffix in filename
123+
primary_deb = f"{pkg_name}_{DOVECOT_ARCHIVE_VERSION}+deb{deb_release}u1_{arch}.deb"
124+
primary_url = f"https://download.delta.chat/dovecot/{primary_deb}"
125+
# GitHub release files: escaped + in tag and filename
126+
url_version_escaped = DOVECOT_ARCHIVE_VERSION.replace("+", "%2B")
127+
fallback_deb = f"{pkg_name}_{url_version_escaped}%2Bdeb{deb_release}u1_{arch}.deb"
128+
fallback_url = (
129+
f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{url_version_escaped}/{fallback_deb}"
130+
)
129131
url = _pick_url(primary_url, fallback_url)
130-
deb_filename = f"/root/{deb_base}"
132+
deb_filename = f"/root/{primary_deb}"
131133

132134
files.download(
133135
name=f"Download {pkg_name}",

‎cmdeploy/src/cmdeploy/tests/test_dovecot_deployer.py‎

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ def make_host(*fact_pairs):
2323
def get_fact(cls, *args):
2424
if cls not in facts:
2525
registered = ", ".join(c.__name__ for c in facts)
26-
raise LookupError(
27-
f"unexpected get_fact({cls.__name__}); only registered: {registered}"
28-
)
26+
raise LookupError(f"unexpected get_fact({cls.__name__}); only registered: {registered}")
2927
return facts[cls]
3028

3129
return SimpleNamespace(get_fact=get_fact)
@@ -83,9 +81,7 @@ def test_download_dovecot_package_skips_epoch_matched_install(monkeypatch):
8381
lambda **kwargs: downloads.append(kwargs),
8482
)
8583

86-
deb, changed = dovecot_deployer._download_dovecot_package(
87-
"core", "amd64", codename="bookworm"
88-
)
84+
deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64", deb_release=12)
8985

9086
assert deb is None, f"expected no deb path when version matches, got {deb!r}"
9187
assert changed is False, "should not flag changed when version already installed"
@@ -112,14 +108,11 @@ def test_download_dovecot_package_uses_archive_version_for_url_and_filename(
112108
lambda **kwargs: downloads.append(kwargs),
113109
)
114110

115-
deb, changed = dovecot_deployer._download_dovecot_package(
116-
"core", "amd64", codename="bookworm"
117-
)
111+
deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64", deb_release=12)
118112

119-
archive_version = dovecot_deployer.DOVECOT_ARCHIVE_VERSION.replace("+", "%2B")
120-
expected_deb = f"/root/dovecot-core_{archive_version}_amd64.deb"
113+
expected_deb = f"/root/dovecot-core_{dovecot_deployer.DOVECOT_ARCHIVE_VERSION}+deb12u1_amd64.deb"
121114

122-
# Verify the returned path uses archive version, not package version (with epoch)
115+
# Verify the returned path uses archive version with distro suffix, not package version (with epoch)
123116
assert changed is True, "should flag changed when package not yet installed"
124117
assert deb == expected_deb, f"deb path mismatch: {deb!r} != {expected_deb!r}"
125118
assert dovecot_deployer.DOVECOT_PACKAGE_VERSION not in deb, (
@@ -144,7 +137,7 @@ def test_install_skips_dpkg_path_when_epoch_matched_packages_present(
144137
},
145138
),
146139
(dovecot_deployer.Arch, "x86_64"),
147-
(Command, "bookworm"),
140+
(Command, 'VERSION_ID="12"'),
148141
),
149142
)
150143
downloads = []
@@ -158,20 +151,16 @@ def test_install_skips_dpkg_path_when_epoch_matched_packages_present(
158151

159152
assert downloads == [], "should not download when all packages epoch-matched"
160153
assert track_shell == [], "should not run dpkg when all packages epoch-matched"
161-
assert deployer.need_restart is False, (
162-
"need_restart should be False when nothing changed"
163-
)
154+
assert deployer.need_restart is False, "need_restart should be False when nothing changed"
164155

165156

166-
def test_install_unsupported_arch_falls_back_to_apt(
167-
deployer, patch_blocked, mock_files_put, track_shell, monkeypatch
168-
):
157+
def test_install_unsupported_arch_falls_back_to_apt(deployer, patch_blocked, mock_files_put, track_shell, monkeypatch):
169158
monkeypatch.setattr(
170159
dovecot_deployer,
171160
"host",
172161
make_host(
173162
(dovecot_deployer.Arch, "riscv64"),
174-
(Command, "bookworm"),
163+
(Command, 'VERSION_ID="12"'),
175164
),
176165
)
177166
apt_calls = []
@@ -192,9 +181,7 @@ def fake_apt(**kwargs):
192181
f"expected apt install of core/imapd/lmtpd, got {actual_pkgs}"
193182
)
194183
assert track_shell == [], "should not run dpkg for unsupported arch"
195-
assert deployer.need_restart is True, (
196-
"need_restart should be True when apt installed a package"
197-
)
184+
assert deployer.need_restart is True, "need_restart should be True when apt installed a package"
198185

199186

200187
def test_install_runs_dpkg_when_packages_need_download(
@@ -206,7 +193,7 @@ def test_install_runs_dpkg_when_packages_need_download(
206193
make_host(
207194
(dovecot_deployer.DebPackages, {}),
208195
(dovecot_deployer.Arch, "x86_64"),
209-
(Command, "bookworm"),
196+
(Command, 'VERSION_ID="12"'),
210197
),
211198
)
212199
monkeypatch.setattr(
@@ -222,19 +209,15 @@ def test_install_runs_dpkg_when_packages_need_download(
222209

223210
deployer.install()
224211

225-
assert len(track_shell) == 1, (
226-
f"expected one server.shell() call for dpkg install, got {len(track_shell)}"
227-
)
212+
assert len(track_shell) == 1, f"expected one server.shell() call for dpkg install, got {len(track_shell)}"
228213
cmds = track_shell[0]["commands"]
229214
assert len(cmds) == 1, f"expected single apt-get install command, got: {cmds}"
230215
assert "apt-get install -y" in cmds[0]
231216
assert '-o Dpkg::Options::="--force-confdef"' in cmds[0]
232217
assert '-o Dpkg::Options::="--force-confold"' in cmds[0]
233218
assert "--allow-downgrades" in cmds[0]
234219
assert ".deb" in cmds[0]
235-
assert deployer.need_restart is True, (
236-
"need_restart should be True after dpkg install"
237-
)
220+
assert deployer.need_restart is True, "need_restart should be True after dpkg install"
238221

239222

240223
def test_pick_url_falls_back_on_primary_error(monkeypatch):
@@ -243,22 +226,25 @@ def raise_error(req, timeout):
243226

244227
monkeypatch.setattr(dovecot_deployer.urllib.request, "urlopen", raise_error)
245228
result = dovecot_deployer._pick_url("http://primary", "http://fallback")
246-
assert result == "http://fallback", (
247-
f"should fall back when primary fails, got {result!r}"
248-
)
229+
assert result == "http://fallback", f"should fall back when primary fails, got {result!r}"
249230

250231

251-
def test_install_fails_on_unsupported_debian_version(
252-
deployer, patch_blocked, monkeypatch
253-
):
232+
def test_install_fails_on_unsupported_debian_version(deployer, patch_blocked, monkeypatch):
254233
monkeypatch.setattr(
255234
dovecot_deployer,
256235
"host",
257236
make_host(
258237
(dovecot_deployer.Arch, "x86_64"),
259-
(Command, "sid"),
238+
(Command, 'VERSION_ID="99"'),
260239
),
261240
)
262241

263-
with pytest.raises(ValueError, match="Unsupported Debian codename"):
264-
deployer.install()
242+
# Mock apt.packages to handle unsupported release gracefully
243+
def fake_apt(**kwargs):
244+
return SimpleNamespace(changed=False)
245+
246+
monkeypatch.setattr(dovecot_deployer.apt, "packages", fake_apt)
247+
248+
# Should not error, just skip the dovecot-specific packages and use apt
249+
deployer.install()
250+
assert deployer.need_restart is False, "unsupported release should not trigger restart"

0 commit comments

Comments
 (0)