Skip to content

Commit 8aae4af

Browse files
moiseenkovAnton Nitochkin
authored andcommitted
Add config option [secrets]backends_order
1 parent f9ddaa6 commit 8aae4af

File tree

19 files changed

+540
-24
lines changed

19 files changed

+540
-24
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ 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

4646
On the other hand, if a workers secrets backend is defined, the order of lookup has higher priority for the workers secrets
4747
backend and then the secrets backend.
4848

49+
The secrets backends search ordering is also configurable via the configuration option ``[secrets]backends_order``.
50+
4951
.. warning::
5052

5153
When using environment variables or an alternative secrets backend to store secrets or variables, it is possible to create key collisions.
@@ -64,12 +66,21 @@ The ``[secrets]`` section has the following options:
6466
[secrets]
6567
backend =
6668
backend_kwargs =
69+
backends_order =
6770
6871
Set ``backend`` to the fully qualified class name of the backend you want to enable.
6972

7073
You can provide ``backend_kwargs`` with json and it will be passed as kwargs to the ``__init__`` method of
7174
your secrets backend.
7275

76+
``backends_order`` comma-separated list of secret backends. These backends will be used in the order they are specified.
77+
Please note that the ``environment_variable`` and ``metastore`` are required values and cannot be removed
78+
from the list. Supported values are:
79+
80+
* ``custom``: Custom secret backend specified in the ``secrets[backend]`` configuration option.
81+
* ``environment_variable``: Standard environment variable backend ``airflow.secrets.environment_variables.EnvironmentVariablesBackend``.
82+
* ``metastore``: Standard metastore backend ``airflow.secrets.metastore.MetastoreBackend``.
83+
7384
If you want to check which secret backend is currently set, you can use ``airflow config get-value secrets backend`` command as in
7485
the example below.
7586

airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,52 @@ paths:
7777
security:
7878
- OAuth2PasswordBearer: []
7979
- HTTPBearer: []
80+
/ui/backends_order:
81+
get:
82+
tags:
83+
- Config
84+
summary: Get Backends Order Value
85+
operationId: get_backends_order_value
86+
security:
87+
- OAuth2PasswordBearer: []
88+
- HTTPBearer: []
89+
parameters:
90+
- name: accept
91+
in: header
92+
required: false
93+
schema:
94+
type: string
95+
enum:
96+
- application/json
97+
- text/plain
98+
- '*/*'
99+
default: '*/*'
100+
title: Accept
101+
responses:
102+
'200':
103+
description: Successful Response
104+
content:
105+
application/json:
106+
schema:
107+
$ref: '#/components/schemas/Config'
108+
'404':
109+
content:
110+
application/json:
111+
schema:
112+
$ref: '#/components/schemas/HTTPExceptionResponse'
113+
description: Not Found
114+
'406':
115+
content:
116+
application/json:
117+
schema:
118+
$ref: '#/components/schemas/HTTPExceptionResponse'
119+
description: Not Acceptable
120+
'422':
121+
description: Validation Error
122+
content:
123+
application/json:
124+
schema:
125+
$ref: '#/components/schemas/HTTPValidationError'
80126
/ui/connections/hook_meta:
81127
get:
82128
tags:
@@ -1206,6 +1252,41 @@ components:
12061252
- count
12071253
title: CalendarTimeRangeResponse
12081254
description: Represents a summary of DAG runs for a specific calendar time range.
1255+
Config:
1256+
properties:
1257+
sections:
1258+
items:
1259+
$ref: '#/components/schemas/ConfigSection'
1260+
type: array
1261+
title: Sections
1262+
additionalProperties: false
1263+
type: object
1264+
required:
1265+
- sections
1266+
title: Config
1267+
description: List of config sections with their options.
1268+
ConfigOption:
1269+
properties:
1270+
key:
1271+
type: string
1272+
title: Key
1273+
value:
1274+
anyOf:
1275+
- type: string
1276+
- prefixItems:
1277+
- type: string
1278+
- type: string
1279+
type: array
1280+
maxItems: 2
1281+
minItems: 2
1282+
title: Value
1283+
additionalProperties: false
1284+
type: object
1285+
required:
1286+
- key
1287+
- value
1288+
title: ConfigOption
1289+
description: Config option.
12091290
ConfigResponse:
12101291
properties:
12111292
page_size:
@@ -1259,6 +1340,23 @@ components:
12591340
- show_external_log_redirect
12601341
title: ConfigResponse
12611342
description: configuration serializer.
1343+
ConfigSection:
1344+
properties:
1345+
name:
1346+
type: string
1347+
title: Name
1348+
options:
1349+
items:
1350+
$ref: '#/components/schemas/ConfigOption'
1351+
type: array
1352+
title: Options
1353+
additionalProperties: false
1354+
type: object
1355+
required:
1356+
- name
1357+
- options
1358+
title: ConfigSection
1359+
description: Config Section Schema.
12621360
ConnectionHookFieldBehavior:
12631361
properties:
12641362
hidden:

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@
1818

