(configuration) add configuration Value to support file path in env

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.
This commit is contained in:
soyouzpanda
2025-04-29 13:42:59 +02:00
committed by Manuel Raynaud
parent 88816d1de9
commit 06dc5aa836
6 changed files with 138 additions and 1 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -0,0 +1 @@
"""Test configuration."""

View File

@@ -0,0 +1 @@
TestSecretInFile

View File

@@ -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"