Merge branch 'fix/54-57-config-security'
This commit is contained in:
@@ -317,3 +317,5 @@ mypy redmine_reporter
|
||||
- Рекомендуется хранить секреты через `${VAR}`, а не plaintext.
|
||||
- Используйте аккаунт с минимальными правами, достаточными для чтения time entries и задач.
|
||||
- Инструмент работает только в режиме чтения и не изменяет данные в Redmine.
|
||||
- `REDMINE_URL` обязан использовать HTTPS: API-ключ передаётся в заголовках запроса, и без TLS он может быть перехвачен.
|
||||
- `REDMINE_VERIFY=false` (или `verify_ssl: false`) отключает проверку TLS-сертификата — соединение уязвимо для MITM-атак; при старте выводится предупреждение. Используйте только в доверенной сети.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
@@ -398,8 +399,21 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def validate(cls) -> None:
|
||||
if not cls.get_redmine_url():
|
||||
url = cls.get_redmine_url()
|
||||
if not url:
|
||||
raise ValueError("REDMINE_URL is required (set via env or .env)")
|
||||
if not url.lower().startswith("https://"):
|
||||
raise ValueError(
|
||||
"REDMINE_URL must use HTTPS: the API key is sent in request "
|
||||
"headers and requires TLS"
|
||||
)
|
||||
if cls.get_redmine_verify() is False:
|
||||
print(
|
||||
"⚠️ TLS certificate verification is disabled "
|
||||
"(REDMINE_VERIFY=false / verify_ssl: false): connection is "
|
||||
"vulnerable to MITM attacks",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if cls.get_redmine_api_key():
|
||||
return
|
||||
if not (cls.get_redmine_user() and cls.get_redmine_password()):
|
||||
|
||||
@@ -126,6 +126,96 @@ def test_get_redmine_verify_custom_path():
|
||||
assert Config.get_redmine_verify() == "/tmp/redmine-ca.pem"
|
||||
|
||||
|
||||
# -- #54: REDMINE_URL обязан использовать HTTPS --
|
||||
|
||||
|
||||
class TestConfigRequiresHttpsUrl:
|
||||
"""Config.validate() отклоняет URL без TLS: API-ключ идёт в заголовках."""
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"REDMINE_URL": "http://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||
clear=True,
|
||||
)
|
||||
def test_http_url_rejected(self):
|
||||
with pytest.raises(ValueError, match="HTTPS"):
|
||||
Config.validate()
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"REDMINE_URL": "red.eltex.loc", "REDMINE_API_KEY": "token"},
|
||||
clear=True,
|
||||
)
|
||||
def test_url_without_scheme_rejected(self):
|
||||
with pytest.raises(ValueError, match="HTTPS"):
|
||||
Config.validate()
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"REDMINE_URL": "ftp://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||
clear=True,
|
||||
)
|
||||
def test_non_http_scheme_rejected(self):
|
||||
with pytest.raises(ValueError, match="HTTPS"):
|
||||
Config.validate()
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||
clear=True,
|
||||
)
|
||||
def test_https_url_accepted(self):
|
||||
Config.validate()
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"REDMINE_URL": "HTTPS://red.eltex.loc/", "REDMINE_API_KEY": "token"},
|
||||
clear=True,
|
||||
)
|
||||
def test_uppercase_scheme_accepted(self):
|
||||
Config.validate()
|
||||
|
||||
|
||||
# -- #57: предупреждение при отключённой проверке TLS --
|
||||
|
||||
|
||||
class TestWarnWhenTlsVerificationDisabled:
|
||||
"""verify_ssl: false должен давать видимый warning в stderr (риск MITM)."""
|
||||
|
||||
_ENV = {"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_API_KEY": "token"}
|
||||
|
||||
def _validate_env(self, verify: str):
|
||||
return {**self._ENV, "REDMINE_VERIFY": verify}
|
||||
|
||||
def test_verify_false_warns_in_stderr(self, capsys):
|
||||
with mock.patch.dict(os.environ, self._validate_env("false"), clear=True):
|
||||
Config.validate()
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "⚠️" in err
|
||||
assert "TLS" in err or "SSL" in err
|
||||
|
||||
def test_verify_true_no_warning(self, capsys):
|
||||
with mock.patch.dict(os.environ, self._validate_env("true"), clear=True):
|
||||
Config.validate()
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
def test_verify_ca_path_no_warning(self, capsys):
|
||||
with mock.patch.dict(
|
||||
os.environ, self._validate_env("/tmp/redmine-ca.pem"), clear=True
|
||||
):
|
||||
Config.validate()
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
def test_verify_default_no_warning(self, capsys):
|
||||
with mock.patch.dict(os.environ, self._ENV, clear=True):
|
||||
Config.validate()
|
||||
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
|
||||
# -- #15: .env не должен переопределять переменные окружения --
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user