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
@@ -18,22 +18,25 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import argparse
21
- from collections.abc import Container
22
21
  from functools import cached_property
23
22
  from pathlib import Path
24
23
  from typing import TYPE_CHECKING, Any
24
+ from urllib.parse import urljoin
25
25
 
26
26
  import packaging.version
27
27
  from connexion import FlaskApi
28
- from flask import Blueprint, g, url_for
29
- from packaging.version import Version
28
+ from fastapi import FastAPI
29
+ from flask import Blueprint, g
30
30
  from sqlalchemy import select
31
31
  from sqlalchemy.orm import Session, joinedload
32
+ from starlette.middleware.wsgi import WSGIMiddleware
32
33
 
33
34
  from airflow import __version__ as airflow_version
34
- from airflow.auth.managers.base_auth_manager import BaseAuthManager, ResourceMethod
35
- from airflow.auth.managers.models.resource_details import (
35
+ from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
36
+ from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
37
+ from airflow.api_fastapi.auth.managers.models.resource_details import (
36
38
  AccessView,
39
+ BackfillDetails,
37
40
  ConfigurationDetails,
38
41
  ConnectionDetails,
39
42
  DagAccessEntity,
@@ -41,7 +44,7 @@ from airflow.auth.managers.models.resource_details import (
41
44
  PoolDetails,
42
45
  VariableDetails,
43
46
  )
44
- from airflow.auth.managers.utils.fab import get_fab_action_from_method_map, get_method_from_fab_action_map
47
+ from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem
45
48
  from airflow.cli.cli_config import (
46
49
  DefaultHelpParser,
47
50
  GroupCommand,
@@ -56,8 +59,15 @@ from airflow.providers.fab.auth_manager.cli_commands.definition import (
56
59
  USERS_COMMANDS,
57
60
  )
58
61
  from airflow.providers.fab.auth_manager.models import Permission, Role, User
59
- from airflow.security import permissions
60
- from airflow.security.permissions import (
62
+ from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser
63
+ from airflow.providers.fab.www.app import create_app
64
+ from airflow.providers.fab.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
65
+ from airflow.providers.fab.www.extensions.init_views import (
66
+ _CustomErrorRequestBodyValidator,
67
+ _LazyResolver,
68
+ )
69
+ from airflow.providers.fab.www.security import permissions
70
+ from airflow.providers.fab.www.security.permissions import (
61
71
  RESOURCE_AUDIT_LOG,
62
72
  RESOURCE_CLUSTER_ACTIVITY,
63
73
  RESOURCE_CONFIG,
@@ -66,6 +76,7 @@ from airflow.security.permissions import (
66
76
  RESOURCE_DAG_CODE,
67
77
  RESOURCE_DAG_DEPENDENCIES,
68
78
  RESOURCE_DAG_RUN,
79
+ RESOURCE_DAG_VERSION,
69
80
  RESOURCE_DAG_WARNING,
70
81
  RESOURCE_DOCS,
71
82
  RESOURCE_IMPORT_ERROR,
@@ -82,22 +93,33 @@ from airflow.security.permissions import (
82
93
  RESOURCE_WEBSITE,
83
94
  RESOURCE_XCOM,
84
95
  )
96
+ from airflow.providers.fab.www.utils import (
97
+ get_fab_action_from_method_map,
98
+ get_method_from_fab_action_map,
99
+ )
100
+ from airflow.security.permissions import RESOURCE_BACKFILL
85
101
  from airflow.utils.session import NEW_SESSION, create_session, provide_session
86
102
  from airflow.utils.yaml import safe_load
87
- from airflow.version import version
88
- from airflow.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED
89
- from airflow.www.extensions.init_views import _CustomErrorRequestBodyValidator, _LazyResolver
90
103
 
91
104
  if TYPE_CHECKING:
92
- from airflow.auth.managers.models.base_user import BaseUser
105
+ from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
93
106
  from airflow.cli.cli_config import (
94
107
  CLICommand,
95
108
  )
96
- from airflow.providers.common.compat.assets import AssetDetails
97
- from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
98
- from airflow.security.permissions import RESOURCE_ASSET
109
+ from airflow.providers.common.compat.assets import AssetAliasDetails, AssetDetails
110
+ from airflow.providers.fab.auth_manager.security_manager.override import (
111
+ FabAirflowSecurityManagerOverride,
112
+ )
113
+ from airflow.providers.fab.www.extensions.init_appbuilder import AirflowAppBuilder
114
+ from airflow.providers.fab.www.security.permissions import (
115
+ RESOURCE_ASSET,
116
+ RESOURCE_ASSET_ALIAS,
117
+ )
99
118
  else:
100
- from airflow.providers.common.compat.security.permissions import RESOURCE_ASSET
119
+ from airflow.providers.common.compat.security.permissions import (
120
+ RESOURCE_ASSET,
121
+ RESOURCE_ASSET_ALIAS,
122
+ )
101
123
 
102
124
 
103
125
  _MAP_DAG_ACCESS_ENTITY_TO_FAB_RESOURCE_TYPE: dict[DagAccessEntity, tuple[str, ...]] = {
@@ -115,6 +137,7 @@ _MAP_DAG_ACCESS_ENTITY_TO_FAB_RESOURCE_TYPE: dict[DagAccessEntity, tuple[str, ..
115
137
  DagAccessEntity.TASK_INSTANCE: (RESOURCE_DAG_RUN, RESOURCE_TASK_INSTANCE),
116
138
  DagAccessEntity.TASK_LOGS: (RESOURCE_TASK_LOG,),
117
139
  DagAccessEntity.TASK_RESCHEDULE: (RESOURCE_TASK_RESCHEDULE,),
140
+ DagAccessEntity.VERSION: (RESOURCE_DAG_VERSION,),
118
141
  DagAccessEntity.WARNING: (RESOURCE_DAG_WARNING,),
119
142
  DagAccessEntity.XCOM: (RESOURCE_XCOM,),
120
143
  }
@@ -130,6 +153,19 @@ _MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE = {
130
153
  AccessView.WEBSITE: RESOURCE_WEBSITE,
131
154
  }
132
155
 
156
+ _MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE = {
157
+ MenuItem.ASSETS: RESOURCE_ASSET,
158
+ MenuItem.ASSET_EVENTS: RESOURCE_ASSET,
159
+ MenuItem.CONNECTIONS: RESOURCE_CONNECTION,
160
+ MenuItem.DAGS: RESOURCE_DAG,
161
+ MenuItem.DOCS: RESOURCE_DOCS,
162
+ MenuItem.PLUGINS: RESOURCE_PLUGIN,
163
+ MenuItem.POOLS: RESOURCE_POOL,
164
+ MenuItem.PROVIDERS: RESOURCE_PROVIDER,
165
+ MenuItem.VARIABLES: RESOURCE_VARIABLE,
166
+ MenuItem.XCOMS: RESOURCE_XCOM,
167
+ }
168
+
133
169
 
134
170
  class FabAuthManager(BaseAuthManager[User]):
135
171
  """
@@ -138,10 +174,14 @@ class FabAuthManager(BaseAuthManager[User]):
138
174
  This auth manager is responsible for providing a backward compatible user management experience to users.
139
175
  """
140
176
 
141
- def init(self) -> None:
142
- """Run operations when Airflow is initializing."""
143
- if self.appbuilder:
144
- self._sync_appbuilder_roles()
177
+ appbuilder: AirflowAppBuilder | None = None
178
+
179
+ def init_flask_resources(self) -> None:
180
+ self._sync_appbuilder_roles()
181
+
182
+ @cached_property
183
+ def apiserver_endpoint(self) -> str:
184
+ return conf.get("api", "base_url")
145
185
 
146
186
  @staticmethod
147
187
  def get_cli_commands() -> list[CLICommand]:
@@ -166,6 +206,31 @@ class FabAuthManager(BaseAuthManager[User]):
166
206
  commands.append(GroupCommand(name="fab-db", help="Manage FAB", subcommands=DB_COMMANDS))
167
207
  return commands
168
208
 
209
+ def get_fastapi_app(self) -> FastAPI | None:
210
+ """Get the FastAPI app."""
211
+ from airflow.providers.fab.auth_manager.api_fastapi.routes.login import (
212
+ login_router,
213
+ )
214
+
215
+ flask_app = create_app(enable_plugins=False)
216
+
217
+ app = FastAPI(
218
+ title="FAB auth manager API",
219
+ description=(
220
+ "This is FAB auth manager API. This API is only available if the auth manager used in "
221
+ "the Airflow environment is FAB auth manager. "
222
+ "This API provides endpoints to manage users and permissions managed by the FAB auth "
223
+ "manager."
224
+ ),
225
+ )
226
+
227
+ # Add the login router to the FastAPI app
228
+ app.include_router(login_router)
229
+
230
+ app.mount("/", WSGIMiddleware(flask_app))
231
+
232
+ return app
233
+
169
234
  def get_api_endpoints(self) -> None | Blueprint:
170
235
  folder = Path(__file__).parents[0].resolve() # this is airflow/auth/managers/fab/
171
236
  with folder.joinpath("openapi", "v1.yaml").open() as f:
@@ -173,20 +238,16 @@ class FabAuthManager(BaseAuthManager[User]):
173
238
  return FlaskApi(
174
239
  specification=specification,
175
240
  resolver=_LazyResolver(),
176
- base_path="/auth/fab/v1",
177
- options={"swagger_ui": SWAGGER_ENABLED, "swagger_path": SWAGGER_BUNDLE.__fspath__()},
241
+ base_path="/fab/v1",
242
+ options={
243
+ "swagger_ui": SWAGGER_ENABLED,
244
+ "swagger_path": SWAGGER_BUNDLE.__fspath__(),
245
+ },
178
246
  strict_validation=True,
179
247
  validate_responses=True,
180
248
  validator_map={"body": _CustomErrorRequestBodyValidator},
181
249
  ).blueprint
182
250
 
183
- def get_user_display_name(self) -> str:
184
- """Return the user's display name associated to the user in session."""
185
- user = self.get_user()
186
- first_name = user.first_name.strip() if isinstance(user.first_name, str) else ""
187
- last_name = user.last_name.strip() if isinstance(user.last_name, str) else ""
188
- return f"{first_name} {last_name}".strip()
189
-
190
251
  def get_user(self) -> User:
191
252
  """
192
253
  Return the user associated to the user in session.
@@ -206,29 +267,26 @@ class FabAuthManager(BaseAuthManager[User]):
206
267
 
207
268
  def deserialize_user(self, token: dict[str, Any]) -> User:
208
269
  with create_session() as session:
209
- return session.get(User, token["id"])
270
+ return session.get(User, int(token["sub"]))
210
271
 
211
272
  def serialize_user(self, user: User) -> dict[str, Any]:
212
- return {"id": user.id}
273
+ return {"sub": str(user.id)}
213
274
 
214
275
  def is_logged_in(self) -> bool:
215
276
  """Return whether the user is logged in."""
216
277
  user = self.get_user()
217
- if Version(Version(version).base_version) < Version("3.0.0"):
218
- return not user.is_anonymous and user.is_active
219
- else:
220
- return (
221
- self.appbuilder
222
- and self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
223
- or (not user.is_anonymous and user.is_active)
224
- )
278
+ return (
279
+ self.appbuilder
280
+ and self.appbuilder.get_app.config.get("AUTH_ROLE_PUBLIC", None)
281
+ or (not user.is_anonymous and user.is_active)
282
+ )
225
283
 
226
284
  def is_authorized_configuration(
227
285
  self,
228
286
  *,
229
287
  method: ResourceMethod,
288
+ user: User,
230
289
  details: ConfigurationDetails | None = None,
231
- user: BaseUser | None = None,
232
290
  ) -> bool:
233
291
  return self._is_authorized(method=method, resource_type=RESOURCE_CONFIG, user=user)
234
292
 
@@ -236,8 +294,8 @@ class FabAuthManager(BaseAuthManager[User]):
236
294
  self,
237
295
  *,
238
296
  method: ResourceMethod,
297
+ user: User,
239
298
  details: ConnectionDetails | None = None,
240
- user: BaseUser | None = None,
241
299
  ) -> bool:
242
300
  return self._is_authorized(method=method, resource_type=RESOURCE_CONNECTION, user=user)
243
301
 
@@ -245,9 +303,9 @@ class FabAuthManager(BaseAuthManager[User]):
245
303
  self,
246
304
  *,
247
305
  method: ResourceMethod,
306
+ user: User,
248
307
  access_entity: DagAccessEntity | None = None,
249
308
  details: DagDetails | None = None,
250
- user: BaseUser | None = None,
251
309
  ) -> bool:
252
310
  """
253
311
  Return whether the user is authorized to access the dag.
@@ -264,9 +322,9 @@ class FabAuthManager(BaseAuthManager[User]):
264
322
  if no specific DAG is targeted, just check the sub entity.
265
323
 
266
324
  :param method: The method to authorize.
325
+ :param user: The user performing the action.
267
326
  :param access_entity: The dag access entity.
268
327
  :param details: The dag details.
269
- :param user: The user.
270
328
  """
271
329
  if not access_entity:
272
330
  # Scenario 1
@@ -282,74 +340,100 @@ class FabAuthManager(BaseAuthManager[User]):
282
340
  return False
283
341
 
284
342
  return all(
285
- self._is_authorized(method=method, resource_type=resource_type, user=user)
286
- if resource_type != RESOURCE_DAG_RUN or not hasattr(permissions, "resource_name")
287
- else self._is_authorized_dag_run(method=method, details=details, user=user)
343
+ (
344
+ self._is_authorized(method=method, resource_type=resource_type, user=user)
345
+ if resource_type != RESOURCE_DAG_RUN or not hasattr(permissions, "resource_name")
346
+ else self._is_authorized_dag_run(method=method, details=details, user=user)
347
+ )
288
348
  for resource_type in resource_types
289
349
  )
290
350
 
351
+ def is_authorized_backfill(
352
+ self,
353
+ *,
354
+ method: ResourceMethod,
355
+ user: User,
356
+ details: BackfillDetails | None = None,
357
+ ) -> bool:
358
+ return self._is_authorized(method=method, resource_type=RESOURCE_BACKFILL, user=user)
359
+
291
360
  def is_authorized_asset(
292
- self, *, method: ResourceMethod, details: AssetDetails | None = None, user: BaseUser | None = None
361
+ self, *, method: ResourceMethod, user: User, details: AssetDetails | None = None
293
362
  ) -> bool:
294
363
  return self._is_authorized(method=method, resource_type=RESOURCE_ASSET, user=user)
295
364
 
365
+ def is_authorized_asset_alias(
366
+ self,
367
+ *,
368
+ method: ResourceMethod,
369
+ user: User,
370
+ details: AssetAliasDetails | None = None,
371
+ ) -> bool:
372
+ return self._is_authorized(method=method, resource_type=RESOURCE_ASSET_ALIAS, user=user)
373
+
296
374
  def is_authorized_pool(
297
- self, *, method: ResourceMethod, details: PoolDetails | None = None, user: BaseUser | None = None
375
+ self, *, method: ResourceMethod, user: User, details: PoolDetails | None = None
298
376
  ) -> bool:
299
377
  return self._is_authorized(method=method, resource_type=RESOURCE_POOL, user=user)
300
378
 
301
379
  def is_authorized_variable(
302
- self, *, method: ResourceMethod, details: VariableDetails | None = None, user: BaseUser | None = None
380
+ self,
381
+ *,
382
+ method: ResourceMethod,
383
+ user: User,
384
+ details: VariableDetails | None = None,
303
385
  ) -> bool:
304
386
  return self._is_authorized(method=method, resource_type=RESOURCE_VARIABLE, user=user)
305
387
 
306
- def is_authorized_view(self, *, access_view: AccessView, user: BaseUser | None = None) -> bool:
388
+ def is_authorized_view(self, *, access_view: AccessView, user: User) -> bool:
307
389
  # "Docs" are only links in the menu, there is no page associated
308
390
  method: ResourceMethod = "MENU" if access_view == AccessView.DOCS else "GET"
309
391
  return self._is_authorized(
310
- method=method, resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view], user=user
392
+ method=method,
393
+ resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view],
394
+ user=user,
311
395
  )
312
396
 
313
397
  def is_authorized_custom_view(
314
- self, *, method: ResourceMethod | str, resource_name: str, user: BaseUser | None = None
315
- ):
316
- if not user:
317
- user = self.get_user()
398
+ self, *, method: ResourceMethod | str, resource_name: str, user: User
399
+ ) -> bool:
318
400
  fab_action_name = get_fab_action_from_method_map().get(method, method)
319
401
  return (fab_action_name, resource_name) in self._get_user_permissions(user)
320
402
 
403
+ def filter_authorized_menu_items(self, menu_items: list[MenuItem], user: User) -> list[MenuItem]:
404
+ return [
405
+ menu_item
406
+ for menu_item in menu_items
407
+ if self._is_authorized(
408
+ method="MENU",
409
+ resource_type=_MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE.get(menu_item, menu_item.value),
410
+ user=user,
411
+ )
412
+ ]
413
+
321
414
  @provide_session
322
- def get_permitted_dag_ids(
415
+ def get_authorized_dag_ids(
323
416
  self,
324
417
  *,
325
- methods: Container[ResourceMethod] | None = None,
326
- user=None,
418
+ user: User,
419
+ method: ResourceMethod = "GET",
327
420
  session: Session = NEW_SESSION,
328
421
  ) -> set[str]:
329
- if not methods:
330
- methods = ["PUT", "GET"]
331
-
332
- if not user:
333
- user = self.get_user()
334
-
335
- if not self.is_logged_in():
336
- roles = user.roles
337
- else:
338
- if ("GET" in methods and self.is_authorized_dag(method="GET", user=user)) or (
339
- "PUT" in methods and self.is_authorized_dag(method="PUT", user=user)
340
- ):
341
- # If user is authorized to read/edit all DAGs, return all DAGs
342
- return {dag.dag_id for dag in session.execute(select(DagModel.dag_id))}
343
- user_query = session.scalar(
344
- select(User)
345
- .options(
346
- joinedload(User.roles)
347
- .subqueryload(Role.permissions)
348
- .options(joinedload(Permission.action), joinedload(Permission.resource))
349
- )
350
- .where(User.id == user.id)
422
+ if self._is_authorized(method=method, resource_type=RESOURCE_DAG, user=user):
423
+ # If user is authorized to access all DAGs, return all DAGs
424
+ return {dag.dag_id for dag in session.execute(select(DagModel.dag_id))}
425
+ if isinstance(user, AnonymousUser):
426
+ return set()
427
+ user_query = session.scalar(
428
+ select(User)
429
+ .options(
430
+ joinedload(User.roles)
431
+ .subqueryload(Role.permissions)
432
+ .options(joinedload(Permission.action), joinedload(Permission.resource))
351
433
  )
352
- roles = user_query.roles
434
+ .where(User.id == user.id)
435
+ )
436
+ roles = user_query.roles
353
437
 
354
438
  map_fab_action_name_to_method_name = get_method_from_fab_action_map()
355
439
  resources = set()
@@ -358,7 +442,7 @@ class FabAuthManager(BaseAuthManager[User]):
358
442
  action = permission.action.name
359
443
  if (
360
444
  action in map_fab_action_name_to_method_name
361
- and map_fab_action_name_to_method_name[action] in methods
445
+ and map_fab_action_name_to_method_name[action] == method
362
446
  ):
363
447
  resource = permission.resource.name
364
448
  if resource == permissions.RESOURCE_DAG:
@@ -391,49 +475,72 @@ class FabAuthManager(BaseAuthManager[User]):
391
475
 
392
476
  def get_url_login(self, **kwargs) -> str:
393
477
  """Return the login page url."""
394
- if not self.security_manager.auth_view:
395
- raise AirflowException("`auth_view` not defined in the security manager.")
396
- if next_url := kwargs.get("next_url"):
397
- return url_for(f"{self.security_manager.auth_view.endpoint}.login", next=next_url)
398
- else:
399
- return url_for(f"{self.security_manager.auth_view.endpoint}.login")
478
+ return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login/")
400
479
 
401
- def get_url_logout(self):
480
+ def get_url_logout(self) -> str | None:
402
481
  """Return the logout page url."""
403
- if not self.security_manager.auth_view:
404
- raise AirflowException("`auth_view` not defined in the security manager.")
405
- return url_for(f"{self.security_manager.auth_view.endpoint}.logout")
406
-
407
- def get_url_user_profile(self) -> str | None:
408
- """Return the url to a page displaying info about the current user."""
409
- if not self.security_manager.user_view or self.appbuilder.get_app.config.get(
410
- "AUTH_ROLE_PUBLIC", None
411
- ):
412
- return None
413
- return url_for(f"{self.security_manager.user_view.endpoint}.userinfo")
482
+ return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/logout/")
414
483
 
415
484
  def register_views(self) -> None:
416
485
  self.security_manager.register_views()
417
486
 
487
+ def get_extra_menu_items(self, *, user: User) -> list[ExtraMenuItem]:
488
+ # Contains the list of menu items. ``resource_type`` is the name of the resource in FAB
489
+ # permission model to check whether the user is allowed to see this menu item
490
+ items = [
491
+ {
492
+ "resource_type": "List Users",
493
+ "text": "Users",
494
+ "href": f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/users/list/",
495
+ },
496
+ {
497
+ "resource_type": "List Roles",
498
+ "text": "Roles",
499
+ "href": f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/roles/list/",
500
+ },
501
+ {
502
+ "resource_type": "Actions",
503
+ "text": "Actions",
504
+ "href": f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/actions/list/",
505
+ },
506
+ {
507
+ "resource_type": "Resources",
508
+ "text": "Resources",
509
+ "href": f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/resources/list/",
510
+ },
511
+ {
512
+ "resource_type": "Permission Pairs",
513
+ "text": "Permissions",
514
+ "href": f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/permissions/list/",
515
+ },
516
+ ]
517
+
518
+ return [
519
+ ExtraMenuItem(text=item["text"], href=item["href"])
520
+ for item in items
521
+ if self._is_authorized(method="MENU", resource_type=item["resource_type"], user=user)
522
+ ]
523
+
524
+ @staticmethod
525
+ def get_db_manager() -> str | None:
526
+ return "airflow.providers.fab.auth_manager.models.db.FABDBManager"
527
+
418
528
  def _is_authorized(
419
529
  self,
420
530
  *,
421
531
  method: ResourceMethod,
422
532
  resource_type: str,
423
- user: BaseUser | None = None,
533
+ user: User,
424
534
  ) -> bool:
425
535
  """
426
536
  Return whether the user is authorized to perform a given action.
427
537
 
428
538
  :param method: the method to perform
429
539
  :param resource_type: the type of resource the user attempts to perform the action on
430
- :param user: the user to perform the action on. If not provided (or None), it uses the current user
540
+ :param user: the user to performing the action
431
541
 
432
542
  :meta private:
433
543
  """
434
- if not user:
435
- user = self.get_user()
436
-
437
544
  fab_action = self._get_fab_action(method)
438
545
  user_permissions = self._get_user_permissions(user)
439
546
 
@@ -442,15 +549,15 @@ class FabAuthManager(BaseAuthManager[User]):
442
549
  def _is_authorized_dag(
443
550
  self,
444
551
  method: ResourceMethod,
445
- details: DagDetails | None = None,
446
- user: BaseUser | None = None,
552
+ details: DagDetails | None,
553
+ user: User,
447
554
  ) -> bool:
448
555
  """
449
556
  Return whether the user is authorized to perform a given action on a DAG.
450
557
 
451
558
  :param method: the method to perform
452
- :param details: optional details about the DAG
453
- :param user: the user to perform the action on. If not provided (or None), it uses the current user
559
+ :param details: details about the DAG
560
+ :param user: the user to performing the action
454
561
 
455
562
  :meta private:
456
563
  """
@@ -468,15 +575,15 @@ class FabAuthManager(BaseAuthManager[User]):
468
575
  def _is_authorized_dag_run(
469
576
  self,
470
577
  method: ResourceMethod,
471
- details: DagDetails | None = None,
472
- user: BaseUser | None = None,
578
+ details: DagDetails | None,
579
+ user: User,
473
580
  ) -> bool:
474
581
  """
475
582
  Return whether the user is authorized to perform a given action on a DAG Run.
476
583
 
477
584
  :param method: the method to perform
478
- :param details: optional, details about the DAG
479
- :param user: optional, the user to perform the action on. If not provided, it uses the current user
585
+ :param details: details about the DAG
586
+ :param user: the user to performing the action
480
587
 
481
588
  :meta private:
482
589
  """
@@ -532,7 +639,7 @@ class FabAuthManager(BaseAuthManager[User]):
532
639
  return getattr(permissions, "resource_name_for_dag")(root_dag_id)
533
640
 
534
641
  @staticmethod
535
- def _get_user_permissions(user: BaseUser):
642
+ def _get_user_permissions(user: User):
536
643
  """
537
644
  Return the user permissions.
538
645
 
@@ -569,11 +676,7 @@ class FabAuthManager(BaseAuthManager[User]):
569
676
  # Otherwise, when the name of a view or menu is changed, the framework
570
677
  # will add the new Views and Menus names to the backend, but will not
571
678
  # delete the old ones.
572
- if Version(Version(version).base_version) >= Version("3.0.0"):
573
- fallback = None
574
- else:
575
- fallback = conf.getboolean("webserver", "UPDATE_FAB_PERMS")
576
- if conf.getboolean("fab", "UPDATE_FAB_PERMS", fallback=fallback):
679
+ if conf.getboolean("fab", "UPDATE_FAB_PERMS"):
577
680
  self.security_manager.sync_roles()
578
681
 
579
682
 
@@ -44,7 +44,7 @@ from sqlalchemy import (
44
44
  from sqlalchemy.orm import backref, declared_attr, registry, relationship
45
45
 
46
46
  from airflow import __version__ as airflow_version
47
- from airflow.auth.managers.models.base_user import BaseUser
47
+ from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
48
48
  from airflow.models.base import _get_schema, naming_convention
49
49
 
50
50
  if TYPE_CHECKING:
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  from flask import current_app
21
21
  from flask_login import AnonymousUserMixin
22
22
 
23
- from airflow.auth.managers.models.base_user import BaseUser
23
+ from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
24
24
 
25
25
 
26
26
  class AnonymousUser(AnonymousUserMixin, BaseUser):
@@ -31,6 +31,20 @@ _REVISION_HEADS_MAP: dict[str, str] = {
31
31
  }
32
32
 
33
33
 
34
+ def _get_flask_db(sql_database_uri):
35
+ from flask import Flask
36
+ from flask_sqlalchemy import SQLAlchemy
37
+
38
+ from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
39
+
40
+ flask_app = Flask(__name__)
41
+ flask_app.config["SQLALCHEMY_DATABASE_URI"] = sql_database_uri
42
+ flask_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
43
+ db = SQLAlchemy(flask_app)
44
+ AirflowDatabaseSessionInterface(app=flask_app, db=db, table="session", key_prefix="")
45
+ return db
46
+
47
+
34
48
  class FABDBManager(BaseDBManager):
35
49
  """Manages FAB database."""
36
50
 
@@ -40,6 +54,10 @@ class FABDBManager(BaseDBManager):
40
54
  alembic_file = (PACKAGE_DIR / "alembic.ini").as_posix()
41
55
  supports_table_dropping = True
42
56
 
57
+ def create_db_from_orm(self):
58
+ super().create_db_from_orm()
59
+ _get_flask_db(settings.SQL_ALCHEMY_CONN).create_all()
60
+
43
61
  def upgradedb(self, to_revision=None, from_revision=None, show_sql_only=False):
44
62
  """Upgrade the database."""
45
63
  if from_revision and not show_sql_only:
@@ -68,11 +86,6 @@ class FABDBManager(BaseDBManager):
68
86
  _offline_migration(command.upgrade, config, f"{from_revision}:{to_revision}")
69
87
  return # only running sql; our job is done
70
88
 
71
- if not self.get_current_revision():
72
- # New DB; initialize and exit
73
- self.initdb()
74
- return
75
-
76
89
  command.upgrade(config, revision=to_revision or "heads")
77
90
 
78
91
  def downgrade(self, to_revision, from_revision=None, show_sql_only=False):
@@ -104,3 +117,7 @@ class FABDBManager(BaseDBManager):
104
117
  else:
105
118
  self.log.info("Applying FAB downgrade migrations.")
106
119
  command.downgrade(config, revision=to_revision, sql=show_sql_only)
120
+
121
+ def drop_tables(self, connection):
122
+ super().drop_tables(connection)
123
+ _get_flask_db(settings.SQL_ALCHEMY_CONN).drop_all()
@@ -687,12 +687,21 @@ components:
687
687
  Basic:
688
688
  type: http
689
689
  scheme: basic
690
+ description: To authenticate FAB auth manager API requests, clients have the option to use basic
691
+ authentication. To learn more about FAB auth manager API authentication, please read
692
+ https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html#basic-authentication.
690
693
  GoogleOpenId:
691
694
  type: openIdConnect
692
695
  openIdConnectUrl: https://accounts.google.com/.well-known/openid-configuration
696
+ description: To authenticate FAB auth manager API requests, clients have the option to use Google OpenID.
697
+ To learn more about Google OpenID authentication, please read
698
+ https://airflow.apache.org/docs/apache-airflow-providers-google/stable/api-auth-backend/google-openid.html.
693
699
  Kerberos:
694
700
  type: http
695
701
  scheme: negotiate
702
+ description: To authenticate FAB auth manager API requests, clients have the option to use Kerberos
703
+ authentication. To learn more about FAB auth manager API authentication, please read
704
+ https://airflow.apache.org/docs/apache-airflow-providers-fab/stable/auth-manager/api-authentication.html#kerberos-authentication.
696
705
 
697
706
  tags:
698
707
  - name: Role