Skip to content

Commit 4489808

Browse files
committed
Add config option [secrets]backends_order
1 parent 96d1c81 commit 4489808

File tree

5 files changed

+96
-16
lines changed

5 files changed

+96
-16
lines changed

airflow/config_templates/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,20 @@ secrets:
12961296
sensitive: true
12971297
example: ~
12981298
default: ""
1299+
backends_order:
1300+
description: |
1301+
Comma-separated list of secret backends. These backends will be used in the order they are specified.
1302+
Please note that the `environment_variable` and `metastore` are required values and cannot be removed
1303+
from the list. Supported values are:
1304+
1305+
* ``custom``: Custom secret backend specified in the ``secrets[backend]`` configuration option.
1306+
* ``environment_variable``: Standard environment variable backend
1307+
``airflow.secrets.environment_variables.EnvironmentVariablesBackend``.
1308+
* ``metastore``: Standard metastore backend ``airflow.secrets.metastore.MetastoreBackend``.
1309+
version_added: 3.0.0
1310+
type: string
1311+
example: ~
1312+
default: "custom,environment_variable,metastore"
12991313
use_cache:
13001314
description: |
13011315
.. note:: |experimental|

airflow/configuration.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
from typing_extensions import overload
4747

4848
from airflow.exceptions import AirflowConfigException
49-
from airflow.secrets import DEFAULT_SECRETS_SEARCH_PATH
5049
from airflow.utils import yaml
5150
from airflow.utils.module_loading import import_string
5251
from airflow.utils.providers_configuration_loader import providers_configuration_loaded
@@ -2089,23 +2088,58 @@ def get_custom_secret_backend() -> BaseSecretsBackend | None:
20892088
return secrets_backend_cls(**backend_kwargs)
20902089

20912090

2092-
def initialize_secrets_backends() -> list[BaseSecretsBackend]:
2091+
def get_importable_secret_backend(class_name: str) -> BaseSecretsBackend:
2092+
"""Get secret backend defined in the given class name."""
2093+
secrets_backend_cls = import_string(class_name)
2094+
return secrets_backend_cls()
2095+
2096+
2097+
def initialize_secrets_backends() -> list[BaseSecretsBackend]: # here
20932098
"""
20942099
Initialize secrets backend.
20952100
20962101
* import secrets backend classes
20972102
* instantiate them and return them in a list
20982103
"""
2099-
backend_list = []
2104+
backends_map: dict[str, dict[str, Any]] = {
2105+
"environment_variable": {
2106+
"callback": get_importable_secret_backend,
2107+
"args": ("airflow.secrets.environment_variables.EnvironmentVariablesBackend",),
2108+
},
2109+
"metastore": {
2110+
"callback": get_importable_secret_backend,
2111+
"args": ("airflow.secrets.metastore.MetastoreBackend",),
2112+
},
2113+
"custom": {
2114+
"callback": get_custom_secret_backend,
2115+
"args": None,
2116+
},
2117+
}
21002118

2101-
custom_secret_backend = get_custom_secret_backend()
2119+
backends_order = conf.getlist("secrets", "backends_order", delimiter=",")
21022120

2103-
if custom_secret_backend is not None:
2104-
backend_list.append(custom_secret_backend)
2121+
required_backends = ["metastore", "environment_variable"]
2122+
if missing_backends := [b for b in required_backends if b not in backends_order]:
2123+
raise AirflowConfigException(
2124+
"The configuration option [secrets]backends_order is misconfigured. "
2125+
"The following backend types are missing: %s",
2126+
missing_backends,
2127+
)
2128+
2129+
if unsupported_backends := [b for b in backends_order if b not in backends_map.keys()]:
2130+
raise AirflowConfigException(
2131+
"The configuration option [secrets]backends_order is misconfigured. "
2132+
"The following backend types are unsupported: %s",
2133+
unsupported_backends,
2134+
)
21052135

