Add CERT_DAYS setting and resolve_cert_days()
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,10 @@ SMTP_TLS = "starttls" # "starttls" | "ssl" | "" (plain)
|
|||||||
|
|
||||||
DAYS_PAST = 30
|
DAYS_PAST = 30
|
||||||
DAYS_AHEAD = 14
|
DAYS_AHEAD = 14
|
||||||
|
CERT_DAYS = "default" # lifetime of certs this tool issues, in days.
|
||||||
|
# "default" or "" = leave it to EasyRSA (your vars'
|
||||||
|
# EASYRSA_CERT_EXPIRE). A positive integer overrides
|
||||||
|
# it — e.g. 90.
|
||||||
|
|
||||||
# Names an external .conf file is allowed to override — keep in sync with the
|
# Names an external .conf file is allowed to override — keep in sync with the
|
||||||
# SETTINGS block above. Deliberately excludes CONFIG_PATH itself.
|
# SETTINGS block above. Deliberately excludes CONFIG_PATH itself.
|
||||||
@@ -85,7 +89,7 @@ _OVERRIDABLE_SETTINGS = frozenset({
|
|||||||
"CRYPTGEON_URL",
|
"CRYPTGEON_URL",
|
||||||
"MAIL_FROM", "MAIL_SUBJECT", "EMAIL_TEMPLATE_PATH", "MAIL_BINARY",
|
"MAIL_FROM", "MAIL_SUBJECT", "EMAIL_TEMPLATE_PATH", "MAIL_BINARY",
|
||||||
"SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD", "SMTP_TLS",
|
"SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD", "SMTP_TLS",
|
||||||
"DAYS_PAST", "DAYS_AHEAD",
|
"DAYS_PAST", "DAYS_AHEAD", "CERT_DAYS",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -413,6 +417,37 @@ def resolve_ca_passphrase(
|
|||||||
return configured
|
return configured
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cert_days(configured, name: str = "CERT_DAYS") -> str:
|
||||||
|
"""Resolve a configured certificate lifetime into an EasyRSA --days value.
|
||||||
|
|
||||||
|
"default" / "" → "" (omit --days; EasyRSA's EASYRSA_CERT_EXPIRE wins)
|
||||||
|
positive number → that many days, normalised ("090" → "90")
|
||||||
|
|
||||||
|
Rejects 0, negatives and non-numeric input. EasyRSA rejects those itself,
|
||||||
|
but failing here keeps the error next to the input the user actually typed
|
||||||
|
— in the TUI that means the form is still open, and on the CLI it means no
|
||||||
|
CA passphrase prompt and no subprocess round-trip first.
|
||||||
|
|
||||||
|
`name` names the source ("CERT_DAYS", "--days", "Days") for the message.
|
||||||
|
"""
|
||||||
|
text = str(configured).strip()
|
||||||
|
if text in ("", "default"):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
days = int(text)
|
||||||
|
except ValueError:
|
||||||
|
raise ConfigError(
|
||||||
|
f'{name} must be a positive number of days, "default" or "" '
|
||||||
|
f'(inherit from EasyRSA); got {configured!r}'
|
||||||
|
)
|
||||||
|
if days <= 0:
|
||||||
|
raise ConfigError(
|
||||||
|
f"{name} must be a positive number of days "
|
||||||
|
f"(EasyRSA rejects 0); got {configured!r}"
|
||||||
|
)
|
||||||
|
return str(days)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# === CONFIG ===
|
# === CONFIG ===
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -60,3 +60,10 @@ def test_comments_and_non_string_types_supported(tmp_path):
|
|||||||
)
|
)
|
||||||
overrides = load_settings_overrides(None, "", _script_path(tmp_path))
|
overrides = load_settings_overrides(None, "", _script_path(tmp_path))
|
||||||
assert overrides == {"SMTP_PORT": 2525, "CA_PASSPHRASE": ""}
|
assert overrides == {"SMTP_PORT": 2525, "CA_PASSPHRASE": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cert_days_is_overridable(tmp_path):
|
||||||
|
conf = tmp_path / "app.conf"
|
||||||
|
conf.write_text('CERT_DAYS = 90\n')
|
||||||
|
out = load_settings_overrides(str(conf), "", str(tmp_path / "app.py"))
|
||||||
|
assert out["CERT_DAYS"] == 90
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pytest
|
|||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from openvpncertupdate import (
|
from openvpncertupdate import (
|
||||||
revoke_issued, build_client_full, gen_crl, copy_crl, EasyRSAError,
|
revoke_issued, build_client_full, gen_crl, copy_crl, EasyRSAError,
|
||||||
is_ca_key_encrypted, resolve_ca_passphrase,
|
is_ca_key_encrypted, resolve_ca_passphrase, resolve_cert_days, ConfigError,
|
||||||
has_issued_cert, issued_cert_path,
|
has_issued_cert, issued_cert_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -244,3 +244,45 @@ def test_resolve_literal_passphrase_passed_through_unchanged(tmp_path):
|
|||||||
result = resolve_ca_passphrase("mysecret", str(tmp_path), prompt=prompt)
|
result = resolve_ca_passphrase("mysecret", str(tmp_path), prompt=prompt)
|
||||||
assert result == "mysecret"
|
assert result == "mysecret"
|
||||||
prompt.assert_not_called()
|
prompt.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# resolve_cert_days
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("configured", ["default", "", " ", " default "])
|
||||||
|
def test_resolve_cert_days_sentinels_mean_inherit(configured):
|
||||||
|
# "" and "default" leave the lifetime to EasyRSA's own EASYRSA_CERT_EXPIRE,
|
||||||
|
# so deploying this cannot silently shorten certs on an existing PKI.
|
||||||
|
assert resolve_cert_days(configured) == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("configured,expected", [
|
||||||
|
(90, "90"), ("90", "90"), (" 90 ", "90"), (1, "1"), (3650, "3650"),
|
||||||
|
])
|
||||||
|
def test_resolve_cert_days_accepts_positive_numbers(configured, expected):
|
||||||
|
assert resolve_cert_days(configured) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_cert_days_normalises_leading_zeros():
|
||||||
|
# EasyRSA's own pattern rejects "090" with "Number expected", which reads
|
||||||
|
# as nonsense to someone who typed only digits.
|
||||||
|
assert resolve_cert_days("090") == "90"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_cert_days_rejects_zero():
|
||||||
|
with pytest.raises(ConfigError, match="positive"):
|
||||||
|
resolve_cert_days("0")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("configured", ["-5", -5, "abc", "9 0", "90.5", 90.5, None])
|
||||||
|
def test_resolve_cert_days_rejects_junk(configured):
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
resolve_cert_days(configured)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_cert_days_error_names_the_source():
|
||||||
|
# The same resolver serves CERT_DAYS, --days and the TUI field; the message
|
||||||
|
# must say which one the user actually touched.
|
||||||
|
with pytest.raises(ConfigError, match="--days"):
|
||||||
|
resolve_cert_days("0", "--days")
|
||||||
|
|||||||
Reference in New Issue
Block a user