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.
Files changed (88) hide show
  1. airflow/providers/fab/LICENSE +0 -52
  2. airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -4
  3. airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
  4. airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
  5. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -14
  6. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -13
  7. airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
  8. airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
  9. airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
  10. airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
  11. airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +153 -0
  12. airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
  13. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
  14. airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
  15. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
  16. airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
  17. airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
  18. airflow/providers/fab/auth_manager/cli_commands/utils.py +17 -4
  19. airflow/providers/fab/auth_manager/fab_auth_manager.py +222 -119
  20. airflow/providers/fab/auth_manager/models/__init__.py +1 -1
  21. airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
  22. airflow/providers/fab/auth_manager/models/db.py +22 -5
  23. airflow/providers/fab/auth_manager/openapi/v1.yaml +9 -0
  24. airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
  25. airflow/providers/fab/auth_manager/security_manager/override.py +89 -561
  26. airflow/providers/fab/auth_manager/views/permissions.py +1 -1
  27. airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
  28. airflow/providers/fab/auth_manager/views/user.py +1 -1
  29. airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
  30. airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
  31. airflow/providers/fab/get_provider_info.py +26 -15
  32. airflow/providers/fab/www/airflow_flask_app.py +31 -0
  33. airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
  34. airflow/providers/fab/www/api_connexion/parameters.py +131 -0
  35. airflow/providers/fab/www/api_connexion/security.py +84 -0
  36. airflow/providers/fab/www/api_connexion/types.py +30 -0
  37. airflow/providers/fab/www/app.py +34 -9
  38. airflow/providers/fab/www/auth.py +350 -0
  39. airflow/providers/fab/www/constants.py +28 -0
  40. airflow/providers/fab/www/extensions/init_appbuilder.py +54 -9
  41. airflow/providers/fab/www/extensions/init_jinja_globals.py +5 -3
  42. airflow/providers/fab/www/extensions/init_security.py +19 -0
  43. airflow/providers/fab/www/extensions/init_session.py +64 -0
  44. airflow/providers/fab/www/extensions/init_views.py +111 -1
  45. airflow/providers/fab/www/package-lock.json +4967 -16517
  46. airflow/providers/fab/www/package.json +25 -104
  47. airflow/providers/fab/www/security/__init__.py +17 -0
  48. airflow/providers/fab/www/security/permissions.py +126 -0
  49. airflow/providers/fab/www/security_appless.py +44 -0
  50. airflow/providers/fab/www/security_manager.py +122 -0
  51. airflow/providers/fab/www/session.py +41 -0
  52. airflow/providers/fab/www/static/css/flash.css +57 -0
  53. airflow/providers/fab/www/static/dist/48f0ea180c40270a5b05.png +1 -0
  54. airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -0
  55. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
  56. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
  57. airflow/providers/fab/www/static/dist/f7490d556a6c42e49ba4.png +1 -0
  58. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
  59. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
  60. airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
  61. airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
  62. airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
  63. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
  64. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
  65. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.css +18 -0
  66. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
  67. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
  68. airflow/providers/fab/www/static/dist/manifest.json +20 -0
  69. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
  70. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
  71. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js +2 -0
  72. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
  73. airflow/providers/fab/www/static/dist/oss-licenses.json +20 -0
  74. airflow/providers/fab/www/templates/airflow/main.html +10 -11
  75. airflow/providers/fab/www/templates/airflow/traceback.html +1 -5
  76. airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
  77. airflow/providers/fab/www/templates/appbuilder/navbar.html +7 -0
  78. airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
  79. airflow/providers/fab/www/utils.py +272 -0
  80. airflow/providers/fab/www/views.py +129 -0
  81. airflow/providers/fab/www/webpack.config.js +5 -40
  82. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/METADATA +24 -34
  83. apache_airflow_providers_fab-2.0.0rc2.dist-info/RECORD +125 -0
  84. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/WHEEL +1 -1
  85. airflow/providers/fab/auth_manager/decorators/auth.py +0 -127
  86. apache_airflow_providers_fab-2.0.0rc1.dist-info/RECORD +0 -78
  87. /airflow/providers/fab/{auth_manager/decorators → www/api_connexion}/__init__.py +0 -0
  88. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/entry_points.txt +0 -0
@@ -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.init_views import init_error_handlers, init_plugins
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(config=None, testing=False):
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'Cannot use relative path: `{conf.get("database", "SQL_ALCHEMY_CONN")}` to connect to sqlite. '
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
- init_jinja_globals(flask_app)
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(config=None, testing=False):
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(config=config, testing=testing)
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.extensions.init_auth_manager import init_auth_manager
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 = self.indexview or 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 = init_auth_manager(self)
185
- self.sm = auth_manager.security_manager
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.add_permissions_view(baseview.base_permissions, baseview.class_permission_name)
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.add_permissions_menu(name)
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)