2106-
for class_name in DEFAULT_SECRETS_SEARCH_PATH:
2107-
secrets_backend_cls = import_string(class_name)
2108-
backend_list.append(secrets_backend_cls())
2136+
backend_list = []
2137+
for backend_type in backends_order:
2138+
backend_item = backends_map[backend_type]
2139+
callback, args = backend_item["callback"], backend_item["args"]
2140+
backend = callback(*args) if args else callback()
2141+
if backend:
2142+
backend_list.append(backend)
21092143

21102144
return backend_list
21112145

airflow/secrets/__init__.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@
2727

2828
from __future__ import annotations
2929

30-
__all__ = ["BaseSecretsBackend", "DEFAULT_SECRETS_SEARCH_PATH"]
30+
__all__ = ["BaseSecretsBackend"]
3131

3232
from airflow.secrets.base_secrets import BaseSecretsBackend
33-
34-
DEFAULT_SECRETS_SEARCH_PATH = [
35-
"airflow.secrets.environment_variables.EnvironmentVariablesBackend",
36-
"airflow.secrets.metastore.MetastoreBackend",
37-
]

docs/apache-airflow/security/secrets/secrets-backend/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ When looking up a connection/variable, by default Airflow will search environmen
3939
database second.
4040

4141
If you enable an alternative secrets backend, it will be searched first, followed by environment variables,
42-
then metastore. This search ordering is not configurable. Though, in some alternative secrets backend you might have
42+
then metastore. Though, in some alternative secrets backend you might have
4343
the option to filter which connection/variable/config is searched in the secret backend. Please look at the
4444
documentation of the secret backend you are using to see if such option is available.
4545

46+
The secrets backends search ordering is also configurable via the configuration option ``[secrets]backends_order``.
47+
4648
.. warning::
4749

4850
When using environment variables or an alternative secrets backend to store secrets or variables, it is possible to create key collisions.

tests/always/test_secrets.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pytest
2323

2424
from airflow.configuration import ensure_secrets_loaded, initialize_secrets_backends
25+
from airflow.exceptions import AirflowConfigException
2526
from airflow.models import Connection, Variable
2627
from airflow.secrets.cache import SecretCache
2728

@@ -117,6 +118,40 @@ def test_backend_fallback_to_env_var(self, mock_get_connection):
117118

118119
assert conn.get_uri() == "mysql://airflow:airflow@host:5432/airflow"
119120

121+
@conf_vars(
122+
{
123+
(
124+
"secrets",
125+
"backend",
126+
): "airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend",
127+
("secrets", "backend_kwargs"): '{"connections_prefix": "/airflow", "profile_name": null}',
128+
("secrets", "backends_order"): "custom,environment_variable,metastore",
129+
}
130+
)
131+
def test_backends_order(self):
132+
backends = ensure_secrets_loaded()
133+
backend_classes = [backend.__class__.__name__ for backend in backends]
134+
assert backend_classes == [
135+
"SystemsManagerParameterStoreBackend",
136+
"EnvironmentVariablesBackend",
137+
"MetastoreBackend",
138+
]
139+
140+
@conf_vars({("secrets", "backends_order"): "custom,metastore"})
141+
def test_backends_order_no_environment_variable_backend(self):
142+
with pytest.raises(AirflowConfigException):
143+
ensure_secrets_loaded()
144+
145+
@conf_vars({("secrets", "backends_order"): "environment_variable"})
146+
def test_backends_order_no_metastore_backend(self):
147+
with pytest.raises(AirflowConfigException):
148+
ensure_secrets_loaded()
149+
150+
@conf_vars({("secrets", "backends_order"): "metastore,environment_variable,unsupported"})
151+
def test_backends_order_unsupported(self):
152+
with pytest.raises(AirflowConfigException):
153+
ensure_secrets_loaded()
154+
120155

121156
@pytest.mark.db_test
122157
class TestVariableFromSecrets:

0 commit comments

Comments
 (0)