apache-airflow-providers-fab 3.0.0rc1__py3-none-any.whl → 3.1.0rc1__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/fab_auth_manager.py +33 -3
- airflow/providers/fab/auth_manager/models/__init__.py +7 -21
- airflow/providers/fab/auth_manager/models/db.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +55 -13
- 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} +4 -4
- 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/package-lock.json +390 -266
- airflow/providers/fab/www/package.json +9 -9
- airflow/providers/fab/www/session.py +5 -8
- airflow/providers/fab/www/static/dist/{743.fc7a7c6ef9d09365976e.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/static/dist/runtime.254c277d91ce3ac79c64.js +1 -0
- airflow/providers/fab/www/views.py +19 -9
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/METADATA +14 -13
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/RECORD +49 -43
- airflow/providers/fab/www/static/dist/runtime.ad800fc1845ad5c6ddeb.js +0 -1
- /airflow/providers/fab/www/static/dist/{743.fc7a7c6ef9d09365976e.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
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
- {airflow/providers/fab → apache_airflow_providers_fab-3.1.0rc1.dist-info/licenses}/LICENSE +0 -0
- {apache_airflow_providers_fab-3.0.0rc1.dist-info → apache_airflow_providers_fab-3.1.0rc1.dist-info}/licenses/NOTICE +0 -0
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import copy
|
|
21
21
|
import datetime
|
|
22
|
+
import importlib
|
|
22
23
|
import itertools
|
|
23
24
|
import logging
|
|
24
25
|
import uuid
|
|
@@ -59,13 +60,14 @@ from flask_jwt_extended import JWTManager
|
|
|
59
60
|
from flask_login import LoginManager
|
|
60
61
|
from itsdangerous import want_bytes
|
|
61
62
|
from markupsafe import Markup, escape
|
|
63
|
+
from packaging.version import Version
|
|
62
64
|
from sqlalchemy import delete, func, inspect, or_, select
|
|
63
65
|
from sqlalchemy.exc import MultipleResultsFound
|
|
64
66
|
from sqlalchemy.orm import joinedload
|
|
65
67
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
66
68
|
|
|
67
69
|
from airflow.configuration import conf
|
|
68
|
-
from airflow.
|
|
70
|
+
from airflow.providers.common.compat.sdk import AirflowException
|
|
69
71
|
from airflow.providers.fab.auth_manager.models import (
|
|
70
72
|
Action,
|
|
71
73
|
Group,
|
|
@@ -122,11 +124,17 @@ if AIRFLOW_V_3_1_PLUS:
|
|
|
122
124
|
with create_session() as session:
|
|
123
125
|
yield from DBDagBag().iter_all_latest_version_dags(session=session)
|
|
124
126
|
else:
|
|
125
|
-
|
|
127
|
+
try:
|
|
128
|
+
from airflow.models.dagbag import DagBag
|
|
129
|
+
except (ImportError, AttributeError):
|
|
130
|
+
DagBag = None
|
|
126
131
|
|
|
127
132
|
def _iter_dags() -> Iterable[DAG | SerializedDAG]:
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
if DagBag is None:
|
|
134
|
+
return []
|
|
135
|
+
dagbag = DagBag(read_dags_from_db=True)
|
|
136
|
+
if hasattr(dagbag, "collect_dags_from_db"):
|
|
137
|
+
dagbag.collect_dags_from_db()
|
|
130
138
|
return dagbag.dags.values()
|
|
131
139
|
|
|
132
140
|
|
|
@@ -725,6 +733,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
725
733
|
"""The JMESPATH role to use for user registration."""
|
|
726
734
|
return current_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
|
|
727
735
|
|
|
736
|
+
@property
|
|
737
|
+
def auth_remote_user_env_var(self) -> str:
|
|
738
|
+
return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
|
|
739
|
+
|
|
728
740
|
@property
|
|
729
741
|
def auth_username_ci(self):
|
|
730
742
|
"""Get the auth username for CI."""
|
|
@@ -790,10 +802,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
790
802
|
current_app.config.setdefault("AUTH_ROLES_SYNC_AT_LOGIN", False)
|
|
791
803
|
current_app.config.setdefault("AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS", False)
|
|
792
804
|
|
|
793
|
-
|
|
794
|
-
from werkzeug import __version__ as werkzeug_version
|
|
795
|
-
|
|
796
|
-
parsed_werkzeug_version = Version(werkzeug_version)
|
|
805
|
+
parsed_werkzeug_version = Version(importlib.metadata.version("werkzeug"))
|
|
797
806
|
if parsed_werkzeug_version < Version("3.0.0"):
|
|
798
807
|
current_app.config.setdefault("FAB_PASSWORD_HASH_METHOD", "pbkdf2:sha256")
|
|
799
808
|
current_app.config.setdefault(
|
|
@@ -1040,7 +1049,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1040
1049
|
self.remove_permission_from_role(role, perm)
|
|
1041
1050
|
|
|
1042
1051
|
# Adding the access control permissions
|
|
1043
|
-
for rolename,
|
|
1052
|
+
for rolename, resource_actions_raw in access_control.items():
|
|
1044
1053
|
role = self.find_role(rolename)
|
|
1045
1054
|
if not role:
|
|
1046
1055
|
raise AirflowException(
|
|
@@ -1048,9 +1057,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1048
1057
|
f"'{rolename}', but that role does not exist"
|
|
1049
1058
|
)
|
|
1050
1059
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1060
|
+
# Support for old-style access_control where only the actions are specified
|
|
1061
|
+
resource_actions = (
|
|
1062
|
+
resource_actions_raw
|
|
1063
|
+
if isinstance(resource_actions_raw, dict)
|
|
1064
|
+
else {permissions.RESOURCE_DAG: set(resource_actions_raw)}
|
|
1065
|
+
)
|
|
1054
1066
|
|
|
1055
1067
|
for resource_name, actions in resource_actions.items():
|
|
1056
1068
|
if resource_name not in self.RESOURCE_DETAILS_MAP:
|
|
@@ -1644,7 +1656,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1644
1656
|
return perm
|
|
1645
1657
|
resource = self.create_resource(resource_name)
|
|
1646
1658
|
if resource is None:
|
|
1647
|
-
log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW,
|
|
1659
|
+
log.error(const.LOGMSG_ERR_SEC_ADD_PERMVIEW, "Resource creation failed %s", resource_name)
|
|
1648
1660
|
return None
|
|
1649
1661
|
action = self.create_action(action_name)
|
|
1650
1662
|
perm = self.permission_model()
|
|
@@ -2205,6 +2217,36 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2205
2217
|
# decode - if empty string, default to fallback, otherwise take first element
|
|
2206
2218
|
return raw_value[0].decode("utf-8") or fallback
|
|
2207
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
|
+
|
|
2208
2250
|
"""
|
|
2209
2251
|
---------------
|
|
2210
2252
|
Private methods
|
|
@@ -139,8 +139,8 @@ def upgrade() -> None:
|
|
|
139
139
|
if_not_exists=True,
|
|
140
140
|
)
|
|
141
141
|
with op.batch_alter_table("ab_group_role", schema=None) as batch_op:
|
|
142
|
-
batch_op.create_index("idx_group_id", ["group_id"], unique=False)
|
|
143
|
-
batch_op.create_index("idx_group_role_id", ["role_id"], unique=False)
|
|
142
|
+
batch_op.create_index("idx_group_id", ["group_id"], unique=False, if_not_exists=True)
|
|
143
|
+
batch_op.create_index("idx_group_role_id", ["role_id"], unique=False, if_not_exists=True)
|
|
144
144
|
|
|
145
145
|
op.create_table(
|
|
146
146
|
"ab_permission_view",
|
|
@@ -175,8 +175,8 @@ def upgrade() -> None:
|
|
|
175
175
|
if_not_exists=True,
|
|
176
176
|
)
|
|
177
177
|
with op.batch_alter_table("ab_user_group", schema=None) as batch_op:
|
|
178
|
-
batch_op.create_index("idx_user_group_id", ["group_id"], unique=False)
|
|
179
|
-
batch_op.create_index("idx_user_id", ["user_id"], unique=False)
|
|
178
|
+
batch_op.create_index("idx_user_group_id", ["group_id"], unique=False, if_not_exists=True)
|
|
179
|
+
batch_op.create_index("idx_user_id", ["user_id"], unique=False, if_not_exists=True)
|
|
180
180
|
|
|
181
181
|
op.create_table(
|
|
182
182
|
"ab_user_role",
|
|
@@ -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
|
|