1919
from typing import Any
2020

21-
from fastapi import Depends, status
21+
from fastapi import Depends, HTTPException, status
2222

23+
from airflow.api_fastapi.common.headers import HeaderAcceptJsonOrText
2324
from airflow.api_fastapi.common.router import AirflowRouter
2425
from airflow.api_fastapi.common.types import UIAlert
26+
from airflow.api_fastapi.core_api.datamodels.config import (
27+
Config,
28+
ConfigOption,
29+
ConfigSection,
30+
)
2531
from airflow.api_fastapi.core_api.datamodels.ui.config import ConfigResponse
2632
from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
2733
from airflow.api_fastapi.core_api.security import requires_authenticated
34+
from airflow.api_fastapi.core_api.services.public.config import _response_based_on_accept
2835
from airflow.configuration import conf
2936
from airflow.settings import DASHBOARD_UIALERTS
3037
from airflow.utils.log.log_reader import TaskLogReader
@@ -64,3 +71,32 @@ def get_configs() -> ConfigResponse:
6471
config.update({key: value for key, value in additional_config.items()})
6572

6673
return ConfigResponse.model_validate(config)
74+
75+
76+
@config_router.get(
77+
"/backends_order",
78+
responses={
79+
**create_openapi_http_exception_doc(
80+
[
81+
status.HTTP_404_NOT_FOUND,
82+
status.HTTP_406_NOT_ACCEPTABLE,
83+
]
84+
),
85+
},
86+
response_model=Config,
87+
dependencies=[Depends(requires_authenticated())],
88+
)
89+
def get_backends_order_value(
90+
accept: HeaderAcceptJsonOrText,
91+
):
92+
section, option = "secrets", "backends_order"
93+
if not conf.has_option(section, option):
94+
raise HTTPException(
95+
status_code=status.HTTP_404_NOT_FOUND,
96+
detail=f"Option [{section}/{option}] not found.",
97+
)
98+
99+
value = conf.get(section, option)
100+
101+
config = Config(sections=[ConfigSection(name=section, options=[ConfigOption(key=option, value=value)])])
102+
return _response_based_on_accept(accept, config)

airflow-core/src/airflow/config_templates/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,20 @@ secrets:
13231323
sensitive: true
13241324
example: ~
13251325
default: ""
1326+
backends_order:
1327+
description: |
1328+
Comma-separated list of secret backends. These backends will be used in the order they are specified.
1329+
Please note that the `environment_variable` and `metastore` are required values and cannot be removed
1330+
from the list. Supported values are:
1331+
1332+
* ``custom``: Custom secret backend specified in the ``secrets[backend]`` configuration option.
1333+
* ``environment_variable``: Standard environment variable backend
1334+
``airflow.secrets.environment_variables.EnvironmentVariablesBackend``.
1335+
* ``metastore``: Standard metastore backend ``airflow.secrets.metastore.MetastoreBackend``.
1336+
version_added: 3.0.0
1337+
type: string
1338+
example: ~
1339+
default: "custom,environment_variable,metastore"
13261340
use_cache:
13271341
description: |
13281342
.. note:: |experimental|

airflow-core/src/airflow/configuration.py

