Skip to content

Commit 1ee6593

Browse files
committed
✨(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.
1 parent 88816d1 commit 1ee6593

File tree

6 files changed

+137
-1
lines changed

6 files changed

+137
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to
1010

1111
### Changed
1212

13+
- ✨(configuration) add configuration Value to support file path in environment #15
1314
- ♻️(malware_detection) retry getting analyse result sooner
1415

1516
## [0.0.8] - 2025-05-06

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ dev = [
5252
malware_detection = [
5353
"celery>=5.0",
5454
]
55+
configuration = [
56+
"django-configurations>=2.5.1",
57+
]
5558
all=[
56-
"django-lasuite[malware_detection]"
59+
"django-lasuite[malware_detection]",
60+
"django-lasuite[configuration]",
5761
]
5862

5963
[tool.hatch.build.targets.sdist]

src/lasuite/configuration/values.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Custom value classes for django-configurations."""
2+
3+
import os
4+
5+
from configurations import values
6+
7+
8+
class SecretFileValue(values.Value):
9+
"""
10+
Class used to interpret value from environment variables with reading file support.
11+
12+
The value set is either (in order of priority):
13+
* The content of the file referenced by the environment variable
14+
`{name}_{file_suffix}` if set.
15+
* The value of the environment variable `{name}` if set.
16+
* The default value
17+
"""
18+
19+
file_suffix = "FILE"
20+
21+
def __init__(self, *args, **kwargs):
22+
"""Initialize the value."""
23+
super().__init__(*args, **kwargs)
24+
if "file_suffix" in kwargs:
25+
self.file_suffix = kwargs["file_suffix"]
26+
27+
def setup(self, name):
28+
"""Get the value from environment variables."""
29+
value = self.default
30+
if self.environ:
31+
full_environ_name = self.full_environ_name(name)
32+
full_environ_name_file = f"{full_environ_name}_{self.file_suffix}"
33+
if full_environ_name_file in os.environ:
34+
filename = os.environ[full_environ_name_file]
35+
if not os.path.exists(filename):
36+
raise ValueError(f"Path {filename!r} does not exist.")
37+
try:
38+
with open(filename) as file:
39+
value = self.to_python(file.read().removesuffix("\n"))
40+
except (OSError, PermissionError) as err:
41+
raise ValueError(f"Path {filename!r} cannot be read: {err!r}") from err
42+
elif full_environ_name in os.environ:
43+
value = self.to_python(os.environ[full_environ_name])
44+
elif self.environ_required:
45+
raise ValueError(
46+
f"Value {name!r} is required to be set as the "
47+
f"environment variable {full_environ_name_file!r} or {full_environ_name!r}"
48+
)
49+
self.value = value
50+
return value

tests/configuration/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test configuration."""

tests/configuration/test_secret

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TestSecretInFile
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Tests for SecretFileValue."""
2+
3+
import os
4+
5+
import pytest
6+
7+
from lasuite.configuration.values import SecretFileValue
8+
9+
FILE_SECRET_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_secret")
10+
11+
12+
@pytest.fixture(autouse=True)
13+
def _mock_clear_env(monkeypatch):
14+
"""Reset environment variables."""
15+
monkeypatch.delenv("DJANGO_TEST_SECRET_KEY", raising=False)
16+
monkeypatch.delenv("DJANGO_TEST_SECRET_KEY_FILE", raising=False)
17+
monkeypatch.delenv("DJANGO_TEST_SECRET_KEY_PATH", raising=False)
18+
19+
20+
@pytest.fixture
21+
def _mock_secret_key_env(monkeypatch):
22+
"""Set secret key in environment variable."""
23+
monkeypatch.setenv("DJANGO_TEST_SECRET_KEY", "TestSecretInEnv")
24+
25+
26+
@pytest.fixture
27+
def _mock_secret_key_file_env(monkeypatch):
28+
"""Set secret key path in environment variable."""
29+
monkeypatch.setenv("DJANGO_TEST_SECRET_KEY_FILE", FILE_SECRET_PATH)
30+
31+
32+
@pytest.fixture
33+
def _mock_secret_key_path_env(monkeypatch):
34+
"""Set secret key path in environment variable with another `file_suffix`."""
35+
monkeypatch.setenv("DJANGO_TEST_SECRET_KEY_PATH", FILE_SECRET_PATH)
36+
37+
38+
def test_secret_default():
39+
"""Test call with no environment variable."""
40+
value = SecretFileValue("DefaultTestSecret")
41+
assert value.setup("TEST_SECRET_KEY") == "DefaultTestSecret"
42+
43+
44+
@pytest.mark.usefixtures("_mock_secret_key_env")
45+
def test_secret_in_env():
46+
"""Test call with secret key environment variable."""
47+
value = SecretFileValue("DefaultTestSecret")
48+
assert os.environ["DJANGO_TEST_SECRET_KEY"] == "TestSecretInEnv"
49+
assert value.setup("TEST_SECRET_KEY") == "TestSecretInEnv"
50+
51+
52+
@pytest.mark.usefixtures("_mock_secret_key_file_env")
53+
def test_secret_in_file():
54+
"""Test call with secret key file environment variable."""
55+
value = SecretFileValue("DefaultTestSecret")
56+
assert os.environ["DJANGO_TEST_SECRET_KEY_FILE"] == FILE_SECRET_PATH
57+
assert value.setup("TEST_SECRET_KEY") == "TestSecretInFile"
58+
59+
60+
def test_secret_default_suffix():
61+
"""Test call with no environment variable and non default `file_suffix`."""
62+
value = SecretFileValue("DefaultTestSecret", file_suffix="PATH")
63+
assert value.setup("TEST_SECRET_KEY") == "DefaultTestSecret"
64+
65+
66+
@pytest.mark.usefixtures("_mock_secret_key_env")
67+
def test_secret_in_env_suffix():
68+
"""Test call with secret key environment variable and non default `file_suffix`."""
69+
value = SecretFileValue("DefaultTestSecret", file_suffix="PATH")
70+
assert os.environ["DJANGO_TEST_SECRET_KEY"] == "TestSecretInEnv"
71+
assert value.setup("TEST_SECRET_KEY") == "TestSecretInEnv"
72+
73+
74+
@pytest.mark.usefixtures("_mock_secret_key_path_env")
75+
def test_secret_in_file_suffix():
76+
"""Test call with secret key file environment variable and non default `file_suffix`."""
77+
value = SecretFileValue("DefaultTestSecret", file_suffix="PATH")
78+
assert os.environ["DJANGO_TEST_SECRET_KEY_PATH"] == FILE_SECRET_PATH
79+
assert value.setup("TEST_SECRET_KEY") == "TestSecretInFile"

0 commit comments

Comments
 (0)