Skip to content

Commit 8d980cb

Browse files
authored
Implement multiple API auth backends (#21472)
* Implement API auth through session * Expand API auth to multiple backends As part of AIP-42, the auth_backend setting is expanded to auth_backends, and on an API request each is tried one after the other until one succeeds. A new auth backend of session is added that will validate against the signed-in user in the case where requests are made via JavaScript from the UI.
1 parent 5fdd6fa commit 8d980cb

File tree

27 files changed

+224
-66
lines changed

27 files changed

+224
-66
lines changed

UPDATING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ A new `log_template` table is introduced to solve this problem. This table is sy
134134

135135
Previously, there was an empty class `airflow.models.base.Operator` for “type hinting”. This class was never really useful for anything (everything it did could be done better with `airflow.models.baseoperator.BaseOperator`), and has been removed. If you are relying on the class’s existence, use `BaseOperator` (for concrete operators), `airflow.models.abstractoperator.AbstractOperator` (the base class of both `BaseOperator` and the AIP-42 `MappedOperator`), or `airflow.models.operator.Operator` (a union type `BaseOperator | MappedOperator` for type annotation).
136136

137+
### `auth_backends` replaces `auth_backend` configuration setting
138+
139+
Previously, only one backend was used to authorize use of the REST API. In 2.3 this was changed to support multiple backends, separated by whitespace. Each will be tried in turn until a successful response is returned.
140+
141+
This setting is also used for the deprecated experimental API, which only uses
142+
the first option even if multiple are given.
143+
137144
## Airflow 2.2.3
138145

139146
No breaking changes.

airflow/api/__init__.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,20 @@
2626

2727

2828
def load_auth():
29-
"""Loads authentication backend"""
30-
auth_backend = 'airflow.api.auth.backend.default'
29+
"""Loads authentication backends"""
30+
auth_backends = 'airflow.api.auth.backend.default'
3131
try:
32-
auth_backend = conf.get("api", "auth_backend")
32+
auth_backends = conf.get("api", "auth_backends")
3333
except AirflowConfigException:
3434
pass
3535

36-
try:
37-
auth_backend = import_module(auth_backend)
38-
log.info("Loaded API auth backend: %s", auth_backend)
39-
return auth_backend
40-
except ImportError as err:
41-
log.critical("Cannot import %s for API authentication due to: %s", auth_backend, err)
42-
raise AirflowException(err)
36+
backends = []
37+
for backend in auth_backends.split():
38+
try:
39+
auth = import_module(backend)
40+
log.info("Loaded API auth backend: %s", backend)
41+
backends.append(auth)
42+
except ImportError as err:
43+
log.critical("Cannot import %s for API authentication due to: %s", backend, err)
44+
raise AirflowException(err)
45+
return backends
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
"""Session authentication backend"""
18+
from functools import wraps
19+
from typing import Any, Callable, Optional, Tuple, TypeVar, Union, cast
20+
21+
from flask import Response, g
22+
23+
CLIENT_AUTH: Optional[Union[Tuple[str, str], Any]] = None
24+
25+
26+
def init_app(_):
27+
"""Initializes authentication backend"""
28+
29+
30+
T = TypeVar("T", bound=Callable)
31+
32+
33+
def requires_authentication(function: T):
34+
"""Decorator for functions that require authentication"""
35+
36+
@wraps(function)
37+
def decorated(*args, **kwargs):
38+
if g.user.is_anonymous:
39+
return Response("Unauthorized", 401, {})
40+
return function(*args, **kwargs)
41+
42+
return cast(T, decorated)

airflow/api/client/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
def get_current_api_client() -> Client:
2828
"""Return current API Client based on current Airflow configuration"""
2929
api_module = import_module(conf.get('cli', 'api_client')) # type: Any
30-
auth_backend = api.load_auth()
30+
auth_backends = api.load_auth()
3131
session = None
32-
session_factory = getattr(auth_backend, 'create_client_session', None)
33-
if session_factory:
34-
session = session_factory()
35-
api_client = api_module.Client(
36-
api_base_url=conf.get('cli', 'endpoint_url'),
37-
auth=getattr(auth_backend, 'CLIENT_AUTH', None),
38-
session=session,
39-
)
32+
for backend in auth_backends:
33+
session_factory = getattr(backend, 'create_client_session', None)
34+
if session_factory:
35+
session = session_factory()
36+
api_client = api_module.Client(
37+
api_base_url=conf.get('cli', 'endpoint_url'),
38+
auth=getattr(backend, 'CLIENT_AUTH', None),
39+
session=session,
40+
)
4041
return api_client

airflow/api_connexion/openapi/v1.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ info:
179179
and it is even possible to add your own method.
180180
181181
If you want to check which auth backend is currently set, you can use
182-
`airflow config get-value api auth_backend` command as in the example below.
182+
`airflow config get-value api auth_backends` command as in the example below.
183183
```bash
184-
$ airflow config get-value api auth_backend
184+
$ airflow config get-value api auth_backends
185185
airflow.api.auth.backend.basic_auth
186186
```
187187
The default is to deny all requests.

airflow/api_connexion/security.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727

2828
def check_authentication() -> None:
2929
"""Checks that the request has valid authorization information."""
30-
response = current_app.api_auth.requires_authentication(Response)()
31-
if response.status_code != 200:
32-
# since this handler only checks authentication, not authorization,
33-
# we should always return 401
34-
raise Unauthenticated(headers=response.headers)
30+
for auth in current_app.api_auth:
31+
response = auth.requires_authentication(Response)()
32+
if response.status_code == 200:
33+
return
34+
# since this handler only checks authentication, not authorization,
35+
# we should always return 401
36+
raise Unauthenticated(headers=response.headers)
3537

3638

3739
def requires_access(permissions: Optional[Sequence[Tuple[str, str]]] = None) -> Callable[[T], T]:

airflow/config_templates/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@
793793
type: boolean
794794
example: ~
795795
default: "False"
796-
- name: auth_backend
796+
- name: auth_backends
797797
description: |
798798
How to authenticate users of the API. See
799799
https://airflow.apache.org/docs/apache-airflow/stable/security.html for possible values.

airflow/config_templates/default_airflow.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ enable_experimental_api = False
433433
# How to authenticate users of the API. See
434434
# https://airflow.apache.org/docs/apache-airflow/stable/security.html for possible values.
435435
# ("airflow.api.auth.backend.default" allows all requests for historic reasons)
436-
auth_backend = airflow.api.auth.backend.deny_all
436+
auth_backends = airflow.api.auth.backend.deny_all
437437

438438
# Used to set the maximum page limit for API requests
439439
maximum_page_limit = 100

airflow/config_templates/default_test.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ api_client = airflow.api.client.local_client
6161
endpoint_url = http://localhost:8080
6262

6363
[api]
64-
auth_backend = airflow.api.auth.backend.default
64+
auth_backends = airflow.api.auth.backend.default
6565

6666
[operators]
6767
default_owner = airflow
6868

69+
6970
[hive]
7071
default_hive_mapred_queue = airflow
7172

airflow/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ class AirflowConfigParser(ConfigParser):
185185
('core', 'max_active_tasks_per_dag'): ('core', 'dag_concurrency', '2.2.0'),
186186
('logging', 'worker_log_server_port'): ('celery', 'worker_log_server_port', '2.2.0'),
187187
('api', 'access_control_allow_origins'): ('api', 'access_control_allow_origin', '2.2.0'),
188+
('api', 'auth_backends'): ('api', 'auth_backend', '2.3'),
188189
}
189190

190191
# A mapping of old default values that we want to change and warn the user

0 commit comments

Comments
 (0)