Closes #22212: Support for exposing environment parameters in Jinja template context (#22289)

This commit is contained in:
Jeremy Stretch 2026-05-26 14:47:04 -04:00 committed by GitHub
parent 88eac5b37d
commit 4d8dbc6ffe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 2 deletions

View File

@ -145,6 +145,24 @@ Set this configuration parameter to `True` for NetBox deployments which do not h
---
## JINJA_ENVIRONMENT_PARAMS
Default: `[]`
A list of system environment variable names which may be referenced from within Jinja2 templates via the built-in [`env`](#jinja2_filters) filter. Patterns may include wildcards (matched using Python's `fnmatch` syntax). Any variable whose name does not match an entry in this list cannot be referenced from a template. For example:
```python
JINJA_ENVIRONMENT_PARAMS = [
'WEBHOOK_TOKEN_*',
'DEFAULT_SECRET_ID',
]
```
!!! info "Parameter names are case-sensitive"
For example, `FOO_*` will match `FOO_BAR` but `foo_*` will not.
---
## JINJA2_FILTERS
Default: `{}`
@ -160,6 +178,18 @@ JINJA2_FILTERS = {
}
```
NetBox also registers the following filters by default. Any entry defined in `JINJA2_FILTERS` with the same name will override the default.
| Filter | Description |
|---|---|
| `env` | Returns the value of the system environment variable with the given name, provided its name matches an entry in [`JINJA_ENVIRONMENT_PARAMS`](#jinja_environment_params). Returns `None` if the variable is not defined or its name is not whitelisted. |
For example, given `JINJA_ENVIRONMENT_PARAMS = ['WEBHOOK_TOKEN_*']`, a Jinja2 template may reference an environment variable as:
```
Authorization: Bearer {{ 'WEBHOOK_TOKEN_3' | env }}
```
---
## LOGGING

View File

@ -31,7 +31,7 @@ from extras.models import (
from extras.models.mixins import RenderTemplateMixin
from tenancy.models import Tenant, TenantGroup
from utilities.exceptions import AbortRequest
from utilities.jinja2 import render_jinja2
from utilities.jinja2 import env_filter, render_jinja2
from utilities.tables import get_table_for_model
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -973,6 +973,48 @@ class ConfigTemplateDebugTestCase(TestCase):
render_jinja2("{% debug %}", {}, debug=False)
class JinjaEnvFilterTestCase(TestCase):
"""
Tests for the env() Jinja2 filter and the JINJA_ENVIRONMENT_PARAMS configuration parameter.
"""
def test_env_filter_returns_value_for_matching_name(self):
with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \
self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']):
self.assertEqual(env_filter('NETBOX_TEST_TOKEN'), 'secret')
def test_env_filter_returns_none_for_unmatched_name(self):
with patch.dict('os.environ', {'NETBOX_OTHER_TOKEN': 'secret'}, clear=False), \
self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']):
self.assertIsNone(env_filter('NETBOX_OTHER_TOKEN'))
def test_env_filter_wildcard_match(self):
with patch.dict('os.environ', {'NETBOX_TEST_TOKEN_1': 'one', 'NETBOX_TEST_TOKEN_2': 'two'}, clear=False), \
self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN_*']):
self.assertEqual(env_filter('NETBOX_TEST_TOKEN_1'), 'one')
self.assertEqual(env_filter('NETBOX_TEST_TOKEN_2'), 'two')
def test_env_filter_returns_none_for_missing_env_var(self):
with self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_MISSING_VAR']):
self.assertIsNone(env_filter('NETBOX_MISSING_VAR'))
def test_env_filter_empty_whitelist_returns_none(self):
with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \
self.settings(JINJA_ENVIRONMENT_PARAMS=[]):
self.assertIsNone(env_filter('NETBOX_TEST_TOKEN'))
def test_env_filter_registered_by_default(self):
with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \
self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']):
output = render_jinja2("{{ 'NETBOX_TEST_TOKEN' | env }}", {})
self.assertEqual(output, 'secret')
def test_user_defined_filter_overrides_default(self):
with self.settings(JINJA2_FILTERS={'env': lambda name: 'overridden'}):
output = render_jinja2("{{ 'NETBOX_TEST_TOKEN' | env }}", {})
self.assertEqual(output, 'overridden')
class ExportTemplateContextTestCase(TestCase):
"""
Tests for ExportTemplate.get_context() including public model population.

View File

@ -140,6 +140,7 @@ HTTP_CLIENT_IP_HEADERS = getattr(configuration, 'HTTP_CLIENT_IP_HEADERS', (
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)
JINJA_ENVIRONMENT_PARAMS = getattr(configuration, 'JINJA_ENVIRONMENT_PARAMS', [])
JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us')
LANGUAGE_COOKIE_PATH = CSRF_COOKIE_PATH

View File

@ -1,3 +1,6 @@
import fnmatch
import os
from django.apps import apps
from jinja2 import BaseLoader, TemplateNotFound
from jinja2.meta import find_referenced_templates
@ -6,11 +9,30 @@ from jinja2.sandbox import SandboxedEnvironment
from netbox.config import get_config
__all__ = (
'DEFAULT_JINJA2_FILTERS',
'DataFileLoader',
'env_filter',
'render_jinja2',
)
def env_filter(name):
"""
Jinja2 filter which returns the value of an environment variable, provided its name matches one of the patterns
listed in the JINJA_ENVIRONMENT_PARAMS configuration parameter. Patterns may include wildcards. Returns None if the
variable is not defined or its name does not match an allowed pattern.
"""
patterns = get_config().JINJA_ENVIRONMENT_PARAMS or []
if not any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns):
return None
return os.environ.get(name)
DEFAULT_JINJA2_FILTERS = {
'env': env_filter,
}
class DataFileLoader(BaseLoader):
"""
Custom Jinja2 loader to facilitate populating template content from DataFiles.
@ -74,7 +96,11 @@ def render_jinja2(template_code, context, environment_params=None, data_file=Non
environment_params['loader'] = loader
environment = SandboxedEnvironment(**environment_params)
environment.filters.update(get_config().JINJA2_FILTERS)
# Register default filters, then apply any user-defined filters. User-defined entries take precedence so that
# existing JINJA2_FILTERS configurations are never overridden.
filters = {**DEFAULT_JINJA2_FILTERS, **get_config().JINJA2_FILTERS}
environment.filters.update(filters)
if data_file:
template = environment.get_template(data_file.path)