apache-airflow-providers-fab 2.0.0rc1__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airflow/providers/fab/LICENSE +0 -52
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -4
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -14
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -13
- airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +153 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +17 -4
- airflow/providers/fab/auth_manager/fab_auth_manager.py +222 -119
- airflow/providers/fab/auth_manager/models/__init__.py +1 -1
- airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
- airflow/providers/fab/auth_manager/models/db.py +22 -5
- airflow/providers/fab/auth_manager/openapi/v1.yaml +9 -0
- airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +89 -561
- airflow/providers/fab/auth_manager/views/permissions.py +1 -1
- airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
- airflow/providers/fab/auth_manager/views/user.py +1 -1
- airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
- airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
- airflow/providers/fab/get_provider_info.py +26 -15
- airflow/providers/fab/www/airflow_flask_app.py +31 -0
- airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
- airflow/providers/fab/www/api_connexion/parameters.py +131 -0
- airflow/providers/fab/www/api_connexion/security.py +84 -0
- airflow/providers/fab/www/api_connexion/types.py +30 -0
- airflow/providers/fab/www/app.py +34 -9
- airflow/providers/fab/www/auth.py +350 -0
- airflow/providers/fab/www/constants.py +28 -0
- airflow/providers/fab/www/extensions/init_appbuilder.py +54 -9
- airflow/providers/fab/www/extensions/init_jinja_globals.py +5 -3
- airflow/providers/fab/www/extensions/init_security.py +19 -0
- airflow/providers/fab/www/extensions/init_session.py +64 -0
- airflow/providers/fab/www/extensions/init_views.py +111 -1
- airflow/providers/fab/www/package-lock.json +4967 -16517
- airflow/providers/fab/www/package.json +25 -104
- airflow/providers/fab/www/security/__init__.py +17 -0
- airflow/providers/fab/www/security/permissions.py +126 -0
- airflow/providers/fab/www/security_appless.py +44 -0
- airflow/providers/fab/www/security_manager.py +122 -0
- airflow/providers/fab/www/session.py +41 -0
- airflow/providers/fab/www/static/css/flash.css +57 -0
- airflow/providers/fab/www/static/dist/48f0ea180c40270a5b05.png +1 -0
- airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
- airflow/providers/fab/www/static/dist/f7490d556a6c42e49ba4.png +1 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.css +18 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
- airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
- airflow/providers/fab/www/static/dist/manifest.json +20 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
- airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js +2 -0
- airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
- airflow/providers/fab/www/static/dist/oss-licenses.json +20 -0
- airflow/providers/fab/www/templates/airflow/main.html +10 -11
- airflow/providers/fab/www/templates/airflow/traceback.html +1 -5
- airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
- airflow/providers/fab/www/templates/appbuilder/navbar.html +7 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
- airflow/providers/fab/www/utils.py +272 -0
- airflow/providers/fab/www/views.py +129 -0
- airflow/providers/fab/www/webpack.config.js +5 -40
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/METADATA +24 -34
- apache_airflow_providers_fab-2.0.0rc2.dist-info/RECORD +125 -0
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/WHEEL +1 -1
- airflow/providers/fab/auth_manager/decorators/auth.py +0 -127
- apache_airflow_providers_fab-2.0.0rc1.dist-info/RECORD +0 -78
- /airflow/providers/fab/{auth_manager/decorators → www/api_connexion}/__init__.py +0 -0
- {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/entry_points.txt +0 -0
@@ -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
|
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,7 +44,7 @@ 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,
|
@@ -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
|
)
|
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.
|
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,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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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="/
|
177
|
-
options={
|
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["
|
270
|
+
return session.get(User, int(token["sub"]))
|
210
271
|
|
211
272
|
def serialize_user(self, user: User) -> dict[str, Any]:
|
212
|
-
return {"
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
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,
|
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,
|
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[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]
|
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
|
-
|
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
|
-
|
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:
|
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
|
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
|
446
|
-
user:
|
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:
|
453
|
-
:param user: the user to
|
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
|
472
|
-
user:
|
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:
|
479
|
-
:param 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:
|
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
|
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
|