Lines changed: 57 additions & 14 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.task.weight_rule import WeightRule
5150
from airflow.utils import yaml
5251
from airflow.utils.module_loading import import_string
@@ -2154,7 +2153,7 @@ def make_group_other_inaccessible(file_path: str):
21542153

21552154

21562155
def ensure_secrets_loaded(
2157-
default_backends: list[str] = DEFAULT_SECRETS_SEARCH_PATH,
2156+
default_backends: list[str] | None = None,
21582157
) -> list[BaseSecretsBackend]:
21592158
"""
21602159
Ensure that all secrets backends are loaded.
@@ -2163,9 +2162,8 @@ def ensure_secrets_loaded(
21632162
"""
21642163
# Check if the secrets_backend_list contains only 2 default backends.
21652164

2166-
# Check if we are loading the backends for worker too by checking if the default_backends is equal
2167-
# to DEFAULT_SECRETS_SEARCH_PATH.
2168-
if len(secrets_backend_list) == 2 or default_backends != DEFAULT_SECRETS_SEARCH_PATH:
2165+
# Check if we are loading the backends for worker too by checking if the default_backends is not None
2166+
if len(secrets_backend_list) == 2 or default_backends is not None:
21692167
return initialize_secrets_backends(default_backends=default_backends)
21702168
return secrets_backend_list
21712169

@@ -2210,28 +2208,73 @@ def get_custom_secret_backend(worker_mode: bool = False) -> BaseSecretsBackend |
22102208
return secrets_backend_cls(**backend_kwargs)
22112209

22122210

2211+
def get_importable_secret_backend(class_name: str | None) -> BaseSecretsBackend | None:
2212+
"""Get secret backend defined in the given class name."""
2213+
if class_name is not None:
2214+
secrets_backend_cls = import_string(class_name)
2215+
return secrets_backend_cls()
2216+
return None
2217+
2218+
22132219
def initialize_secrets_backends(
2214-
default_backends: list[str] = DEFAULT_SECRETS_SEARCH_PATH,
2220+
default_backends: list[str] | None = None,
22152221
) -> list[BaseSecretsBackend]:
22162222
"""
22172223
Initialize secrets backend.
22182224
22192225
* import secrets backend classes
22202226
* instantiate them and return them in a list
22212227
"""
2222-
backend_list = []
22232228
worker_mode = False
2224-
if default_backends != DEFAULT_SECRETS_SEARCH_PATH:
2229+
environment_variable_args: str | None = (
2230+
"airflow.secrets.environment_variables.EnvironmentVariablesBackend"
2231+
)
2232+
metastore_args: str | None = "airflow.secrets.metastore.MetastoreBackend"
2233+
if default_backends is not None:
22252234
worker_mode = True
2235+
environment_variable_args = (
2236+
environment_variable_args if environment_variable_args in default_backends else None
2237+
)
2238+
metastore_args = metastore_args if metastore_args in default_backends else None
2239+
backends_map: dict[str, dict[str, Any]] = {
2240+
"environment_variable": {
2241+
"callback": get_importable_secret_backend,
2242+
"args": (environment_variable_args,),
2243+
},
2244+
"metastore": {
2245+
"callback": get_importable_secret_backend,
2246+
"args": (metastore_args,),
2247+
},
2248+
"custom": {
2249+
"callback": get_custom_secret_backend,
2250+
"args": (worker_mode,),
2251+
},
2252+
}
22262253

2227-
custom_secret_backend = get_custom_secret_backend(worker_mode)
2254+
backends_order = conf.getlist("secrets", "backends_order", delimiter=",")
22282255

2229-
if custom_secret_backend is not None:
2230-
backend_list.append(custom_secret_backend)
2256+
required_backends = ["environment_variable"] if worker_mode else ["metastore", "environment_variable"]
2257+
if missing_backends := [b for b in required_backends if b not in backends_order]:
2258+
raise AirflowConfigException(
2259+
"The configuration option [secrets]backends_order is misconfigured. "
2260+
"The following backend types are missing: %s",
2261+
missing_backends,
2262+
)
22312263

