fix: unify verify_ssl semantics across config sources

YAML verify_ssl: true раньше подставлял захардкоженный путь
/etc/ssl/certs/ca-certificates.crt, а REDMINE_VERIFY=true — bool True.
Путь отсутствует на части дистрибутивов, семантика источников различалась.

Теперь едино для YAML и env:
- true (bool/строка) → True: стандартная проверка TLS средствами requests;
- false (bool/строка) → False (+ сохраняется warning из #57);
- иная строка → путь к CA-bundle как есть.
- дефолт (значение не задано) → True вместо захардкоженного пути.

Существующие тесты на путь-от-true переписаны под новую семантику
(изменение поведения): test_verify_ssl_true_returns_default_ca_path →
test_verify_ssl_true_returns_true.

Closes #62
This commit is contained in:
Кокос Артем Николаевич
2026-07-17 13:28:32 +07:00
parent debdede97a
commit 829f1b73fd
4 changed files with 76 additions and 16 deletions

View File

@@ -131,7 +131,7 @@ DEFAULT_TO_DATE=2026-01-31
| `REDMINE_AUTHOR` | Нет | Имя автора для отчёта. | | `REDMINE_AUTHOR` | Нет | Имя автора для отчёта. |
| `DEFAULT_FROM_DATE` | Нет | Начальная дата периода по умолчанию (`YYYY-MM-DD`). | | `DEFAULT_FROM_DATE` | Нет | Начальная дата периода по умолчанию (`YYYY-MM-DD`). |
| `DEFAULT_TO_DATE` | Нет | Конечная дата периода по умолчанию (`YYYY-MM-DD`). Если не задана, а `DEFAULT_FROM_DATE` задана — используется сегодняшняя дата. | | `DEFAULT_TO_DATE` | Нет | Конечная дата периода по умолчанию (`YYYY-MM-DD`). Если не задана, а `DEFAULT_FROM_DATE` задана — используется сегодняшняя дата. |
| `REDMINE_VERIFY` | Нет | TLS-проверка: `true` / `false` / путь к CA bundle. | | `REDMINE_VERIFY` | Нет | TLS-проверка: `true` (по умолчанию — стандартная проверка средствами requests) / `false` / путь к CA bundle. Семантика совпадает с `redmine.verify_ssl` в YAML. |
## Использование ## Использование

View File

@@ -65,6 +65,22 @@ email:
attach: true attach: true
``` ```
### `redmine.verify_ssl` — проверка TLS-сертификата
Управляет проверкой TLS-сертификата Redmine. Семантика едина с переменной
окружения `REDMINE_VERIFY`:
| Значение | Поведение |
|---|---|
| `true` (по умолчанию) | Стандартная проверка TLS средствами requests (системные CA / certifi) |
| `false` | Проверка отключена — при запуске выводится предупреждение о риске MITM |
| строка с путём, например `/etc/ssl/my-ca.pem` | Путь к собственному CA-bundle, передаётся в requests как есть |
До версии с унификацией `verify_ssl: true` подставлял захардкоженный путь
`/etc/ssl/certs/ca-certificates.crt`, который отсутствует на части
дистрибутивов. Теперь `true` в YAML и `REDMINE_VERIFY=true` в env работают
одинаково — оба включают стандартную проверку без привязки к конкретному пути.
### `period.precision` — точность периода ### `period.precision` — точность периода
Определяет, как вычисляется следующий период после фиксации: Определяет, как вычисляется следующий период после фиксации:
@@ -422,7 +438,7 @@ DEFAULT_TO_DATE=2026-06-30
``` ```
Если ни `.env`, ни YAML не заданы — используются кодовые дефолты (текущий месяц Если ни `.env`, ни YAML не заданы — используются кодовые дефолты (текущий месяц
как период, стандартный путь сертификатов, пустой автор). как период, стандартная проверка TLS, пустой автор).
## Безопасность ## Безопасность

View File

@@ -15,7 +15,7 @@ load_dotenv(override=False)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_REDMINE_VERIFY = "/etc/ssl/certs/ca-certificates.crt" DEFAULT_REDMINE_VERIFY: Union[bool, str] = True
FALSE_VALUES = {"0", "false", "no", "off"} FALSE_VALUES = {"0", "false", "no", "off"}
TRUE_VALUES = {"1", "true", "yes", "on"} TRUE_VALUES = {"1", "true", "yes", "on"}
@@ -157,17 +157,23 @@ class AppConfig:
@classmethod @classmethod
def _resolve_verify(cls, raw: dict) -> Union[bool, str]: def _resolve_verify(cls, raw: dict) -> Union[bool, str]:
"""Семантика verify_ssl, единая с REDMINE_VERIFY из env:
- true (bool или строка) → True — стандартная проверка TLS (requests);
- false (bool или строка) → False — проверка отключена;
- любая другая строка → путь к CA-bundle как есть.
"""
value = raw.get("redmine", {}).get("verify_ssl") value = raw.get("redmine", {}).get("verify_ssl")
if value is None: if value is None:
return DEFAULT_REDMINE_VERIFY return DEFAULT_REDMINE_VERIFY
if isinstance(value, bool): if isinstance(value, bool):
return DEFAULT_REDMINE_VERIFY if value else False return value
if isinstance(value, str): if isinstance(value, str):
normalized = value.lower() normalized = value.lower()
if normalized in FALSE_VALUES: if normalized in FALSE_VALUES:
return False return False
if normalized in TRUE_VALUES: if normalized in TRUE_VALUES:
return DEFAULT_REDMINE_VERIFY return True
return resolve_env_vars(value) return resolve_env_vars(value)
return DEFAULT_REDMINE_VERIFY return DEFAULT_REDMINE_VERIFY

View File

@@ -5,12 +5,7 @@ from unittest import mock
import pytest import pytest
from redmine_reporter.config import ( from redmine_reporter.config import AppConfig, Config, EmailConfig
DEFAULT_REDMINE_VERIFY,
AppConfig,
Config,
EmailConfig,
)
@mock.patch.dict( @mock.patch.dict(
@@ -106,7 +101,9 @@ def test_get_default_date_range_fallback():
@mock.patch.dict(os.environ, {}, clear=True) @mock.patch.dict(os.environ, {}, clear=True)
def test_get_redmine_verify_default(): def test_get_redmine_verify_default():
assert Config.get_redmine_verify() == DEFAULT_REDMINE_VERIFY """Без явной настройки проверка TLS включена (стандартные CA requests)."""
Config._app = None
assert Config.get_redmine_verify() is True
@pytest.mark.parametrize("value", ["0", "false", "False", "no", "off"]) @pytest.mark.parametrize("value", ["0", "false", "False", "no", "off"])
@@ -355,16 +352,57 @@ class TestAppConfigFromYaml:
assert cfg.redmine_api_key == "secret-token" assert cfg.redmine_api_key == "secret-token"
def test_verify_ssl_true_returns_default_ca_path(self): def test_verify_ssl_true_returns_true(self):
"""verify_ssl: true → DEFAULT_REDMINE_VERIFY (путь), не Python True.""" """verify_ssl: true → bool True (стандартная проверка TLS)."""
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml" yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text("redmine:\n verify_ssl: true\n") yaml_path.write_text("redmine:\n verify_ssl: true\n")
cfg = AppConfig.from_yaml(yaml_path) cfg = AppConfig.from_yaml(yaml_path)
assert cfg.redmine_verify == DEFAULT_REDMINE_VERIFY assert cfg.redmine_verify is True
assert cfg.redmine_verify is not True # не бул!
@pytest.mark.parametrize("value", ["true", "True", "yes", "on", "1"])
def test_verify_ssl_true_string_returns_true(self, value):
"""Строковые true-значения в YAML → bool True, как и из env."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text(f"redmine:\n verify_ssl: '{value}'\n")
cfg = AppConfig.from_yaml(yaml_path)
assert cfg.redmine_verify is True
def test_verify_ssl_custom_path_returns_path(self):
"""Явный путь к CA-bundle в YAML передаётся как есть."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text("redmine:\n verify_ssl: /etc/ssl/my-ca.pem\n")
cfg = AppConfig.from_yaml(yaml_path)
assert cfg.redmine_verify == "/etc/ssl/my-ca.pem"
@pytest.mark.parametrize("value", ["false", "False", "no", "off", "0"])
def test_verify_ssl_false_string_returns_false(self, value):
"""Строковые false-значения в YAML → bool False."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text(f"redmine:\n verify_ssl: '{value}'\n")
cfg = AppConfig.from_yaml(yaml_path)
assert cfg.redmine_verify is False
def test_verify_ssl_missing_returns_true(self):
"""Без verify_ssl в YAML — проверка TLS включена (True)."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text("redmine:\n url: https://x.com/\n")
cfg = AppConfig.from_yaml(yaml_path)
assert cfg.redmine_verify is True
def test_verify_ssl_false_returns_false(self): def test_verify_ssl_false_returns_false(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp: