diff --git a/README.md b/README.md index 1cf982e..04a381d 100644 --- a/README.md +++ b/README.md @@ -317,3 +317,4 @@ mypy redmine_reporter - Рекомендуется хранить секреты через `${VAR}`, а не plaintext. - Используйте аккаунт с минимальными правами, достаточными для чтения time entries и задач. - Инструмент работает только в режиме чтения и не изменяет данные в Redmine. +- `REDMINE_URL` обязан использовать HTTPS: API-ключ передаётся в заголовках запроса, и без TLS он может быть перехвачен. diff --git a/redmine_reporter/config.py b/redmine_reporter/config.py index 9d3fc29..53198b8 100644 --- a/redmine_reporter/config.py +++ b/redmine_reporter/config.py @@ -398,8 +398,14 @@ 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_api_key(): return if not (cls.get_redmine_user() and cls.get_redmine_password()): diff --git a/tests/test_config.py b/tests/test_config.py index c1feb7b..a1a58d6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -126,6 +126,56 @@ 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() + + # -- #15: .env не должен переопределять переменные окружения --