apache-airflow-providers-fab 3.0.1__py3-none-any.whl → 3.1.1rc1__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/__init__.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +2 -2
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +0 -7
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/roles.py +63 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +416 -16
- airflow/providers/fab/auth_manager/api_fastapi/parameters.py +55 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +37 -5
- airflow/providers/fab/auth_manager/api_fastapi/routes/roles.py +137 -0
- airflow/providers/fab/auth_manager/api_fastapi/security.py +32 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +12 -25
- airflow/providers/fab/auth_manager/api_fastapi/services/roles.py +158 -0
- airflow/providers/fab/auth_manager/api_fastapi/sorting.py +49 -0
- airflow/providers/fab/auth_manager/cli_commands/permissions_command.py +6 -2
- airflow/providers/fab/auth_manager/fab_auth_manager.py +33 -3
- airflow/providers/fab/auth_manager/models/__init__.py +3 -8
- airflow/providers/fab/auth_manager/models/db.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +60 -17
- airflow/providers/fab/version_compat.py +1 -0
- airflow/providers/fab/www/api_connexion/parameters.py +1 -46
- airflow/providers/fab/www/app.py +13 -10
- airflow/providers/fab/www/extensions/init_appbuilder.py +5 -2
- airflow/providers/fab/www/extensions/init_security.py +1 -1
- airflow/providers/fab/www/extensions/init_views.py +11 -7
- airflow/providers/fab/www/package-lock.json +417 -265
- airflow/providers/fab/www/package.json +13 -10
- airflow/providers/fab/www/session.py +5 -8
- airflow/providers/fab/www/static/dist/{743.935ed3d26e56ed8f63d3.js → 743.0c0bf201ae17e66a9a3f.js} +1 -1
- airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.js → main.bc1f701c3d133e2a3bab.js} +1 -1
- airflow/providers/fab/www/static/dist/manifest.json +13 -13
- airflow/providers/fab/www/views.py +18 -14
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/METADATA +15 -14
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/RECORD +51 -45
- /airflow/providers/fab/migrations/versions/{0001_1_4_0_create_ab_tables_if_missing.py → 0000_1_4_0_create_ab_tables_if_missing.py} +0 -0
- /airflow/providers/fab/www/static/dist/{743.935ed3d26e56ed8f63d3.js.LICENSE.txt → 743.0c0bf201ae17e66a9a3f.js.LICENSE.txt} +0 -0
- /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ff5a35f322070b094aa2.css → airflowDefaultTheme.ef6fc04c9b6920cd75c9.css} +0 -0
- /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ff5a35f322070b094aa2.js → airflowDefaultTheme.ef6fc04c9b6920cd75c9.js} +0 -0
- /airflow/providers/fab/www/static/dist/{flash.5583a9e0cf11f2be93da.css → flash.eaaf777ec1b3628cf7be.css} +0 -0
- /airflow/providers/fab/www/static/dist/{flash.5583a9e0cf11f2be93da.js → flash.eaaf777ec1b3628cf7be.js} +0 -0
- /airflow/providers/fab/www/static/dist/{loadingDots.2e5f555f0753107b0300.css → loadingDots.76f4332c0a932c3dc08f.css} +0 -0
- /airflow/providers/fab/www/static/dist/{loadingDots.2e5f555f0753107b0300.js → loadingDots.76f4332c0a932c3dc08f.js} +0 -0
- /airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.css → main.bc1f701c3d133e2a3bab.css} +0 -0
- /airflow/providers/fab/www/static/dist/{main.3cf3be1a0c5439bb640d.js.LICENSE.txt → main.bc1f701c3d133e2a3bab.js.LICENSE.txt} +0 -0
- /airflow/providers/fab/www/static/dist/{materialIcons.3e67dd6fbfcc4f3b5105.css → materialIcons.ad07a489b2f0fc1a96bf.css} +0 -0
- /airflow/providers/fab/www/static/dist/{materialIcons.3e67dd6fbfcc4f3b5105.js → materialIcons.ad07a489b2f0fc1a96bf.js} +0 -0
- /airflow/providers/fab/www/static/dist/{moment.9baee5ec3d7639a10897.js → moment.5b85b4f6be2fe9c405ac.js} +0 -0
- /airflow/providers/fab/www/static/dist/{runtime.6ad9da077ea169d60db9.js → runtime.254c277d91ce3ac79c64.js} +0 -0
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
- {airflow/providers/fab → apache_airflow_providers_fab-3.1.1rc1.dist-info/licenses}/LICENSE +0 -0
- {apache_airflow_providers_fab-3.0.1.dist-info → apache_airflow_providers_fab-3.1.1rc1.dist-info}/licenses/NOTICE +0 -0
|
@@ -67,7 +67,7 @@ from sqlalchemy.orm import joinedload
|
|
|
67
67
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
68
68
|
|
|
69
69
|
from airflow.configuration import conf
|
|
70
|
-
from airflow.
|
|
70
|
+
from airflow.providers.common.compat.sdk import AirflowException
|
|
71
71
|
from airflow.providers.fab.auth_manager.models import (
|
|
72
72
|
Action,
|
|
73
73
|
Group,
|
|
@@ -101,7 +101,6 @@ from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
|
|
|
101
101
|
from airflow.providers.fab.www.security import permissions
|
|
102
102
|
from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
|
103
103
|
from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
|
|
104
|
-
from airflow.security.permissions import RESOURCE_BACKFILL
|
|
105
104
|
|
|
106
105
|
if TYPE_CHECKING:
|
|
107
106
|
from airflow.providers.fab.www.security.permissions import (
|
|
@@ -109,7 +108,7 @@ if TYPE_CHECKING:
|
|
|
109
108
|
RESOURCE_ASSET_ALIAS,
|
|
110
109
|
)
|
|
111
110
|
from airflow.sdk import DAG
|
|
112
|
-
from airflow.serialization.
|
|
111
|
+
from airflow.serialization.definitions.dag import SerializedDAG
|
|
113
112
|
else:
|
|
114
113
|
from airflow.providers.common.compat.security.permissions import (
|
|
115
114
|
RESOURCE_ASSET,
|
|
@@ -124,11 +123,17 @@ if AIRFLOW_V_3_1_PLUS:
|
|
|
124
123
|
with create_session() as session:
|
|
125
124
|
yield from DBDagBag().iter_all_latest_version_dags(session=session)
|
|
126
125
|
else:
|
|
127
|
-
|
|
126
|
+
try:
|
|
127
|
+
from airflow.models.dagbag import DagBag
|
|
128
|
+
except (ImportError, AttributeError):
|
|
129
|
+
DagBag = None
|
|
128
130
|
|
|
129
131
|
def _iter_dags() -> Iterable[DAG | SerializedDAG]:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
if DagBag is None:
|
|
133
|
+
return []
|
|
134
|
+
dagbag = DagBag(read_dags_from_db=True)
|
|
135
|
+
if hasattr(dagbag, "collect_dags_from_db"):
|
|
136
|
+
dagbag.collect_dags_from_db()
|
|
132
137
|
return dagbag.dags.values()
|
|
133
138
|
|
|
134
139
|
|
|
@@ -230,7 +235,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
230
235
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
|
|
231
236
|
(permissions.ACTION_CAN_READ, RESOURCE_ASSET),
|
|
232
237
|
(permissions.ACTION_CAN_READ, RESOURCE_ASSET_ALIAS),
|
|
233
|
-
(permissions.ACTION_CAN_READ, RESOURCE_BACKFILL),
|
|
238
|
+
(permissions.ACTION_CAN_READ, permissions.RESOURCE_BACKFILL),
|
|
234
239
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
|
|
235
240
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_POOL),
|
|
236
241
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR),
|
|
@@ -302,9 +307,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
302
307
|
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
|
|
303
308
|
(permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
|
|
304
309
|
(permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
|
|
305
|
-
(permissions.ACTION_CAN_CREATE, RESOURCE_BACKFILL),
|
|
306
|
-
(permissions.ACTION_CAN_EDIT, RESOURCE_BACKFILL),
|
|
307
|
-
(permissions.ACTION_CAN_DELETE, RESOURCE_BACKFILL),
|
|
310
|
+
(permissions.ACTION_CAN_CREATE, permissions.RESOURCE_BACKFILL),
|
|
311
|
+
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_BACKFILL),
|
|
312
|
+
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_BACKFILL),
|
|
308
313
|
]
|
|
309
314
|
# [END security_op_perms]
|
|
310
315
|
|
|
@@ -727,6 +732,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
727
732
|
"""The JMESPATH role to use for user registration."""
|
|
728
733
|
return current_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
|
|
729
734
|
|
|
735
|
+
@property
|
|
736
|
+
def auth_remote_user_env_var(self) -> str:
|
|
737
|
+
return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
|
|
738
|
+
|
|
730
739
|
@property
|
|
731
740
|
def auth_username_ci(self):
|
|
732
741
|
"""Get the auth username for CI."""
|
|
@@ -1039,7 +1048,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1039
1048
|
self.remove_permission_from_role(role, perm)
|
|
1040
1049
|
|
|
1041
1050
|
# Adding the access control permissions
|
|
1042
|
-
for rolename,
|
|
1051
|
+
for rolename, resource_actions_raw in access_control.items():
|
|
1043
1052
|
role = self.find_role(rolename)
|
|
1044
1053
|
if not role:
|
|
1045
1054
|
raise AirflowException(
|
|
@@ -1047,9 +1056,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1047
1056
|
f"'{rolename}', but that role does not exist"
|
|
1048
1057
|
)
|
|
1049
1058
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1059
|
+
# Support for old-style access_control where only the actions are specified
|
|
1060
|
+
resource_actions = (
|
|
1061
|
+
resource_actions_raw
|
|
1062
|
+
if isinstance(resource_actions_raw, dict)
|
|
1063
|
+
else {permissions.RESOURCE_DAG: set(resource_actions_raw)}
|
|
1064
|
+
)
|
|
1053
1065
|
|
|
1054
1066
|
for resource_name, actions in resource_actions.items():
|
|
1055
1067
|
if resource_name not in self.RESOURCE_DETAILS_MAP:
|
|
@@ -1643,7 +1655,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1643
1655
|
return perm
|
|
1644
1656
|
resource = self.create_resource(resource_name)
|
|
1645
1657
|
if resource is None:
|
|
1646
|
-
log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW,
|
|
1658
|
+
log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW, "Resource creation failed %s", resource_name)
|
|
1647
1659
|
return None
|
|
1648
1660
|
action = self.create_action(action_name)
|
|
1649
1661
|
perm = self.permission_model()
|
|
@@ -2006,7 +2018,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2006
2018
|
if _provider["name"] == provider:
|
|
2007
2019
|
return _provider.get("token_secret", "oauth_token_secret")
|
|
2008
2020
|
|
|
2009
|
-
def auth_user_oauth(self, userinfo):
|
|
2021
|
+
def auth_user_oauth(self, userinfo, rotate_session_id=True):
|
|
2010
2022
|
"""
|
|
2011
2023
|
Authenticate user with OAuth.
|
|
2012
2024
|
|
|
@@ -2060,7 +2072,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2060
2072
|
|
|
2061
2073
|
# LOGIN SUCCESS (only if user is now registered)
|
|
2062
2074
|
if user:
|
|
2063
|
-
|
|
2075
|
+
if rotate_session_id:
|
|
2076
|
+
self._rotate_session_id()
|
|
2064
2077
|
self.update_user_auth_stat(user)
|
|
2065
2078
|
return user
|
|
2066
2079
|
return None
|
|
@@ -2204,6 +2217,36 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2204
2217
|
# decode - if empty string, default to fallback, otherwise take first element
|
|
2205
2218
|
return raw_value[0].decode("utf-8") or fallback
|
|
2206
2219
|
|
|
2220
|
+
def auth_user_remote_user(self, username):
|
|
2221
|
+
"""
|
|
2222
|
+
REMOTE_USER user Authentication.
|
|
2223
|
+
|
|
2224
|
+
:param username: user's username for remote auth
|
|
2225
|
+
"""
|
|
2226
|
+
user = self.find_user(username=username)
|
|
2227
|
+
|
|
2228
|
+
# User does not exist, create one if auto user registration.
|
|
2229
|
+
if user is None and self.auth_user_registration:
|
|
2230
|
+
user = self.add_user(
|
|
2231
|
+
# All we have is REMOTE_USER, so we set
|
|
2232
|
+
# the other fields to blank.
|
|
2233
|
+
username=username,
|
|
2234
|
+
first_name=username,
|
|
2235
|
+
last_name="-",
|
|
2236
|
+
email=username + "@email.notfound",
|
|
2237
|
+
role=self.find_role(self.auth_user_registration_role),
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
# If user does not exist on the DB and not auto user registration,
|
|
2241
|
+
# or user is inactive, go away.
|
|
2242
|
+
elif user is None or (not user.is_active):
|
|
2243
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
2244
|
+
return None
|
|
2245
|
+
|
|
2246
|
+
self._rotate_session_id()
|
|
2247
|
+
self.update_user_auth_stat(user)
|
|
2248
|
+
return user
|
|
2249
|
+
|
|
2207
2250
|
"""
|
|
2208
2251
|
---------------
|
|
2209
2252
|
Private methods
|
|
@@ -34,3 +34,4 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
|
|
|
34
34
|
|
|
35
35
|
AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
|
|
36
36
|
AIRFLOW_V_3_1_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 1)
|
|
37
|
+
AIRFLOW_V_3_2_PLUS = get_base_airflow_version_tuple() >= (3, 2, 0)
|
|
@@ -17,21 +17,16 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
-
from collections.abc import Callable
|
|
20
|
+
from collections.abc import Callable
|
|
21
21
|
from functools import wraps
|
|
22
22
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
23
23
|
|
|
24
|
-
from pendulum.parsing import ParserError
|
|
25
|
-
from sqlalchemy import text
|
|
26
|
-
|
|
27
24
|
from airflow.configuration import conf
|
|
28
25
|
from airflow.providers.fab.www.api_connexion.exceptions import BadRequest
|
|
29
|
-
from airflow.utils import timezone
|
|
30
26
|
|
|
31
27
|
if TYPE_CHECKING:
|
|
32
28
|
from datetime import datetime
|
|
33
29
|
|
|
34
|
-
from sqlalchemy.sql import Select
|
|
35
30
|
|
|
36
31
|
log = logging.getLogger(__name__)
|
|
37
32
|
|
|
@@ -42,24 +37,6 @@ def validate_istimezone(value: datetime) -> None:
|
|
|
42
37
|
raise BadRequest("Invalid datetime format", detail="Naive datetime is disallowed")
|
|
43
38
|
|
|
44
39
|
|
|
45
|
-
def format_datetime(value: str) -> datetime:
|
|
46
|
-
"""
|
|
47
|
-
Format datetime objects.
|
|
48
|
-
|
|
49
|
-
Datetime format parser for args since connexion doesn't parse datetimes
|
|
50
|
-
https://github.com/zalando/connexion/issues/476
|
|
51
|
-
|
|
52
|
-
This should only be used within connection views because it raises 400
|
|
53
|
-
"""
|
|
54
|
-
value = value.strip()
|
|
55
|
-
if value[-1] != "Z":
|
|
56
|
-
value = value.replace(" ", "+")
|
|
57
|
-
try:
|
|
58
|
-
return timezone.parse(value)
|
|
59
|
-
except (ParserError, TypeError) as err:
|
|
60
|
-
raise BadRequest("Incorrect datetime argument", detail=str(err))
|
|
61
|
-
|
|
62
|
-
|
|
63
40
|
def check_limit(value: int) -> int:
|
|
64
41
|
"""
|
|
65
42
|
Check the limit does not exceed configured value.
|
|
@@ -107,25 +84,3 @@ def format_parameters(params_formatters: dict[str, Callable[[Any], Any]]) -> Cal
|
|
|
107
84
|
return cast("T", wrapped_function)
|
|
108
85
|
|
|
109
86
|
return format_parameters_decorator
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def apply_sorting(
|
|
113
|
-
query: Select,
|
|
114
|
-
order_by: str,
|
|
115
|
-
to_replace: dict[str, str] | None = None,
|
|
116
|
-
allowed_attrs: Container[str] | None = None,
|
|
117
|
-
) -> Select:
|
|
118
|
-
"""Apply sorting to query."""
|
|
119
|
-
lstriped_orderby = order_by.lstrip("-")
|
|
120
|
-
if allowed_attrs and lstriped_orderby not in allowed_attrs:
|
|
121
|
-
raise BadRequest(
|
|
122
|
-
detail=f"Ordering with '{lstriped_orderby}' is disallowed or "
|
|
123
|
-
f"the attribute does not exist on the model"
|
|
124
|
-
)
|
|
125
|
-
if to_replace:
|
|
126
|
-
lstriped_orderby = to_replace.get(lstriped_orderby, lstriped_orderby)
|
|
127
|
-
if order_by[0] == "-":
|
|
128
|
-
order_by = f"{lstriped_orderby} desc"
|
|
129
|
-
else:
|
|
130
|
-
order_by = f"{lstriped_orderby} asc"
|
|
131
|
-
return query.order_by(text(order_by))
|
airflow/providers/fab/www/app.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
from datetime import timedelta
|
|
21
|
+
from functools import cache
|
|
21
22
|
from os.path import isabs
|
|
22
23
|
|
|
23
24
|
from flask import Flask
|
|
@@ -30,7 +31,7 @@ from airflow.api_fastapi.app import get_auth_manager
|
|
|
30
31
|
from airflow.configuration import conf
|
|
31
32
|
from airflow.exceptions import AirflowConfigException
|
|
32
33
|
from airflow.logging_config import configure_logging
|
|
33
|
-
from airflow.providers.fab.www.extensions.init_appbuilder import
|
|
34
|
+
from airflow.providers.fab.www.extensions.init_appbuilder import AirflowAppBuilder
|
|
34
35
|
from airflow.providers.fab.www.extensions.init_jinja_globals import init_jinja_globals
|
|
35
36
|
from airflow.providers.fab.www.extensions.init_manifest_files import configure_manifest_files
|
|
36
37
|
from airflow.providers.fab.www.extensions.init_security import init_api_auth
|
|
@@ -44,8 +45,6 @@ from airflow.providers.fab.www.extensions.init_views import (
|
|
|
44
45
|
from airflow.providers.fab.www.extensions.init_wsgi_middlewares import init_wsgi_middleware
|
|
45
46
|
from airflow.providers.fab.www.utils import get_session_lifetime_config
|
|
46
47
|
|
|
47
|
-
app: Flask | None = None
|
|
48
|
-
|
|
49
48
|
# Initializes at the module level, so plugins can access it.
|
|
50
49
|
# See: /docs/plugins.rst
|
|
51
50
|
csrf = CSRFProtect()
|
|
@@ -85,6 +84,8 @@ def create_app(enable_plugins: bool):
|
|
|
85
84
|
csrf.init_app(flask_app)
|
|
86
85
|
|
|
87
86
|
db = SQLAlchemy(flask_app)
|
|
87
|
+
if settings.Session is None:
|
|
88
|
+
raise RuntimeError("Session not configured. Call configure_orm() first.")
|
|
88
89
|
db.session = settings.Session
|
|
89
90
|
|
|
90
91
|
configure_logging()
|
|
@@ -92,7 +93,12 @@ def create_app(enable_plugins: bool):
|
|
|
92
93
|
init_api_auth(flask_app)
|
|
93
94
|
|
|
94
95
|
with flask_app.app_context():
|
|
95
|
-
|
|
96
|
+
AirflowAppBuilder(
|
|
97
|
+
app=flask_app,
|
|
98
|
+
session=db.session(),
|
|
99
|
+
base_template="airflow/main.html",
|
|
100
|
+
enable_plugins=enable_plugins,
|
|
101
|
+
)
|
|
96
102
|
init_error_handlers(flask_app)
|
|
97
103
|
# In two scenarios a Flask application can be created:
|
|
98
104
|
# - To support Airflow 2 plugins relying on Flask (``enable_plugins`` is True)
|
|
@@ -112,15 +118,12 @@ def create_app(enable_plugins: bool):
|
|
|
112
118
|
return flask_app
|
|
113
119
|
|
|
114
120
|
|
|
121
|
+
@cache
|
|
115
122
|
def cached_app():
|
|
116
123
|
"""Return cached instance of Airflow WWW app."""
|
|
117
|
-
|
|
118
|
-
if not app:
|
|
119
|
-
app = create_app()
|
|
120
|
-
return app
|
|
124
|
+
return create_app()
|
|
121
125
|
|
|
122
126
|
|
|
123
127
|
def purge_cached_app():
|
|
124
128
|
"""Remove the cached version of the app in global state."""
|
|
125
|
-
|
|
126
|
-
app = None
|
|
129
|
+
cached_app.cache_clear()
|
|
@@ -42,7 +42,7 @@ from airflow import settings
|
|
|
42
42
|
from airflow.api_fastapi.app import create_auth_manager, get_auth_manager
|
|
43
43
|
from airflow.configuration import conf
|
|
44
44
|
from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
|
45
|
-
from airflow.providers.fab.www.views import FabIndexView
|
|
45
|
+
from airflow.providers.fab.www.views import FabIndexView, redirect
|
|
46
46
|
|
|
47
47
|
if TYPE_CHECKING:
|
|
48
48
|
from flask import Flask
|
|
@@ -216,6 +216,7 @@ class AirflowAppBuilder:
|
|
|
216
216
|
from airflow.providers.fab.www.views import get_safe_url
|
|
217
217
|
|
|
218
218
|
fab_sec_views.get_safe_redirect = get_safe_url
|
|
219
|
+
fab_sec_views.redirect = redirect
|
|
219
220
|
|
|
220
221
|
def _init_extension(self, app):
|
|
221
222
|
app.appbuilder = self
|
|
@@ -473,7 +474,7 @@ class AirflowAppBuilder:
|
|
|
473
474
|
baseview=baseview,
|
|
474
475
|
cond=cond,
|
|
475
476
|
)
|
|
476
|
-
if
|
|
477
|
+
if current_app:
|
|
477
478
|
self._add_permissions_menu(name)
|
|
478
479
|
if category:
|
|
479
480
|
self._add_permissions_menu(category)
|
|
@@ -592,6 +593,8 @@ class AirflowAppBuilder:
|
|
|
592
593
|
|
|
593
594
|
def init_appbuilder(app: Flask, enable_plugins: bool) -> AirflowAppBuilder:
|
|
594
595
|
"""Init `Flask App Builder <https://flask-appbuilder.readthedocs.io/en/latest/>`__."""
|
|
596
|
+
if settings.Session is None:
|
|
597
|
+
raise RuntimeError("Session not configured. Call configure_orm() first.")
|
|
595
598
|
return AirflowAppBuilder(
|
|
596
599
|
app=app,
|
|
597
600
|
session=settings.Session(),
|
|
@@ -20,7 +20,7 @@ import logging
|
|
|
20
20
|
from importlib import import_module
|
|
21
21
|
|
|
22
22
|
from airflow.configuration import conf
|
|
23
|
-
from airflow.
|
|
23
|
+
from airflow.providers.common.compat.sdk import AirflowException
|
|
24
24
|
|
|
25
25
|
log = logging.getLogger(__name__)
|
|
26
26
|
|
|
@@ -26,7 +26,7 @@ from connexion.exceptions import BadRequestProblem, ProblemException
|
|
|
26
26
|
from flask import request
|
|
27
27
|
|
|
28
28
|
from airflow.api_fastapi.app import get_auth_manager
|
|
29
|
-
from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
|
|
29
|
+
from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS, AIRFLOW_V_3_2_PLUS
|
|
30
30
|
from airflow.providers.fab.www.api_connexion.exceptions import common_error_handler
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
@@ -102,11 +102,17 @@ def init_plugins(app):
|
|
|
102
102
|
"""Integrate Flask and FAB with plugins."""
|
|
103
103
|
from airflow import plugins_manager
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
if AIRFLOW_V_3_2_PLUS:
|
|
106
|
+
blueprints, appbuilder_views, appbuilder_menu_links = plugins_manager.get_flask_plugins()
|
|
107
|
+
else:
|
|
108
|
+
plugins_manager.initialize_flask_plugins() # type: ignore
|
|
109
|
+
blueprints = plugins_manager.flask_blueprints # type: ignore
|
|
110
|
+
appbuilder_views = plugins_manager.flask_appbuilder_views # type: ignore
|
|
111
|
+
appbuilder_menu_links = plugins_manager.flask_appbuilder_menu_links # type: ignore
|
|
106
112
|
|
|
107
113
|
appbuilder = app.appbuilder
|
|
108
114
|
|
|
109
|
-
for view in
|
|
115
|
+
for view in appbuilder_views:
|
|
110
116
|
name = view.get("name")
|
|
111
117
|
if name:
|
|
112
118
|
filtered_view_kwargs = {k: v for k, v in view.items() if k not in ["view"]}
|
|
@@ -124,13 +130,11 @@ def init_plugins(app):
|
|
|
124
130
|
# Since Airflow 3.1 flask_appbuilder_menu_links are added to the Airflow 3 UI
|
|
125
131
|
# navbar..
|
|
126
132
|
if not AIRFLOW_V_3_1_PLUS:
|
|
127
|
-
for menu_link in sorted(
|
|
128
|
-
plugins_manager.flask_appbuilder_menu_links, key=lambda x: (x.get("category", ""), x["name"])
|
|
129
|
-
):
|
|
133
|
+
for menu_link in sorted(appbuilder_menu_links, key=lambda x: (x.get("category", ""), x["name"])):
|
|
130
134
|
log.debug("Adding menu link %s to %s", menu_link["name"], menu_link["href"])
|
|
131
135
|
appbuilder.add_link(**menu_link)
|
|
132
136
|
|
|
133
|
-
for blue_print in
|
|
137
|
+
for blue_print in blueprints:
|
|
134
138
|
log.debug("Adding blueprint %s:%s", blue_print["name"], blue_print["blueprint"].import_name)
|
|
135
139
|
app.register_blueprint(blue_print["blueprint"])
|
|
136
140
|
|