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:
@@ -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. |
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
|
|||||||
@@ -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, пустой автор).
|
||||||
|
|
||||||
## Безопасность
|
## Безопасность
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user