From ea908d988bf0aa01415b49cbf13bc0b93eae6028 Mon Sep 17 00:00:00 2001 From: soyouzpanda Date: Tue, 29 Apr 2025 13:42:59 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(configuration)=20add=20configuration?= =?UTF-8?q?=20Value=20to=20support=20file=20path=20in=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This supports use of environment variables that either reference a value or a path to file containing the value. This is useful for secrets, to avoid the secret to be in a world-readable environment file. --- CHANGELOG.md | 2 + pyproject.toml | 6 +- src/lasuite/configuration/values.py | 50 ++++++++++++++++ tests/configuration/__init__.py | 1 + tests/configuration/test_secret | 1 + tests/configuration/test_secret_file.py | 79 +++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/lasuite/configuration/values.py create mode 100644 tests/configuration/__init__.py create mode 100644 tests/configuration/test_secret create mode 100644 tests/configuration/test_secret_file.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e404f..eb1eec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to ### Changed +- ✨(configuration) add configuration Value to support file path + in environment #15 - ♻️(malware_detection) retry getting analyse result sooner ## [0.0.8] - 2025-05-06 diff --git a/pyproject.toml b/pyproject.toml index 4dbd72d..98ff6ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,8 +52,12 @@ dev = [ malware_detection = [ "celery>=5.0", ] +configuration = [ + "django-configurations>=2.5.1", +] all=[ - "django-lasuite[malware_detection]" + "django-lasuite[malware_detection]", + "django-lasuite[configuration]", ] [tool.hatch.build.targets.sdist] diff --git a/src/lasuite/configuration/values.py b/src/lasuite/configuration/values.py new file mode 100644 index 0000000..0f105fd --- /dev/null +++ b/src/lasuite/configuration/values.py @@ -0,0 +1,50 @@ +"""Custom value classes for django-configurations.""" + +import os + +from configurations import values + + +class SecretFileValue(values.Value): + """ + Class used to interpret value from environment variables with reading file support. + + The value set is either (in order of priority): + * The content of the file referenced by the environment variable + `{name}_{file_suffix}` if set. + * The value of the environment variable `{name}` if set. + * The default value + """ + + file_suffix = "FILE" + + def __init__(self, *args, **kwargs): + """Initialize the value.""" + super().__init__(*args, **kwargs) + if "file_suffix" in kwargs: + self.file_suffix = kwargs["file_suffix"] + + def setup(self, name): + """Get the value from environment variables.""" + value = self.default + if self.environ: + full_environ_name = self.full_environ_name(name) + full_environ_name_file = f"{full_environ_name}_{self.file_suffix}" + if full_environ_name_file in os.environ: + filename = os.environ[full_environ_name_file] + if not os.path.exists(filename): + raise ValueError(f"Path {filename!r} does not exist.") + try: + with open(filename) as file: + value = self.to_python(file.read().removesuffix("\n")) + except (OSError, PermissionError) as err: + raise ValueError(f"Path {filename!r} cannot be read: {err!r}") from err + elif full_environ_name in os.environ: + value = self.to_python(os.environ[full_environ_name]) + elif self.environ_required: + raise ValueError( + f"Value {name!r} is required to be set as the " + f"environment variable {full_environ_name_file!r} or {full_environ_name!r}" + ) + self.value = value + return value diff --git a/tests/configuration/__init__.py b/tests/configuration/__init__.py new file mode 100644 index 0000000..2786ce9 --- /dev/null +++ b/tests/configuration/__init__.py @@ -0,0 +1 @@ +"""Test configuration.""" diff --git a/tests/configuration/test_secret b/tests/configuration/test_secret new file mode 100644 index 0000000..9a3f563 --- /dev/null +++ b/tests/configuration/test_secret @@ -0,0 +1 @@ +TestSecretInFile diff --git a/tests/configuration/test_secret_file.py b/tests/configuration/test_secret_file.py new file mode 100644 index 0000000..18a23fd --- /dev/null +++ b/tests/configuration/test_secret_file.py @@ -0,0 +1,79 @@ +"""Tests for SecretFileValue.""" + +import os + +import pytest + +from lasuite.configuration.values import SecretFileValue + +FILE_SECRET_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_secret") + + +@pytest.fixture(autouse=True) +def _mock_clear_env(monkeypatch): + """Reset environment variables.""" + monkeypatch.delenv("DJANGO_TEST_SECRET_KEY", raising=False) + monkeypatch.delenv("DJANGO_TEST_SECRET_KEY_FILE", raising=False) + monkeypatch.delenv("DJANGO_TEST_SECRET_KEY_PATH", raising=False) + + +@pytest.fixture +def _mock_secret_key_env(monkeypatch): + """Set secret key in environment variable.""" + monkeypatch.setenv("DJANGO_TEST_SECRET_KEY", "TestSecretInEnv") + + +@pytest.fixture +def _mock_secret_key_file_env(monkeypatch): + """Set secret key path in environment variable.""" + monkeypatch.setenv("DJANGO_TEST_SECRET_KEY_FILE", FILE_SECRET_PATH) + + +@pytest.fixture +def _mock_secret_key_path_env(monkeypatch): + """Set secret key path in environment variable with another `file_suffix`.""" + monkeypatch.setenv("DJANGO_TEST_SECRET_KEY_PATH", FILE_SECRET_PATH) + + +def test_secret_default(): + """Test call with no environment variable.""" + value = SecretFileValue("DefaultTestSecret") + assert value.setup("TEST_SECRET_KEY") == "DefaultTestSecret" + + +@pytest.mark.usefixtures("_mock_secret_key_env") +def test_secret_in_env(): + """Test call with secret key environment variable.""" + value = SecretFileValue("DefaultTestSecret") + assert os.environ["DJANGO_TEST_SECRET_KEY"] == "TestSecretInEnv" + assert value.setup("TEST_SECRET_KEY") == "TestSecretInEnv" + + +@pytest.mark.usefixtures("_mock_secret_key_file_env") +def test_secret_in_file(): + """Test call with secret key file environment variable.""" + value = SecretFileValue("DefaultTestSecret") + assert os.environ["DJANGO_TEST_SECRET_KEY_FILE"] == FILE_SECRET_PATH + assert value.setup("TEST_SECRET_KEY") == "TestSecretInFile" + + +def test_secret_default_suffix(): + """Test call with no environment variable and non default `file_suffix`.""" + value = SecretFileValue("DefaultTestSecret", file_suffix="PATH") + assert value.setup("TEST_SECRET_KEY") == "DefaultTestSecret" + + +@pytest.mark.usefixtures("_mock_secret_key_env") +def test_secret_in_env_suffix(): + """Test call with secret key environment variable and non default `file_suffix`.""" + value = SecretFileValue("DefaultTestSecret", file_suffix="PATH") + assert os.environ["DJANGO_TEST_SECRET_KEY"] == "TestSecretInEnv" + assert value.setup("TEST_SECRET_KEY") == "TestSecretInEnv" + + +@pytest.mark.usefixtures("_mock_secret_key_path_env") +def test_secret_in_file_suffix(): + """Test call with secret key file environment variable and non default `file_suffix`.""" + value = SecretFileValue("DefaultTestSecret", file_suffix="PATH") + assert os.environ["DJANGO_TEST_SECRET_KEY_PATH"] == FILE_SECRET_PATH + assert value.setup("TEST_SECRET_KEY") == "TestSecretInFile"