apache-airflow-providers-fab 2.0.0rc1__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airflow/providers/fab/LICENSE +0 -52
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -4
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -14
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -13
- airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +153 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +17 -4
- airflow/providers/fab/auth_manager/fab_auth_manager.py +222 -119
- airflow/providers/fab/auth_manager/models/__init__.py +1 -1
- airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
- airflow/providers/fab/auth_manager/models/db.py +22 -5
- airflow/providers/fab/auth_manager/openapi/v1.yaml +9 -0
- airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +89 -561
- airflow/providers/fab/auth_manager/views/permissions.py +1 -1
- airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
- airflow/providers/fab/auth_manager/views/user.py +1 -1
- airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
- airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
- airflow/providers/fab/get_provider_info.py +26 -15
- airflow/providers/fab/www/airflow_flask_app.py +31 -0
- airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
- airflow/providers/fab/www/api_connexion/parameters.py +131 -0
- airflow/providers/fab/www/api_connexion/security.py +84 -0
- airflow/providers/fab/www/api_connexion/types.py +30 -0
- airflow/providers/fab/www/app.py +34 -9
- airflow/providers/fab/www/auth.py +350 -0
- airflow/providers/fab/www/constants.py +28 -0
- airflow/providers/fab/www/extensions/init_appbuilder.py +54 -9
- airflow/providers/fab/www/extensions/init_jinja_globals.py +5 -3
- airflow/providers/fab/www/extensions/init_security.py +19 -0
- airflow/providers/fab/www/extensions/init_session.py +64 -0
- airflow/providers/fab/www/extensions/init_views.py +111 -1
- airflow/providers/fab/www/package-lock.json +4967 -16517
- airflow/providers/fab/www/package.json +25 -104
- airflow/providers/fab/www/security/__init__.py +17 -0
- airflow/providers/fab/www/security/permissions.py +126 -0
- airflow/providers/fab/www/security_appless.py +44 -0
- airflow/providers/fab/www/security_manager.py +122 -0
- airflow/providers/fab/www/session.py +41 -0
- airflow/providers/fab/www/static/css/flash.css +57 -0
- airflow/providers/fab/www/static/dist/48f0ea180c40270a5b05.png +1 -0
- airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
- airflow/providers/fab/www/static/dist/f7490d556a6c42e49ba4.png +1 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.css +18 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
- airflow/providers/fab/www/static/dist/manifest.json +20 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
- airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js +2 -0
- airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
- airflow/providers/fab/www/static/dist/oss-licenses.json +20 -0
- airflow/providers/fab/www/templates/airflow/main.html +10 -11
- airflow/providers/fab/www/templates/airflow/traceback.html +1 -5
- airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
- airflow/providers/fab/www/templates/appbuilder/navbar.html +7 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
- airflow/providers/fab/www/utils.py +272 -0
- airflow/providers/fab/www/views.py +129 -0
- airflow/providers/fab/www/webpack.config.js +5 -40
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/METADATA +24 -34
- apache_airflow_providers_fab-2.0.0rc2.dist-info/RECORD +125 -0
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/WHEEL +1 -1
- airflow/providers/fab/auth_manager/decorators/auth.py +0 -127
- apache_airflow_providers_fab-2.0.0rc1.dist-info/RECORD +0 -78
- /airflow/providers/fab/{auth_manager/decorators → www/api_connexion}/__init__.py +0 -0
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/entry_points.txt +0 -0
airflow/providers/fab/www/app.py
CHANGED
@@ -25,14 +25,21 @@ from flask_wtf.csrf import CSRFProtect
|
|
25
25
|
from sqlalchemy.engine.url import make_url
|
26
26
|
|
27
27
|
from airflow import settings
|
28
|
+
from airflow.api_fastapi.app import get_auth_manager
|
28
29
|
from airflow.configuration import conf
|
29
30
|
from airflow.exceptions import AirflowConfigException
|
30
31
|
from airflow.logging_config import configure_logging
|
31
32
|
from airflow.providers.fab.www.extensions.init_appbuilder import init_appbuilder
|
32
33
|
from airflow.providers.fab.www.extensions.init_jinja_globals import init_jinja_globals
|
33
34
|
from airflow.providers.fab.www.extensions.init_manifest_files import configure_manifest_files
|
34
|
-
from airflow.providers.fab.www.extensions.init_security import init_xframe_protection
|
35
|
-
from airflow.providers.fab.www.extensions.
|
35
|
+
from airflow.providers.fab.www.extensions.init_security import init_api_auth, init_xframe_protection
|
36
|
+
from airflow.providers.fab.www.extensions.init_session import init_airflow_session_interface
|
37
|
+
from airflow.providers.fab.www.extensions.init_views import (
|
38
|
+
init_api_auth_provider,
|
39
|
+
init_api_error_handlers,
|
40
|
+
init_error_handlers,
|
41
|
+
init_plugins,
|
42
|
+
)
|
36
43
|
|
37
44
|
app: Flask | None = None
|
38
45
|
|
@@ -41,43 +48,61 @@ app: Flask | None = None
|
|
41
48
|
csrf = CSRFProtect()
|
42
49
|
|
43
50
|
|
44
|
-
def create_app(
|
51
|
+
def create_app(enable_plugins: bool):
|
45
52
|
"""Create a new instance of Airflow WWW app."""
|
53
|
+
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
|
54
|
+
|
46
55
|
flask_app = Flask(__name__)
|
47
56
|
flask_app.secret_key = conf.get("webserver", "SECRET_KEY")
|
48
57
|
flask_app.config["SQLALCHEMY_DATABASE_URI"] = conf.get("database", "SQL_ALCHEMY_CONN")
|
58
|
+
flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
49
59
|
|
50
60
|
url = make_url(flask_app.config["SQLALCHEMY_DATABASE_URI"])
|
51
61
|
if url.drivername == "sqlite" and url.database and not isabs(url.database):
|
52
62
|
raise AirflowConfigException(
|
53
|
-
f
|
63
|
+
f"Cannot use relative path: `{conf.get('database', 'SQL_ALCHEMY_CONN')}` to connect to sqlite. "
|
54
64
|
"Please use absolute path such as `sqlite:////tmp/airflow.db`."
|
55
65
|
)
|
56
66
|
|
57
67
|
if "SQLALCHEMY_ENGINE_OPTIONS" not in flask_app.config:
|
58
68
|
flask_app.config["SQLALCHEMY_ENGINE_OPTIONS"] = settings.prepare_engine_args()
|
59
69
|
|
70
|
+
csrf.init_app(flask_app)
|
71
|
+
|
60
72
|
db = SQLA()
|
61
73
|
db.session = settings.Session
|
62
74
|
db.init_app(flask_app)
|
63
75
|
|
64
76
|
configure_logging()
|
65
77
|
configure_manifest_files(flask_app)
|
78
|
+
init_api_auth(flask_app)
|
66
79
|
|
67
80
|
with flask_app.app_context():
|
68
|
-
init_appbuilder(flask_app)
|
69
|
-
init_plugins(flask_app)
|
81
|
+
init_appbuilder(flask_app, enable_plugins=enable_plugins)
|
70
82
|
init_error_handlers(flask_app)
|
71
|
-
|
83
|
+
# In two scenarios a Flask application can be created:
|
84
|
+
# - To support Airflow 2 plugins relying on Flask (``enable_plugins`` is True)
|
85
|
+
# - To support FAB auth manager (``enable_plugins`` is False)
|
86
|
+
# There are some edge cases where ``enable_plugins`` is False but the auth manager configured is not
|
87
|
+
# FAB auth manager. One example is ``run_update_fastapi_api_spec``, it calls
|
88
|
+
# ``FabAuthManager().get_fastapi_app()`` to generate the openapi documentation regardless of the
|
89
|
+
# configured auth manager.
|
90
|
+
if enable_plugins:
|
91
|
+
init_plugins(flask_app)
|
92
|
+
elif isinstance(get_auth_manager(), FabAuthManager):
|
93
|
+
init_api_auth_provider(flask_app)
|
94
|
+
init_api_error_handlers(flask_app)
|
95
|
+
init_jinja_globals(flask_app, enable_plugins=enable_plugins)
|
72
96
|
init_xframe_protection(flask_app)
|
97
|
+
init_airflow_session_interface(flask_app)
|
73
98
|
return flask_app
|
74
99
|
|
75
100
|
|
76
|
-
def cached_app(
|
101
|
+
def cached_app():
|
77
102
|
"""Return cached instance of Airflow WWW app."""
|
78
103
|
global app
|
79
104
|
if not app:
|
80
|
-
app = create_app(
|
105
|
+
app = create_app()
|
81
106
|
return app
|
82
107
|
|
83
108
|
|
@@ -0,0 +1,350 @@
|
|
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
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
import functools
|
20
|
+
import logging
|
21
|
+
from collections.abc import Sequence
|
22
|
+
from functools import wraps
|
23
|
+
from typing import TYPE_CHECKING, Callable, TypeVar, cast
|
24
|
+
|
25
|
+
from flask import flash, redirect, render_template, request, url_for
|
26
|
+
from flask_appbuilder._compat import as_unicode
|
27
|
+
from flask_appbuilder.const import (
|
28
|
+
FLAMSG_ERR_SEC_ACCESS_DENIED,
|
29
|
+
LOGMSG_ERR_SEC_ACCESS_DENIED,
|
30
|
+
PERMISSION_PREFIX,
|
31
|
+
)
|
32
|
+
|
33
|
+
from airflow.api_fastapi.app import get_auth_manager
|
34
|
+
from airflow.api_fastapi.auth.managers.models.resource_details import (
|
35
|
+
AccessView,
|
36
|
+
ConnectionDetails,
|
37
|
+
DagAccessEntity,
|
38
|
+
DagDetails,
|
39
|
+
PoolDetails,
|
40
|
+
VariableDetails,
|
41
|
+
)
|
42
|
+
from airflow.configuration import conf
|
43
|
+
from airflow.providers.fab.www.utils import get_fab_auth_manager
|
44
|
+
from airflow.utils.net import get_hostname
|
45
|
+
|
46
|
+
if TYPE_CHECKING:
|
47
|
+
from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
|
48
|
+
from airflow.api_fastapi.auth.managers.models.batch_apis import (
|
49
|
+
IsAuthorizedConnectionRequest,
|
50
|
+
IsAuthorizedDagRequest,
|
51
|
+
IsAuthorizedPoolRequest,
|
52
|
+
IsAuthorizedVariableRequest,
|
53
|
+
)
|
54
|
+
from airflow.models import DagRun, Pool, TaskInstance, Variable
|
55
|
+
from airflow.models.connection import Connection
|
56
|
+
from airflow.models.xcom import XComModel
|
57
|
+
|
58
|
+
T = TypeVar("T", bound=Callable)
|
59
|
+
|
60
|
+
log = logging.getLogger(__name__)
|
61
|
+
|
62
|
+
|
63
|
+
def get_access_denied_message():
|
64
|
+
return conf.get("webserver", "access_denied_message")
|
65
|
+
|
66
|
+
|
67
|
+
def has_access_with_pk(f):
|
68
|
+
"""
|
69
|
+
Check permissions on views.
|
70
|
+
|
71
|
+
The implementation is very similar from
|
72
|
+
https://github.com/dpgaspar/Flask-AppBuilder/blob/c6fecdc551629e15467fde5d06b4437379d90592/flask_appbuilder/security/decorators.py#L134
|
73
|
+
|
74
|
+
The difference is that this decorator will pass the resource ID to check permissions. It allows
|
75
|
+
fined-grained access control using resource IDs.
|
76
|
+
"""
|
77
|
+
if hasattr(f, "_permission_name"):
|
78
|
+
permission_str = f._permission_name
|
79
|
+
else:
|
80
|
+
permission_str = f.__name__
|
81
|
+
|
82
|
+
def wraps(self, *args, **kwargs):
|
83
|
+
permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
|
84
|
+
if self.method_permission_name:
|
85
|
+
_permission_name = self.method_permission_name.get(f.__name__)
|
86
|
+
if _permission_name:
|
87
|
+
permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
|
88
|
+
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
|
89
|
+
action_name=permission_str,
|
90
|
+
resource_name=self.class_permission_name,
|
91
|
+
resource_pk=kwargs.get("pk"),
|
92
|
+
):
|
93
|
+
return f(self, *args, **kwargs)
|
94
|
+
else:
|
95
|
+
log.warning(LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__)
|
96
|
+
flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), "danger")
|
97
|
+
return redirect(get_auth_manager().get_url_login(next_url=request.url))
|
98
|
+
|
99
|
+
f._permission_name = permission_str
|
100
|
+
return functools.update_wrapper(wraps, f)
|
101
|
+
|
102
|
+
|
103
|
+
def _has_access_no_details(is_authorized_callback: Callable[[], bool]) -> Callable[[T], T]:
|
104
|
+
"""
|
105
|
+
Check current user's permissions against required permissions.
|
106
|
+
|
107
|
+
This works only for resources with no details. This function is used in some ``has_access_`` functions
|
108
|
+
below.
|
109
|
+
|
110
|
+
:param is_authorized_callback: callback to execute to figure whether the user is authorized to access
|
111
|
+
the resource?
|
112
|
+
"""
|
113
|
+
|
114
|
+
def has_access_decorator(func: T):
|
115
|
+
@wraps(func)
|
116
|
+
def decorated(*args, **kwargs):
|
117
|
+
return _has_access(
|
118
|
+
is_authorized=is_authorized_callback(),
|
119
|
+
func=func,
|
120
|
+
args=args,
|
121
|
+
kwargs=kwargs,
|
122
|
+
)
|
123
|
+
|
124
|
+
return cast("T", decorated)
|
125
|
+
|
126
|
+
return has_access_decorator
|
127
|
+
|
128
|
+
|
129
|
+
def _has_access(*, is_authorized: bool, func: Callable, args, kwargs):
|
130
|
+
"""
|
131
|
+
Define the behavior whether the user is authorized to access the resource.
|
132
|
+
|
133
|
+
:param is_authorized: whether the user is authorized to access the resource
|
134
|
+
:param func: the function to call if the user is authorized
|
135
|
+
:param args: the arguments of ``func``
|
136
|
+
:param kwargs: the keyword arguments ``func``
|
137
|
+
|
138
|
+
:meta private:
|
139
|
+
"""
|
140
|
+
if is_authorized:
|
141
|
+
return func(*args, **kwargs)
|
142
|
+
elif get_fab_auth_manager().is_logged_in() and not get_auth_manager().is_authorized_view(
|
143
|
+
access_view=AccessView.WEBSITE,
|
144
|
+
user=get_fab_auth_manager().get_user(),
|
145
|
+
):
|
146
|
+
return (
|
147
|
+
render_template(
|
148
|
+
"airflow/no_roles_permissions.html",
|
149
|
+
hostname=get_hostname() if conf.getboolean("webserver", "EXPOSE_HOSTNAME") else "",
|
150
|
+
logout_url=get_fab_auth_manager().get_url_logout(),
|
151
|
+
),
|
152
|
+
403,
|
153
|
+
)
|
154
|
+
elif not get_fab_auth_manager().is_logged_in():
|
155
|
+
return redirect(get_auth_manager().get_url_login(next_url=request.url))
|
156
|
+
else:
|
157
|
+
access_denied = get_access_denied_message()
|
158
|
+
flash(access_denied, "danger")
|
159
|
+
return redirect(url_for("FabIndexView.index"))
|
160
|
+
|
161
|
+
|
162
|
+
def has_access_configuration(method: ResourceMethod) -> Callable[[T], T]:
|
163
|
+
return _has_access_no_details(
|
164
|
+
lambda: get_auth_manager().is_authorized_configuration(
|
165
|
+
method=method, user=get_fab_auth_manager().get_user()
|
166
|
+
)
|
167
|
+
)
|
168
|
+
|
169
|
+
|
170
|
+
def has_access_connection(method: ResourceMethod) -> Callable[[T], T]:
|
171
|
+
def has_access_decorator(func: T):
|
172
|
+
@wraps(func)
|
173
|
+
def decorated(*args, **kwargs):
|
174
|
+
connections: set[Connection] = set(args[1])
|
175
|
+
requests: Sequence[IsAuthorizedConnectionRequest] = [
|
176
|
+
{
|
177
|
+
"method": method,
|
178
|
+
"details": ConnectionDetails(conn_id=connection.conn_id),
|
179
|
+
}
|
180
|
+
for connection in connections
|
181
|
+
]
|
182
|
+
is_authorized = get_auth_manager().batch_is_authorized_connection(
|
183
|
+
requests, user=get_auth_manager().get_user()
|
184
|
+
)
|
185
|
+
return _has_access(
|
186
|
+
is_authorized=is_authorized,
|
187
|
+
func=func,
|
188
|
+
args=args,
|
189
|
+
kwargs=kwargs,
|
190
|
+
)
|
191
|
+
|
192
|
+
return cast("T", decorated)
|
193
|
+
|
194
|
+
return has_access_decorator
|
195
|
+
|
196
|
+
|
197
|
+
def has_access_dag(method: ResourceMethod, access_entity: DagAccessEntity | None = None) -> Callable[[T], T]:
|
198
|
+
def has_access_decorator(func: T):
|
199
|
+
@wraps(func)
|
200
|
+
def decorated(*args, **kwargs):
|
201
|
+
dag_id_kwargs = kwargs.get("dag_id")
|
202
|
+
dag_id_args = request.args.get("dag_id")
|
203
|
+
dag_id_form = request.form.get("dag_id")
|
204
|
+
dag_id_json = request.json.get("dag_id") if request.is_json else None
|
205
|
+
all_dag_ids = [dag_id_kwargs, dag_id_args, dag_id_form, dag_id_json]
|
206
|
+
unique_dag_ids = set(dag_id for dag_id in all_dag_ids if dag_id is not None)
|
207
|
+
|
208
|
+
if len(unique_dag_ids) > 1:
|
209
|
+
log.warning(
|
210
|
+
"There are different dag_ids passed in the request: %s. Returning 403.", unique_dag_ids
|
211
|
+
)
|
212
|
+
log.warning(
|
213
|
+
"kwargs: %s, args: %s, form: %s, json: %s",
|
214
|
+
dag_id_kwargs,
|
215
|
+
dag_id_args,
|
216
|
+
dag_id_form,
|
217
|
+
dag_id_json,
|
218
|
+
)
|
219
|
+
return (
|
220
|
+
render_template(
|
221
|
+
"airflow/no_roles_permissions.html",
|
222
|
+
hostname=get_hostname() if conf.getboolean("webserver", "EXPOSE_HOSTNAME") else "",
|
223
|
+
logout_url=get_auth_manager().get_url_logout(),
|
224
|
+
),
|
225
|
+
403,
|
226
|
+
)
|
227
|
+
dag_id = unique_dag_ids.pop() if unique_dag_ids else None
|
228
|
+
|
229
|
+
is_authorized = get_auth_manager().is_authorized_dag(
|
230
|
+
method=method,
|
231
|
+
access_entity=access_entity,
|
232
|
+
details=None if not dag_id else DagDetails(id=dag_id),
|
233
|
+
user=get_auth_manager().get_user(),
|
234
|
+
)
|
235
|
+
|
236
|
+
return _has_access(
|
237
|
+
is_authorized=is_authorized,
|
238
|
+
func=func,
|
239
|
+
args=args,
|
240
|
+
kwargs=kwargs,
|
241
|
+
)
|
242
|
+
|
243
|
+
return cast("T", decorated)
|
244
|
+
|
245
|
+
return has_access_decorator
|
246
|
+
|
247
|
+
|
248
|
+
def has_access_dag_entities(method: ResourceMethod, access_entity: DagAccessEntity) -> Callable[[T], T]:
|
249
|
+
def has_access_decorator(func: T):
|
250
|
+
@wraps(func)
|
251
|
+
def decorated(*args, **kwargs):
|
252
|
+
items: set[XComModel | DagRun | TaskInstance] = set(args[1])
|
253
|
+
requests: Sequence[IsAuthorizedDagRequest] = [
|
254
|
+
{
|
255
|
+
"method": method,
|
256
|
+
"access_entity": access_entity,
|
257
|
+
"details": DagDetails(id=item.dag_id),
|
258
|
+
}
|
259
|
+
for item in items
|
260
|
+
if item is not None
|
261
|
+
]
|
262
|
+
is_authorized = get_auth_manager().batch_is_authorized_dag(
|
263
|
+
requests, user=get_auth_manager().get_user()
|
264
|
+
)
|
265
|
+
return _has_access(
|
266
|
+
is_authorized=is_authorized,
|
267
|
+
func=func,
|
268
|
+
args=args,
|
269
|
+
kwargs=kwargs,
|
270
|
+
)
|
271
|
+
|
272
|
+
return cast("T", decorated)
|
273
|
+
|
274
|
+
return has_access_decorator
|
275
|
+
|
276
|
+
|
277
|
+
def has_access_asset(method: ResourceMethod) -> Callable[[T], T]:
|
278
|
+
"""Check current user's permissions against required permissions for assets."""
|
279
|
+
return _has_access_no_details(
|
280
|
+
lambda: get_auth_manager().is_authorized_asset(method=method, user=get_fab_auth_manager().get_user())
|
281
|
+
)
|
282
|
+
|
283
|
+
|
284
|
+
def has_access_pool(method: ResourceMethod) -> Callable[[T], T]:
|
285
|
+
def has_access_decorator(func: T):
|
286
|
+
@wraps(func)
|
287
|
+
def decorated(*args, **kwargs):
|
288
|
+
pools: set[Pool] = set(args[1])
|
289
|
+
requests: Sequence[IsAuthorizedPoolRequest] = [
|
290
|
+
{
|
291
|
+
"method": method,
|
292
|
+
"details": PoolDetails(name=pool.pool),
|
293
|
+
}
|
294
|
+
for pool in pools
|
295
|
+
]
|
296
|
+
is_authorized = get_auth_manager().batch_is_authorized_pool(
|
297
|
+
requests, user=get_auth_manager().get_user()
|
298
|
+
)
|
299
|
+
return _has_access(
|
300
|
+
is_authorized=is_authorized,
|
301
|
+
func=func,
|
302
|
+
args=args,
|
303
|
+
kwargs=kwargs,
|
304
|
+
)
|
305
|
+
|
306
|
+
return cast("T", decorated)
|
307
|
+
|
308
|
+
return has_access_decorator
|
309
|
+
|
310
|
+
|
311
|
+
def has_access_variable(method: ResourceMethod) -> Callable[[T], T]:
|
312
|
+
def has_access_decorator(func: T):
|
313
|
+
@wraps(func)
|
314
|
+
def decorated(*args, **kwargs):
|
315
|
+
if len(args) == 1:
|
316
|
+
# No items provided
|
317
|
+
is_authorized = get_auth_manager().is_authorized_variable(
|
318
|
+
method=method, user=get_auth_manager().get_user()
|
319
|
+
)
|
320
|
+
else:
|
321
|
+
variables: set[Variable] = set(args[1])
|
322
|
+
requests: Sequence[IsAuthorizedVariableRequest] = [
|
323
|
+
{
|
324
|
+
"method": method,
|
325
|
+
"details": VariableDetails(key=variable.key),
|
326
|
+
}
|
327
|
+
for variable in variables
|
328
|
+
]
|
329
|
+
is_authorized = get_auth_manager().batch_is_authorized_variable(
|
330
|
+
requests, user=get_auth_manager().get_user()
|
331
|
+
)
|
332
|
+
return _has_access(
|
333
|
+
is_authorized=is_authorized,
|
334
|
+
func=func,
|
335
|
+
args=args,
|
336
|
+
kwargs=kwargs,
|
337
|
+
)
|
338
|
+
|
339
|
+
return cast("T", decorated)
|
340
|
+
|
341
|
+
return has_access_decorator
|
342
|
+
|
343
|
+
|
344
|
+
def has_access_view(access_view: AccessView = AccessView.WEBSITE) -> Callable[[T], T]:
|
345
|
+
"""Check current user's permissions to access the website."""
|
346
|
+
return _has_access_no_details(
|
347
|
+
lambda: get_auth_manager().is_authorized_view(
|
348
|
+
access_view=access_view, user=get_fab_auth_manager().get_user()
|
349
|
+
)
|
350
|
+
)
|
@@ -0,0 +1,28 @@
|
|
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
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from pathlib import Path
|
20
|
+
|
21
|
+
from airflow.configuration import conf
|
22
|
+
|
23
|
+
WWW = Path(__file__).resolve().parent
|
24
|
+
# There is a difference with configuring Swagger in Connexion 2.x and Connexion 3.x
|
25
|
+
# Connexion 2: https://connexion.readthedocs.io/en/2.14.2/quickstart.html#the-swagger-ui-console
|
26
|
+
# Connexion 3: https://connexion.readthedocs.io/en/stable/swagger_ui.html#configuring-the-swagger-ui
|
27
|
+
SWAGGER_ENABLED = conf.getboolean("webserver", "enable_swagger_ui", fallback=True)
|
28
|
+
SWAGGER_BUNDLE = WWW.joinpath("static", "dist", "swagger-ui")
|
@@ -36,11 +36,13 @@ from flask_appbuilder.const import (
|
|
36
36
|
)
|
37
37
|
from flask_appbuilder.filters import TemplateFilters
|
38
38
|
from flask_appbuilder.menu import Menu
|
39
|
-
from flask_appbuilder.views import IndexView
|
39
|
+
from flask_appbuilder.views import IndexView, UtilView
|
40
40
|
|
41
41
|
from airflow import settings
|
42
|
+
from airflow.api_fastapi.app import create_auth_manager, get_auth_manager
|
42
43
|
from airflow.configuration import conf
|
43
|
-
from airflow.www.
|
44
|
+
from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
45
|
+
from airflow.providers.fab.www.views import FabIndexView
|
44
46
|
|
45
47
|
if TYPE_CHECKING:
|
46
48
|
from flask import Flask
|
@@ -108,6 +110,7 @@ class AirflowAppBuilder:
|
|
108
110
|
base_template="airflow/main.html",
|
109
111
|
static_folder="static/appbuilder",
|
110
112
|
static_url_path="/appbuilder",
|
113
|
+
enable_plugins: bool = False,
|
111
114
|
):
|
112
115
|
"""
|
113
116
|
App-builder constructor.
|
@@ -124,6 +127,15 @@ class AirflowAppBuilder:
|
|
124
127
|
optional, your override for the global static folder
|
125
128
|
:param static_url_path:
|
126
129
|
optional, your override for the global static url path
|
130
|
+
:param enable_plugins:
|
131
|
+
optional, whether plugins are enabled for this app. AirflowAppBuilder from FAB provider can be
|
132
|
+
instantiated in two modes:
|
133
|
+
- Plugins enabled. The Flask application is responsible to execute Airflow 2 plugins.
|
134
|
+
This application is only running if there are Airflow 2 plugins defined as part of the Airflow
|
135
|
+
environment
|
136
|
+
- Plugins disabled. The Flask application is responsible to execute the FAB auth manager login
|
137
|
+
process. This application is only running if FAB auth manager is the auth manager configured
|
138
|
+
in the Airflow environment
|
127
139
|
"""
|
128
140
|
from airflow.providers_manager import ProvidersManager
|
129
141
|
|
@@ -138,6 +150,7 @@ class AirflowAppBuilder:
|
|
138
150
|
self.static_folder = static_folder
|
139
151
|
self.static_url_path = static_url_path
|
140
152
|
self.app = app
|
153
|
+
self.enable_plugins = enable_plugins
|
141
154
|
self.update_perms = conf.getboolean("fab", "UPDATE_FAB_PERMS")
|
142
155
|
self.auth_rate_limited = conf.getboolean("fab", "AUTH_RATE_LIMITED")
|
143
156
|
self.auth_rate_limit = conf.get("fab", "AUTH_RATE_LIMIT")
|
@@ -171,8 +184,10 @@ class AirflowAppBuilder:
|
|
171
184
|
_index_view = app.config.get("FAB_INDEX_VIEW", None)
|
172
185
|
if _index_view is not None:
|
173
186
|
self.indexview = dynamic_class_import(_index_view)
|
187
|
+
elif not self.enable_plugins:
|
188
|
+
self.indexview = FabIndexView
|
174
189
|
else:
|
175
|
-
self.indexview =
|
190
|
+
self.indexview = IndexView
|
176
191
|
_menu = app.config.get("FAB_MENU", None)
|
177
192
|
if _menu is not None:
|
178
193
|
self.menu = dynamic_class_import(_menu)
|
@@ -181,8 +196,14 @@ class AirflowAppBuilder:
|
|
181
196
|
|
182
197
|
self._addon_managers = app.config["ADDON_MANAGERS"]
|
183
198
|
self.session = session
|
184
|
-
auth_manager =
|
185
|
-
|
199
|
+
auth_manager = create_auth_manager()
|
200
|
+
auth_manager.appbuilder = self
|
201
|
+
if hasattr(auth_manager, "init_flask_resources"):
|
202
|
+
auth_manager.init_flask_resources()
|
203
|
+
if hasattr(auth_manager, "security_manager"):
|
204
|
+
self.sm = auth_manager.security_manager
|
205
|
+
else:
|
206
|
+
self.sm = AirflowSecurityManagerV2(self)
|
186
207
|
self.bm = BabelManager(self)
|
187
208
|
self._add_global_static()
|
188
209
|
self._add_global_filters()
|
@@ -190,6 +211,15 @@ class AirflowAppBuilder:
|
|
190
211
|
self._add_admin_views()
|
191
212
|
self._add_addon_views()
|
192
213
|
self._init_extension(app)
|
214
|
+
self._swap_url_filter()
|
215
|
+
|
216
|
+
def _swap_url_filter(self):
|
217
|
+
"""Use our url filtering util function so there is consistency between FAB and Airflow routes."""
|
218
|
+
from flask_appbuilder.security import views as fab_sec_views
|
219
|
+
|
220
|
+
from airflow.providers.fab.www.views import get_safe_url
|
221
|
+
|
222
|
+
fab_sec_views.get_safe_redirect = get_safe_url
|
193
223
|
|
194
224
|
def _init_extension(self, app):
|
195
225
|
app.appbuilder = self
|
@@ -276,6 +306,10 @@ class AirflowAppBuilder:
|
|
276
306
|
"""Register indexview, utilview (back function), babel views and Security views."""
|
277
307
|
self.indexview = self._check_and_init(self.indexview)
|
278
308
|
self.add_view_no_menu(self.indexview)
|
309
|
+
self.add_view_no_menu(UtilView())
|
310
|
+
auth_manager = get_auth_manager()
|
311
|
+
if hasattr(auth_manager, "register_views"):
|
312
|
+
auth_manager.register_views()
|
279
313
|
|
280
314
|
def _add_addon_views(self):
|
281
315
|
"""Register declared addons."""
|
@@ -494,9 +528,11 @@ class AirflowAppBuilder:
|
|
494
528
|
|
495
529
|
@property
|
496
530
|
def get_url_for_index(self):
|
497
|
-
# TODO: Return the fast api application homepage
|
498
531
|
return url_for(f"{self.indexview.endpoint}.{self.indexview.default_view}")
|
499
532
|
|
533
|
+
def get_url_for_login_with(self, next_url: str | None = None) -> str:
|
534
|
+
return get_auth_manager().get_url_login(next_url=next_url)
|
535
|
+
|
500
536
|
def get_url_for_locale(self, lang):
|
501
537
|
return url_for(
|
502
538
|
f"{self.bm.locale_view.endpoint}.{self.bm.locale_view.default_view}",
|
@@ -510,15 +546,23 @@ class AirflowAppBuilder:
|
|
510
546
|
def _add_permission(self, baseview, update_perms=False):
|
511
547
|
if self.update_perms or update_perms:
|
512
548
|
try:
|
513
|
-
self.sm
|
549
|
+
if hasattr(self.sm, "add_permissions_view"):
|
550
|
+
self.sm.add_permissions_view(baseview.base_permissions, baseview.class_permission_name)
|
514
551
|
except Exception as e:
|
515
552
|
log.exception(e)
|
516
553
|
log.error(LOGMSG_ERR_FAB_ADD_PERMISSION_VIEW, e)
|
517
554
|
|
555
|
+
def add_permissions(self, update_perms=False):
|
556
|
+
if self.update_perms or update_perms:
|
557
|
+
for baseview in self.baseviews:
|
558
|
+
self._add_permission(baseview, update_perms=update_perms)
|
559
|
+
self._add_menu_permissions(update_perms=update_perms)
|
560
|
+
|
518
561
|
def _add_permissions_menu(self, name, update_perms=False):
|
519
562
|
if self.update_perms or update_perms:
|
520
563
|
try:
|
521
|
-
self.sm
|
564
|
+
if hasattr(self.sm, "add_permissions_menu"):
|
565
|
+
self.sm.add_permissions_menu(name)
|
522
566
|
except Exception as e:
|
523
567
|
log.exception(e)
|
524
568
|
log.error(LOGMSG_ERR_FAB_ADD_PERMISSION_MENU, e)
|
@@ -548,10 +592,11 @@ class AirflowAppBuilder:
|
|
548
592
|
view.get_init_inner_views().append(v)
|
549
593
|
|
550
594
|
|
551
|
-
def init_appbuilder(app: Flask) -> AirflowAppBuilder:
|
595
|
+
def init_appbuilder(app: Flask, enable_plugins: bool) -> AirflowAppBuilder:
|
552
596
|
"""Init `Flask App Builder <https://flask-appbuilder.readthedocs.io/en/latest/>`__."""
|
553
597
|
return AirflowAppBuilder(
|
554
598
|
app=app,
|
555
599
|
session=settings.Session,
|
556
600
|
base_template="airflow/main.html",
|
601
|
+
enable_plugins=enable_plugins,
|
557
602
|
)
|
@@ -30,17 +30,17 @@ from airflow.utils.platform import get_airflow_git_version
|
|
30
30
|
logger = logging.getLogger(__name__)
|
31
31
|
|
32
32
|
|
33
|
-
def init_jinja_globals(app):
|
33
|
+
def init_jinja_globals(app, enable_plugins: bool):
|
34
34
|
"""Add extra globals variable to Jinja context."""
|
35
35
|
server_timezone = conf.get("core", "default_timezone")
|
36
36
|
if server_timezone == "system":
|
37
|
-
server_timezone = pendulum.local_timezone().name
|
37
|
+
server_timezone = pendulum.local_timezone().name # type: ignore[operator]
|
38
38
|
elif server_timezone == "utc":
|
39
39
|
server_timezone = "UTC"
|
40
40
|
|
41
41
|
default_ui_timezone = conf.get("webserver", "default_ui_timezone")
|
42
42
|
if default_ui_timezone == "system":
|
43
|
-
default_ui_timezone = pendulum.local_timezone().name
|
43
|
+
default_ui_timezone = pendulum.local_timezone().name # type: ignore[operator]
|
44
44
|
elif default_ui_timezone == "utc":
|
45
45
|
default_ui_timezone = "UTC"
|
46
46
|
if not default_ui_timezone:
|
@@ -70,6 +70,8 @@ def init_jinja_globals(app):
|
|
70
70
|
"state_color_mapping": STATE_COLORS,
|
71
71
|
"airflow_version": airflow_version,
|
72
72
|
"git_version": git_version,
|
73
|
+
"show_plugin_message": enable_plugins,
|
74
|
+
"disable_nav_bar": not enable_plugins,
|
73
75
|
}
|
74
76
|
|
75
77
|
# Extra global specific to auth manager
|
@@ -17,8 +17,10 @@
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
19
|
import logging
|
20
|
+
from importlib import import_module
|
20
21
|
|
21
22
|
from airflow.configuration import conf
|
23
|
+
from airflow.exceptions import AirflowException
|
22
24
|
|
23
25
|
log = logging.getLogger(__name__)
|
24
26
|
|
@@ -40,3 +42,20 @@ def init_xframe_protection(app):
|
|
40
42
|
return response
|
41
43
|
|
42
44
|
app.after_request(apply_caching)
|
45
|
+
|
46
|
+
|
47
|
+
def init_api_auth(app):
|
48
|
+
"""Load authentication backends."""
|
49
|
+
auth_backends = conf.get(
|
50
|
+
"fab", "auth_backends", fallback="airflow.providers.fab.auth_manager.api.auth.backend.session"
|
51
|
+
)
|
52
|
+
|
53
|
+
app.api_auth = []
|
54
|
+
try:
|
55
|
+
for backend in auth_backends.split(","):
|
56
|
+
auth = import_module(backend.strip())
|
57
|
+
auth.init_app(app)
|
58
|
+
app.api_auth.append(auth)
|
59
|
+
except ImportError as err:
|
60
|
+
log.critical("Cannot import %s for API authentication due to: %s", backend, err)
|
61
|
+
raise AirflowException(err)
|