2232-
for class_name in default_backends:
2233-
secrets_backend_cls = import_string(class_name)
2234-
backend_list.append(secrets_backend_cls())
2264+
if unsupported_backends := [b for b in backends_order if b not in backends_map.keys()]:
2265+
raise AirflowConfigException(
2266+
"The configuration option [secrets]backends_order is misconfigured. "
2267+
"The following backend types are unsupported: %s",
2268+
unsupported_backends,
2269+
)
2270+
2271+
backend_list = []
2272+
for backend_type in backends_order:
2273+
backend_item = backends_map[backend_type]
2274+
callback, args = backend_item["callback"], backend_item["args"]
2275+
backend = callback(*args) if args else callback()
2276+
if backend:
2277+
backend_list.append(backend)
22352278

22362279
return backend_list
22372280

airflow-core/src/airflow/secrets/__init__.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,11 @@
2929

3030
from airflow.utils.deprecation_tools import add_deprecated_classes
3131

32-
__all__ = ["BaseSecretsBackend", "DEFAULT_SECRETS_SEARCH_PATH"]
33-
34-
from airflow.secrets.base_secrets import BaseSecretsBackend
35-
36-
DEFAULT_SECRETS_SEARCH_PATH = [
37-
"airflow.secrets.environment_variables.EnvironmentVariablesBackend",
38-
"airflow.secrets.metastore.MetastoreBackend",
32+
__all__ = [
33+
"BaseSecretsBackend",
3934
]
4035

36+
from airflow.secrets.base_secrets import BaseSecretsBackend
4137

4238
__deprecated_classes = {
4339
"cache": {

airflow-core/src/airflow/ui/openapi-gen/queries/common.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ export type ConfigServiceGetConfigsDefaultResponse = Awaited<ReturnType<typeof C
231231
export type ConfigServiceGetConfigsQueryResult<TData = ConfigServiceGetConfigsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
232232
export const useConfigServiceGetConfigsKey = "ConfigServiceGetConfigs";
233233
export const UseConfigServiceGetConfigsKeyFn = (queryKey?: Array<unknown>) => [useConfigServiceGetConfigsKey, ...(queryKey ?? [])];
234+
export type ConfigServiceGetBackendsOrderValueDefaultResponse = Awaited<ReturnType<typeof ConfigService.getBackendsOrderValue>>;
235+
export type ConfigServiceGetBackendsOrderValueQueryResult<TData = ConfigServiceGetBackendsOrderValueDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
236+
export const useConfigServiceGetBackendsOrderValueKey = "ConfigServiceGetBackendsOrderValue";
237+
export const UseConfigServiceGetBackendsOrderValueKeyFn = ({ accept }: {
238+
accept?: "application/json" | "text/plain" | "*/*";
239+
} = {}, queryKey?: Array<unknown>) => [useConfigServiceGetBackendsOrderValueKey, ...(queryKey ?? [{ accept }])];
234240
export type DagWarningServiceListDagWarningsDefaultResponse = Awaited<ReturnType<typeof DagWarningService.listDagWarnings>>;
235241
export type DagWarningServiceListDagWarningsQueryResult<TData = DagWarningServiceListDagWarningsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
236242
export const useDagWarningServiceListDagWarningsKey = "DagWarningServiceListDagWarnings";

airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,16 @@ export const ensureUseConfigServiceGetConfigValueData = (queryClient: QueryClien
431431
*/
432432
export const ensureUseConfigServiceGetConfigsData = (queryClient: QueryClient) => queryClient.ensureQueryData({ queryKey: Common.UseConfigServiceGetConfigsKeyFn(), queryFn: () => ConfigService.getConfigs() });
433433
/**
434+
* Get Backends Order Value
435+
* @param data The data for the request.
436+
* @param data.accept
437+
* @returns Config Successful Response
438+
* @throws ApiError
439+
*/
440+
export const ensureUseConfigServiceGetBackendsOrderValueData = (queryClient: QueryClient, { accept }: {
441+
accept?: "application/json" | "text/plain" | "*/*";
442+
} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseConfigServiceGetBackendsOrderValueKeyFn({ accept }), queryFn: () => ConfigService.getBackendsOrderValue({ accept }) });
443+
/**
434444
* List Dag Warnings
435445
* Get a list of DAG warnings.
436446
* @param data The data for the request.

0 commit comments

Comments
 (0)