Skip to content

Commit 62e5d1b

Browse files
committed
feat(jinja): add b64decode/b64encode builtin filters
Core SQLMesh shipped no builtin Jinja filters, so base64-encoded secrets (e.g. a BigQuery service-account key stored in an env var) could not be decoded directly in config YAML. Add b64decode/b64encode and register them on the shared environment factory so they are available both in config YAML and in models, mirroring ansible's filters. Closes #5754 Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 991a327 commit 62e5d1b

3 files changed

Lines changed: 66 additions & 2 deletions

File tree

docs/guides/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ The examples specify a Snowflake connection whose password is stored in an envir
170170
account: <account>
171171
```
172172

173+
!!! tip "Base64-encoded secrets"
174+
175+
If a secret is distributed base64-encoded in a single environment variable (for example a BigQuery service-account key), pipe the variable through the built-in `b64decode` filter to decode it to text inline:
176+
177+
```yaml
178+
keyfile_json: {{ env_var('BIGQUERY_KEY_B64') | b64decode }}
179+
```
180+
181+
A matching `b64encode` filter is also available. Both return UTF-8 text, so they are intended for string/JSON secrets rather than arbitrary binary data.
182+
173183
=== "Python"
174184

175185
Python accesses environment variables via the `os` library's `environ` dictionary.

sqlmesh/utils/jinja.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import base64
34
import importlib
45
import json
56
import re
@@ -28,11 +29,40 @@
2829
SQLMESH_JINJA_PACKAGE = "sqlmesh.utils.jinja"
2930

3031

32+
def b64decode(value: t.Union[str, bytes]) -> str:
33+
"""Decode a base64-encoded value and return it as UTF-8 text.
34+
35+
Intended for base64-encoded string/JSON secrets (for example a service-account
36+
key stored in an environment variable), not arbitrary binary payloads.
37+
"""
38+
decoded = value.encode("utf-8") if isinstance(value, str) else value
39+
return base64.b64decode(decoded).decode("utf-8")
40+
41+
42+
def b64encode(value: t.Union[str, bytes]) -> str:
43+
"""Base64-encode a value and return the encoding as UTF-8 text.
44+
45+
The input is treated as UTF-8 text, mirroring ``b64decode``; it is intended for
46+
string/JSON secrets rather than arbitrary binary payloads.
47+
"""
48+
encoded = value.encode("utf-8") if isinstance(value, str) else value
49+
return base64.b64encode(encoded).decode("utf-8")
50+
51+
52+
def create_builtin_filters() -> t.Dict[str, t.Callable]:
53+
return {
54+
"b64decode": b64decode,
55+
"b64encode": b64encode,
56+
}
57+
58+
3159
def environment(**kwargs: t.Any) -> Environment:
3260
extensions = kwargs.pop("extensions", [])
3361
extensions.append("jinja2.ext.do")
3462
extensions.append("jinja2.ext.loopcontrols")
35-
return Environment(extensions=extensions, **kwargs)
63+
env = Environment(extensions=extensions, **kwargs)
64+
env.filters.update(create_builtin_filters())
65+
return env
3666

3767

3868
ENVIRONMENT = environment()

tests/utils/test_jinja.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

3-
from sqlmesh.utils import AttributeDict
3+
from base64 import b64encode
4+
5+
from sqlmesh.utils import AttributeDict, yaml
46
from sqlmesh.utils.jinja import (
57
ENVIRONMENT,
68
JinjaMacroRegistry,
@@ -329,3 +331,25 @@ def test_macro_registry_to_expressions_sorted():
329331
== "refs = {'orders': {'database': 'jaffle_shop', 'nested_list': ['a', 'b', 'c'], 'schema': 'main'}, 'payments': {'database': 'jaffle_shop', 'nested': {'baz': 'bing', 'foo': 'bar'}, 'schema': 'main'}}\n"
330332
"sources = {}"
331333
)
334+
335+
336+
def test_builtin_base64_filters():
337+
encoded = b64encode(b"secret").decode("utf-8")
338+
339+
env = JinjaMacroRegistry().build_environment()
340+
assert env.from_string("{{ value | b64decode }}").render(value=encoded) == "secret"
341+
assert env.from_string("{{ 'secret' | b64encode }}").render() == encoded
342+
assert env.from_string("{{ 'secret' | b64encode | b64decode }}").render() == "secret"
343+
344+
# The same filters are available when rendering Jinja in config YAML files.
345+
config = yaml.load(f'env_vars:\n TOKEN: "{{{{ "{encoded}" | b64decode }}}}"')
346+
assert config == {"env_vars": {"TOKEN": "secret"}}
347+
348+
349+
def test_builtin_b64decode_with_env_var(monkeypatch):
350+
# Real-world use case: a base64-encoded secret stored in an environment variable
351+
# is decoded inline in config YAML via env_var(...) piped through b64decode.
352+
monkeypatch.setenv("SNOWFLAKE_PW_B64", b64encode(b"super-secret-pw").decode("utf-8"))
353+
354+
config = yaml.load("password: \"{{ env_var('SNOWFLAKE_PW_B64') | b64decode }}\"")
355+
assert config == {"password": "super-secret-pw"}

0 commit comments

Comments
 (0)