apache-airflow-providers-fab 1.5.3__py3-none-any.whl → 2.0.0__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/__init__.py +3 -3
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +4 -5
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +5 -5
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +2 -2
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +15 -15
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +13 -14
- 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 +1 -3
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +12 -11
- airflow/providers/fab/auth_manager/fab_auth_manager.py +238 -126
- 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 +186 -655
- 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 +29 -34
- airflow/providers/fab/www/airflow_flask_app.py +31 -0
- airflow/providers/fab/www/api_connexion/__init__.py +17 -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 +120 -0
- airflow/providers/fab/www/auth.py +350 -0
- airflow/providers/fab/www/constants.py +28 -0
- airflow/providers/fab/www/extensions/__init__.py +16 -0
- airflow/providers/fab/www/extensions/init_appbuilder.py +606 -0
- airflow/providers/fab/www/extensions/init_jinja_globals.py +82 -0
- airflow/providers/fab/www/extensions/init_manifest_files.py +61 -0
- airflow/providers/fab/www/extensions/init_security.py +61 -0
- airflow/providers/fab/www/extensions/init_session.py +64 -0
- airflow/providers/fab/www/extensions/init_views.py +177 -0
- airflow/providers/fab/www/package-lock.json +8939 -0
- airflow/providers/fab/www/package.json +77 -0
- 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/bootstrap-theme.css +6215 -0
- airflow/providers/fab/www/static/css/flash.css +57 -0
- airflow/providers/fab/www/static/css/loading-dots.css +60 -0
- airflow/providers/fab/www/static/css/main.css +676 -0
- airflow/providers/fab/www/static/css/material-icons.css +84 -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/static/js/datetime_utils.js +134 -0
- airflow/providers/fab/www/static/js/main.js +324 -0
- airflow/providers/fab/www/static/sort_asc.png +0 -0
- airflow/providers/fab/www/static/sort_both.png +0 -0
- airflow/providers/fab/www/static/sort_desc.png +0 -0
- airflow/providers/fab/www/templates/airflow/_messages.html +30 -0
- airflow/providers/fab/www/templates/airflow/error.html +35 -0
- airflow/providers/fab/www/templates/airflow/main.html +78 -0
- airflow/providers/fab/www/templates/airflow/traceback.html +53 -0
- airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
- airflow/providers/fab/www/templates/appbuilder/index.html +20 -0
- airflow/providers/fab/www/templates/appbuilder/navbar.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_menu.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
- airflow/providers/fab/www/utils.py +288 -0
- airflow/providers/fab/www/views.py +129 -0
- airflow/providers/fab/www/webpack.config.js +213 -0
- {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/METADATA +29 -37
- apache_airflow_providers_fab-2.0.0.dist-info/RECORD +125 -0
- {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/WHEEL +1 -1
- airflow/providers/fab/auth_manager/decorators/auth.py +0 -126
- apache_airflow_providers_fab-1.5.3.dist-info/RECORD +0 -51
- /airflow/providers/fab/{auth_manager/decorators → www}/__init__.py +0 -0
- {apache_airflow_providers_fab-1.5.3.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -18,22 +18,25 @@
|
|
18
18
|
from __future__ import annotations
|
19
19
|
|
20
20
|
import argparse
|
21
|
-
import warnings
|
22
21
|
from functools import cached_property
|
23
22
|
from pathlib import Path
|
24
|
-
from typing import TYPE_CHECKING,
|
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
|
29
|
-
from
|
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.
|
35
|
-
from airflow.auth.managers.
|
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,13 +44,13 @@ from airflow.auth.managers.models.resource_details import (
|
|
41
44
|
PoolDetails,
|
42
45
|
VariableDetails,
|
43
46
|
)
|
44
|
-
from airflow.
|
47
|
+
from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem
|
45
48
|
from airflow.cli.cli_config import (
|
46
49
|
DefaultHelpParser,
|
47
50
|
GroupCommand,
|
48
51
|
)
|
49
52
|
from airflow.configuration import conf
|
50
|
-
from airflow.exceptions import AirflowConfigException, AirflowException
|
53
|
+
from airflow.exceptions import AirflowConfigException, AirflowException
|
51
54
|
from airflow.models import DagModel
|
52
55
|
from airflow.providers.fab.auth_manager.cli_commands.definition import (
|
53
56
|
DB_COMMANDS,
|
@@ -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.
|
60
|
-
from airflow.
|
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
|
)
|
85
|
-
from airflow.utils
|
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
|
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.
|
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
|
98
|
-
|
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
|
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,14 +153,36 @@ _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.AUDIT_LOG: RESOURCE_AUDIT_LOG,
|
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
|
-
class FabAuthManager(BaseAuthManager):
|
170
|
+
class FabAuthManager(BaseAuthManager[User]):
|
135
171
|
"""
|
136
172
|
Flask-AppBuilder auth manager.
|
137
173
|
|
138
174
|
This auth manager is responsible for providing a backward compatible user management experience to users.
|
139
175
|
"""
|
140
176
|
|
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", fallback="/")
|
185
|
+
|
141
186
|
@staticmethod
|
142
187
|
def get_cli_commands() -> list[CLICommand]:
|
143
188
|
"""Vends CLI commands to be included in Airflow CLI."""
|
@@ -161,6 +206,31 @@ class FabAuthManager(BaseAuthManager):
|
|
161
206
|
commands.append(GroupCommand(name="fab-db", help="Manage FAB", subcommands=DB_COMMANDS))
|
162
207
|
return commands
|
163
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
|
+
|
164
234
|
def get_api_endpoints(self) -> None | Blueprint:
|
165
235
|
folder = Path(__file__).parents[0].resolve() # this is airflow/auth/managers/fab/
|
166
236
|
with folder.joinpath("openapi", "v1.yaml").open() as f:
|
@@ -168,20 +238,16 @@ class FabAuthManager(BaseAuthManager):
|
|
168
238
|
return FlaskApi(
|
169
239
|
specification=specification,
|
170
240
|
resolver=_LazyResolver(),
|
171
|
-
base_path="/
|
172
|
-
options={
|
241
|
+
base_path="/fab/v1",
|
242
|
+
options={
|
243
|
+
"swagger_ui": SWAGGER_ENABLED,
|
244
|
+
"swagger_path": SWAGGER_BUNDLE.__fspath__(),
|
245
|
+
},
|
173
246
|
strict_validation=True,
|
174
247
|
validate_responses=True,
|
175
248
|
validator_map={"body": _CustomErrorRequestBodyValidator},
|
176
249
|
).blueprint
|
177
250
|
|
178
|
-
def get_user_display_name(self) -> str:
|
179
|
-
"""Return the user's display name associated to the user in session."""
|
180
|
-
user = self.get_user()
|
181
|
-
first_name = user.first_name.strip() if isinstance(user.first_name, str) else ""
|
182
|
-
last_name = user.last_name.strip() if isinstance(user.last_name, str) else ""
|
183
|
-
return f"{first_name} {last_name}".strip()
|
184
|
-
|
185
251
|
def get_user(self) -> User:
|
186
252
|
"""
|
187
253
|
Return the user associated to the user in session.
|
@@ -199,26 +265,28 @@ class FabAuthManager(BaseAuthManager):
|
|
199
265
|
|
200
266
|
return current_user
|
201
267
|
|
202
|
-
def
|
203
|
-
|
204
|
-
|
268
|
+
def deserialize_user(self, token: dict[str, Any]) -> User:
|
269
|
+
with create_session() as session:
|
270
|
+
return session.get(User, int(token["sub"]))
|
271
|
+
|
272
|
+
def serialize_user(self, user: User) -> dict[str, Any]:
|
273
|
+
return {"sub": str(user.id)}
|
205
274
|
|
206
275
|
def is_logged_in(self) -> bool:
|
207
276
|
"""Return whether the user is logged in."""
|
208
277
|
user = self.get_user()
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
)
|
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
|
+
)
|
215
283
|
|
216
284
|
def is_authorized_configuration(
|
217
285
|
self,
|
218
286
|
*,
|
219
287
|
method: ResourceMethod,
|
288
|
+
user: User,
|
220
289
|
details: ConfigurationDetails | None = None,
|
221
|
-
user: BaseUser | None = None,
|
222
290
|
) -> bool:
|
223
291
|
return self._is_authorized(method=method, resource_type=RESOURCE_CONFIG, user=user)
|
224
292
|
|
@@ -226,8 +294,8 @@ class FabAuthManager(BaseAuthManager):
|
|
226
294
|
self,
|
227
295
|
*,
|
228
296
|
method: ResourceMethod,
|
297
|
+
user: User,
|
229
298
|
details: ConnectionDetails | None = None,
|
230
|
-
user: BaseUser | None = None,
|
231
299
|
) -> bool:
|
232
300
|
return self._is_authorized(method=method, resource_type=RESOURCE_CONNECTION, user=user)
|
233
301
|
|
@@ -235,9 +303,9 @@ class FabAuthManager(BaseAuthManager):
|
|
235
303
|
self,
|
236
304
|
*,
|
237
305
|
method: ResourceMethod,
|
306
|
+
user: User,
|
238
307
|
access_entity: DagAccessEntity | None = None,
|
239
308
|
details: DagDetails | None = None,
|
240
|
-
user: BaseUser | None = None,
|
241
309
|
) -> bool:
|
242
310
|
"""
|
243
311
|
Return whether the user is authorized to access the dag.
|
@@ -254,9 +322,9 @@ class FabAuthManager(BaseAuthManager):
|
|
254
322
|
if no specific DAG is targeted, just check the sub entity.
|
255
323
|
|
256
324
|
:param method: The method to authorize.
|
325
|
+
:param user: The user performing the action.
|
257
326
|
:param access_entity: The dag access entity.
|
258
327
|
:param details: The dag details.
|
259
|
-
:param user: The user.
|
260
328
|
"""
|
261
329
|
if not access_entity:
|
262
330
|
# Scenario 1
|
@@ -272,84 +340,100 @@ class FabAuthManager(BaseAuthManager):
|
|
272
340
|
return False
|
273
341
|
|
274
342
|
return all(
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
+
)
|
278
348
|
for resource_type in resource_types
|
279
349
|
)
|
280
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
|
+
|
281
360
|
def is_authorized_asset(
|
282
|
-
self, *, method: ResourceMethod,
|
361
|
+
self, *, method: ResourceMethod, user: User, details: AssetDetails | None = None
|
283
362
|
) -> bool:
|
284
363
|
return self._is_authorized(method=method, resource_type=RESOURCE_ASSET, user=user)
|
285
364
|
|
286
|
-
def
|
287
|
-
self,
|
365
|
+
def is_authorized_asset_alias(
|
366
|
+
self,
|
367
|
+
*,
|
368
|
+
method: ResourceMethod,
|
369
|
+
user: User,
|
370
|
+
details: AssetAliasDetails | None = None,
|
288
371
|
) -> bool:
|
289
|
-
|
290
|
-
"is_authorized_dataset will be renamed as is_authorized_asset in Airflow 3 and will be removed when the minimum Airflow version is set to 3.0 for the fab provider",
|
291
|
-
AirflowProviderDeprecationWarning,
|
292
|
-
stacklevel=2,
|
293
|
-
)
|
294
|
-
return self.is_authorized_asset(method=method, user=user)
|
372
|
+
return self._is_authorized(method=method, resource_type=RESOURCE_ASSET_ALIAS, user=user)
|
295
373
|
|
296
374
|
def is_authorized_pool(
|
297
|
-
self, *, method: ResourceMethod,
|
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,
|
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:
|
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,
|
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:
|
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
|
415
|
+
def get_authorized_dag_ids(
|
323
416
|
self,
|
324
417
|
*,
|
325
|
-
|
326
|
-
|
418
|
+
user: User,
|
419
|
+
method: ResourceMethod = "GET",
|
327
420
|
session: Session = NEW_SESSION,
|
328
421
|
) -> set[str]:
|
329
|
-
if
|
330
|
-
|
331
|
-
|
332
|
-
if
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
-
|
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):
|
|
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]
|
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:
|
@@ -376,6 +460,9 @@ class FabAuthManager(BaseAuthManager):
|
|
376
460
|
FabAirflowSecurityManagerOverride,
|
377
461
|
)
|
378
462
|
|
463
|
+
if not self.appbuilder:
|
464
|
+
raise AirflowException("AppBuilder is not initialized.")
|
465
|
+
|
379
466
|
sm_from_config = self.appbuilder.get_app.config.get("SECURITY_MANAGER_CLASS")
|
380
467
|
if sm_from_config:
|
381
468
|
if not issubclass(sm_from_config, FabAirflowSecurityManagerOverride):
|
@@ -388,49 +475,72 @@ class FabAuthManager(BaseAuthManager):
|
|
388
475
|
|
389
476
|
def get_url_login(self, **kwargs) -> str:
|
390
477
|
"""Return the login page url."""
|
391
|
-
|
392
|
-
raise AirflowException("`auth_view` not defined in the security manager.")
|
393
|
-
if next_url := kwargs.get("next_url"):
|
394
|
-
return url_for(f"{self.security_manager.auth_view.endpoint}.login", next=next_url)
|
395
|
-
else:
|
396
|
-
return url_for(f"{self.security_manager.auth_view.endpoint}.login")
|
478
|
+
return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login/")
|
397
479
|
|
398
|
-
def get_url_logout(self):
|
480
|
+
def get_url_logout(self) -> str | None:
|
399
481
|
"""Return the logout page url."""
|
400
|
-
|
401
|
-
raise AirflowException("`auth_view` not defined in the security manager.")
|
402
|
-
return url_for(f"{self.security_manager.auth_view.endpoint}.logout")
|
403
|
-
|
404
|
-
def get_url_user_profile(self) -> str | None:
|
405
|
-
"""Return the url to a page displaying info about the current user."""
|
406
|
-
if not self.security_manager.user_view or self.appbuilder.get_app.config.get(
|
407
|
-
"AUTH_ROLE_PUBLIC", None
|
408
|
-
):
|
409
|
-
return None
|
410
|
-
return url_for(f"{self.security_manager.user_view.endpoint}.userinfo")
|
482
|
+
return urljoin(self.apiserver_endpoint, f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/logout/")
|
411
483
|
|
412
484
|
def register_views(self) -> None:
|
413
485
|
self.security_manager.register_views()
|
414
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
|
+
|
415
528
|
def _is_authorized(
|
416
529
|
self,
|
417
530
|
*,
|
418
531
|
method: ResourceMethod,
|
419
532
|
resource_type: str,
|
420
|
-
user:
|
533
|
+
user: User,
|
421
534
|
) -> bool:
|
422
535
|
"""
|
423
536
|
Return whether the user is authorized to perform a given action.
|
424
537
|
|
425
538
|
:param method: the method to perform
|
426
539
|
:param resource_type: the type of resource the user attempts to perform the action on
|
427
|
-
:param user: the user to
|
540
|
+
:param user: the user to performing the action
|
428
541
|
|
429
542
|
:meta private:
|
430
543
|
"""
|
431
|
-
if not user:
|
432
|
-
user = self.get_user()
|
433
|
-
|
434
544
|
fab_action = self._get_fab_action(method)
|
435
545
|
user_permissions = self._get_user_permissions(user)
|
436
546
|
|
@@ -439,15 +549,15 @@ class FabAuthManager(BaseAuthManager):
|
|
439
549
|
def _is_authorized_dag(
|
440
550
|
self,
|
441
551
|
method: ResourceMethod,
|
442
|
-
details: DagDetails | None
|
443
|
-
user:
|
552
|
+
details: DagDetails | None,
|
553
|
+
user: User,
|
444
554
|
) -> bool:
|
445
555
|
"""
|
446
556
|
Return whether the user is authorized to perform a given action on a DAG.
|
447
557
|
|
448
558
|
:param method: the method to perform
|
449
|
-
:param details:
|
450
|
-
:param user: the user to
|
559
|
+
:param details: details about the DAG
|
560
|
+
:param user: the user to performing the action
|
451
561
|
|
452
562
|
:meta private:
|
453
563
|
"""
|
@@ -465,15 +575,15 @@ class FabAuthManager(BaseAuthManager):
|
|
465
575
|
def _is_authorized_dag_run(
|
466
576
|
self,
|
467
577
|
method: ResourceMethod,
|
468
|
-
details: DagDetails | None
|
469
|
-
user:
|
578
|
+
details: DagDetails | None,
|
579
|
+
user: User,
|
470
580
|
) -> bool:
|
471
581
|
"""
|
472
582
|
Return whether the user is authorized to perform a given action on a DAG Run.
|
473
583
|
|
474
584
|
:param method: the method to perform
|
475
|
-
:param details:
|
476
|
-
:param user:
|
585
|
+
:param details: details about the DAG
|
586
|
+
:param user: the user to performing the action
|
477
587
|
|
478
588
|
:meta private:
|
479
589
|
"""
|
@@ -529,7 +639,7 @@ class FabAuthManager(BaseAuthManager):
|
|
529
639
|
return getattr(permissions, "resource_name_for_dag")(root_dag_id)
|
530
640
|
|
531
641
|
@staticmethod
|
532
|
-
def _get_user_permissions(user:
|
642
|
+
def _get_user_permissions(user: User):
|
533
643
|
"""
|
534
644
|
Return the user permissions.
|
535
645
|
|
@@ -537,6 +647,9 @@ class FabAuthManager(BaseAuthManager):
|
|
537
647
|
|
538
648
|
:meta private:
|
539
649
|
"""
|
650
|
+
# If the user gets deleted while being logged in
|
651
|
+
if not user:
|
652
|
+
return []
|
540
653
|
return getattr(user, "perms") or []
|
541
654
|
|
542
655
|
def _get_root_dag_id(self, dag_id: str) -> str:
|
@@ -547,6 +660,9 @@ class FabAuthManager(BaseAuthManager):
|
|
547
660
|
|
548
661
|
:meta private:
|
549
662
|
"""
|
663
|
+
if not self.appbuilder:
|
664
|
+
raise AirflowException("AppBuilder is not initialized.")
|
665
|
+
|
550
666
|
if "." in dag_id and hasattr(DagModel, "root_dag_id"):
|
551
667
|
return self.appbuilder.get_session.scalar(
|
552
668
|
select(DagModel.dag_id, DagModel.root_dag_id).where(DagModel.dag_id == dag_id).limit(1)
|
@@ -563,11 +679,7 @@ class FabAuthManager(BaseAuthManager):
|
|
563
679
|
# Otherwise, when the name of a view or menu is changed, the framework
|
564
680
|
# will add the new Views and Menus names to the backend, but will not
|
565
681
|
# delete the old ones.
|
566
|
-
if
|
567
|
-
fallback = None
|
568
|
-
else:
|
569
|
-
fallback = conf.getboolean("webserver", "UPDATE_FAB_PERMS")
|
570
|
-
if conf.getboolean("fab", "UPDATE_FAB_PERMS", fallback=fallback):
|
682
|
+
if conf.getboolean("fab", "UPDATE_FAB_PERMS"):
|
571
683
|
self.security_manager.sync_roles()
|
572
684
|
|
573
685
|
|
@@ -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):
|