apache-airflow-providers-fab 2.0.2rc2__py3-none-any.whl → 2.1.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/__init__.py +3 -3
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +4 -0
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +4 -0
- airflow/providers/fab/auth_manager/models/__init__.py +56 -6
- airflow/providers/fab/auth_manager/models/anonymous_user.py +5 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +25 -21
- airflow/providers/fab/get_provider_info.py +56 -0
- airflow/providers/fab/www/app.py +2 -0
- airflow/providers/fab/www/auth.py +3 -3
- airflow/providers/fab/www/extensions/init_jinja_globals.py +1 -1
- airflow/providers/fab/www/extensions/init_wsgi_middlewares.py +41 -0
- airflow/providers/fab/www/package-lock.json +1258 -520
- airflow/providers/fab/www/package.json +7 -7
- airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.js → main.eb83be09d97c23018bcb.js} +1 -1
- airflow/providers/fab/www/static/dist/manifest.json +11 -11
- airflow/providers/fab/www/static/dist/{moment.624b1f00ba723d39ce06.js → moment.75a9286ff6019fefe5d9.js} +1 -1
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/METADATA +11 -11
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/RECORD +33 -32
- /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.feec4a4075c2f3d6ae01.css → airflowDefaultTheme.a26736fa84b3356edac0.css} +0 -0
- /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.feec4a4075c2f3d6ae01.js → airflowDefaultTheme.a26736fa84b3356edac0.js} +0 -0
- /airflow/providers/fab/www/static/dist/{flash.137b30cff85b5588e661.css → flash.fbcc531a39479aa27065.css} +0 -0
- /airflow/providers/fab/www/static/dist/{flash.137b30cff85b5588e661.js → flash.fbcc531a39479aa27065.js} +0 -0
- /airflow/providers/fab/www/static/dist/{loadingDots.48ab7d5b04e66f2686b0.css → loadingDots.e1fc82c3ac3f9af3771e.css} +0 -0
- /airflow/providers/fab/www/static/dist/{loadingDots.48ab7d5b04e66f2686b0.js → loadingDots.e1fc82c3ac3f9af3771e.js} +0 -0
- /airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.css → main.eb83be09d97c23018bcb.css} +0 -0
- /airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.js.LICENSE.txt → main.eb83be09d97c23018bcb.js.LICENSE.txt} +0 -0
- /airflow/providers/fab/www/static/dist/{materialIcons.57390fa60d8f61175334.css → materialIcons.b21138ea09d0cdf9ffc4.css} +0 -0
- /airflow/providers/fab/www/static/dist/{materialIcons.57390fa60d8f61175334.js → materialIcons.b21138ea09d0cdf9ffc4.js} +0 -0
- /airflow/providers/fab/www/static/dist/{moment.624b1f00ba723d39ce06.js.LICENSE.txt → moment.75a9286ff6019fefe5d9.js.LICENSE.txt} +0 -0
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
- {apache_airflow_providers_fab-2.0.2rc2.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/licenses/NOTICE +0 -0
@@ -29,11 +29,11 @@ from airflow import __version__ as airflow_version
|
|
29
29
|
|
30
30
|
__all__ = ["__version__"]
|
31
31
|
|
32
|
-
__version__ = "2.0
|
32
|
+
__version__ = "2.1.0"
|
33
33
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
35
|
-
"3.0.
|
35
|
+
"3.0.2"
|
36
36
|
):
|
37
37
|
raise RuntimeError(
|
38
|
-
f"The package `apache-airflow-providers-fab:{__version__}` needs Apache Airflow 3.0.
|
38
|
+
f"The package `apache-airflow-providers-fab:{__version__}` needs Apache Airflow 3.0.2+"
|
39
39
|
)
|
@@ -123,6 +123,8 @@ def patch_role(*, role_name: str, update_mask: UpdateMask = None) -> APIResponse
|
|
123
123
|
"""Update a role."""
|
124
124
|
security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
|
125
125
|
body = request.json
|
126
|
+
if body is None:
|
127
|
+
raise BadRequest("Request body is required")
|
126
128
|
try:
|
127
129
|
data = role_schema.load(body)
|
128
130
|
except ValidationError as err:
|
@@ -156,6 +158,8 @@ def post_role() -> APIResponse:
|
|
156
158
|
"""Create a new role."""
|
157
159
|
security_manager = cast("FabAuthManager", get_auth_manager()).security_manager
|
158
160
|
body = request.json
|
161
|
+
if body is None:
|
162
|
+
raise BadRequest("Request body is required")
|
159
163
|
try:
|
160
164
|
data = role_schema.load(body)
|
161
165
|
except ValidationError as err:
|
@@ -88,6 +88,8 @@ def get_users(*, limit: int, order_by: str = "id", offset: str | None = None) ->
|
|
88
88
|
@requires_access_custom_view("POST", permissions.RESOURCE_USER)
|
89
89
|
def post_user() -> APIResponse:
|
90
90
|
"""Create a new user."""
|
91
|
+
if request.json is None:
|
92
|
+
raise BadRequest("Request body is required")
|
91
93
|
try:
|
92
94
|
data = user_schema.load(request.json)
|
93
95
|
except ValidationError as e:
|
@@ -131,6 +133,8 @@ def post_user() -> APIResponse:
|
|
131
133
|
@requires_access_custom_view("PUT", permissions.RESOURCE_USER)
|
132
134
|
def patch_user(*, username: str, update_mask: UpdateMask = None) -> APIResponse:
|
133
135
|
"""Update a user."""
|
136
|
+
if request.json is None:
|
137
|
+
raise BadRequest("Request body is required")
|
134
138
|
try:
|
135
139
|
data = user_schema.load(request.json)
|
136
140
|
except ValidationError as e:
|
@@ -102,11 +102,37 @@ assoc_permission_role = Table(
|
|
102
102
|
"ab_permission_view_role",
|
103
103
|
Model.metadata,
|
104
104
|
Column("id", Integer, primary_key=True),
|
105
|
-
Column(
|
106
|
-
|
105
|
+
Column(
|
106
|
+
"permission_view_id",
|
107
|
+
Integer,
|
108
|
+
ForeignKey("ab_permission_view.id", ondelete="CASCADE"),
|
109
|
+
),
|
110
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
107
111
|
UniqueConstraint("permission_view_id", "role_id"),
|
108
112
|
)
|
109
113
|
|
114
|
+
assoc_user_group = Table(
|
115
|
+
"ab_user_group",
|
116
|
+
Model.metadata,
|
117
|
+
Column("id", Integer, primary_key=True),
|
118
|
+
Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
|
119
|
+
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
120
|
+
UniqueConstraint("user_id", "group_id"),
|
121
|
+
Index("idx_user_id", "user_id"),
|
122
|
+
Index("idx_user_group_id", "group_id"),
|
123
|
+
)
|
124
|
+
|
125
|
+
assoc_group_role = Table(
|
126
|
+
"ab_group_role",
|
127
|
+
Model.metadata,
|
128
|
+
Column("id", Integer, primary_key=True),
|
129
|
+
Column("group_id", Integer, ForeignKey("ab_group.id", ondelete="CASCADE")),
|
130
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
131
|
+
UniqueConstraint("group_id", "role_id"),
|
132
|
+
Index("idx_group_id", "group_id"),
|
133
|
+
Index("idx_group_role_id", "role_id"),
|
134
|
+
)
|
135
|
+
|
110
136
|
|
111
137
|
class Role(Model):
|
112
138
|
"""Represents a user role to which permissions can be assigned."""
|
@@ -115,7 +141,29 @@ class Role(Model):
|
|
115
141
|
|
116
142
|
id = Column(Integer, primary_key=True)
|
117
143
|
name = Column(String(64), unique=True, nullable=False)
|
118
|
-
permissions = relationship(
|
144
|
+
permissions = relationship(
|
145
|
+
"Permission",
|
146
|
+
secondary=assoc_permission_role,
|
147
|
+
backref="role",
|
148
|
+
lazy="joined",
|
149
|
+
passive_deletes=True,
|
150
|
+
)
|
151
|
+
|
152
|
+
def __repr__(self):
|
153
|
+
return self.name
|
154
|
+
|
155
|
+
|
156
|
+
class Group(Model):
|
157
|
+
"""Represents a user group."""
|
158
|
+
|
159
|
+
__tablename__ = "ab_group"
|
160
|
+
|
161
|
+
id = Column(Integer, primary_key=True)
|
162
|
+
name = Column(String(100), unique=True, nullable=False)
|
163
|
+
label = Column(String(150))
|
164
|
+
description = Column(String(512))
|
165
|
+
users = relationship("User", secondary=assoc_user_group, backref="groups", passive_deletes=True)
|
166
|
+
roles = relationship("Role", secondary=assoc_group_role, backref="groups", passive_deletes=True)
|
119
167
|
|
120
168
|
def __repr__(self):
|
121
169
|
return self.name
|
@@ -148,8 +196,8 @@ assoc_user_role = Table(
|
|
148
196
|
"ab_user_role",
|
149
197
|
Model.metadata,
|
150
198
|
Column("id", Integer, primary_key=True),
|
151
|
-
Column("user_id", Integer, ForeignKey("ab_user.id")),
|
152
|
-
Column("role_id", Integer, ForeignKey("ab_role.id")),
|
199
|
+
Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")),
|
200
|
+
Column("role_id", Integer, ForeignKey("ab_role.id", ondelete="CASCADE")),
|
153
201
|
UniqueConstraint("user_id", "role_id"),
|
154
202
|
)
|
155
203
|
|
@@ -170,7 +218,9 @@ class User(Model, BaseUser):
|
|
170
218
|
last_login = Column(DateTime)
|
171
219
|
login_count = Column(Integer)
|
172
220
|
fail_login_count = Column(Integer)
|
173
|
-
roles = relationship(
|
221
|
+
roles = relationship(
|
222
|
+
"Role", secondary=assoc_user_role, backref="user", lazy="selectin", passive_deletes=True
|
223
|
+
)
|
174
224
|
created_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
|
175
225
|
changed_on = Column(DateTime, default=datetime.datetime.now, nullable=True)
|
176
226
|
|
@@ -37,13 +37,17 @@ class AnonymousUser(AnonymousUserMixin, BaseUser):
|
|
37
37
|
if not self._roles:
|
38
38
|
public_role = current_app.config.get("AUTH_ROLE_PUBLIC", None)
|
39
39
|
self._roles = {current_app.appbuilder.sm.find_role(public_role)} if public_role else set()
|
40
|
-
return self._roles
|
40
|
+
return list(self._roles)
|
41
41
|
|
42
42
|
@roles.setter
|
43
43
|
def roles(self, roles):
|
44
44
|
self._roles = roles
|
45
45
|
self._perms = set()
|
46
46
|
|
47
|
+
@property
|
48
|
+
def groups(self):
|
49
|
+
return []
|
50
|
+
|
47
51
|
@property
|
48
52
|
def perms(self):
|
49
53
|
if not self._perms:
|
@@ -55,6 +55,7 @@ from flask_appbuilder.security.views import (
|
|
55
55
|
AuthOIDView,
|
56
56
|
AuthRemoteUserView,
|
57
57
|
RegisterUserModelView,
|
58
|
+
UserGroupModelView,
|
58
59
|
)
|
59
60
|
from flask_babel import lazy_gettext
|
60
61
|
from flask_jwt_extended import JWTManager
|
@@ -71,6 +72,7 @@ from airflow.exceptions import AirflowException
|
|
71
72
|
from airflow.models import DagBag
|
72
73
|
from airflow.providers.fab.auth_manager.models import (
|
73
74
|
Action,
|
75
|
+
Group,
|
74
76
|
Permission,
|
75
77
|
RegisterUser,
|
76
78
|
Resource,
|
@@ -100,10 +102,7 @@ from airflow.providers.fab.auth_manager.views.user_edit import (
|
|
100
102
|
from airflow.providers.fab.auth_manager.views.user_stats import CustomUserStatsChartView
|
101
103
|
from airflow.providers.fab.www.security import permissions
|
102
104
|
from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
103
|
-
from airflow.providers.fab.www.session import
|
104
|
-
AirflowDatabaseSessionInterface,
|
105
|
-
AirflowDatabaseSessionInterface as FabAirflowDatabaseSessionInterface,
|
106
|
-
)
|
105
|
+
from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
|
107
106
|
from airflow.security.permissions import RESOURCE_BACKFILL
|
108
107
|
|
109
108
|
if TYPE_CHECKING:
|
@@ -149,6 +148,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
149
148
|
""" Models """
|
150
149
|
user_model = User
|
151
150
|
role_model = Role
|
151
|
+
group_model = Group
|
152
152
|
action_model = Action
|
153
153
|
resource_model = Resource
|
154
154
|
permission_model = Permission
|
@@ -173,6 +173,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
173
173
|
actionmodelview = ActionModelView
|
174
174
|
permissionmodelview = PermissionPairModelView
|
175
175
|
rolemodelview = CustomRoleModelView
|
176
|
+
groupmodelview = UserGroupModelView
|
176
177
|
registeruser_model = RegisterUser
|
177
178
|
registerusermodelview = RegisterUserModelView
|
178
179
|
resourcemodelview = ResourceModelView
|
@@ -450,7 +451,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
450
451
|
role_view = self.appbuilder.add_view(
|
451
452
|
self.rolemodelview,
|
452
453
|
"List Roles",
|
453
|
-
icon="fa-
|
454
|
+
icon="fa-user-gear",
|
454
455
|
label=lazy_gettext("List Roles"),
|
455
456
|
category="Security",
|
456
457
|
category_icon="fa-cogs",
|
@@ -532,12 +533,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
532
533
|
return self.update_user(user)
|
533
534
|
|
534
535
|
def reset_user_sessions(self, user: User) -> None:
|
535
|
-
if isinstance(
|
536
|
-
self.appbuilder.get_app.session_interface, AirflowDatabaseSessionInterface
|
537
|
-
) or isinstance(
|
538
|
-
self.appbuilder.get_app.session_interface,
|
539
|
-
FabAirflowDatabaseSessionInterface,
|
540
|
-
):
|
536
|
+
if isinstance(self.appbuilder.get_app.session_interface, AirflowDatabaseSessionInterface):
|
541
537
|
interface = self.appbuilder.get_app.session_interface
|
542
538
|
session = interface.db.session
|
543
539
|
user_session_model = interface.sql_session_model
|
@@ -859,6 +855,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
859
855
|
self.registerusermodelview.datamodel = SQLAInterface(self.registeruser_model)
|
860
856
|
|
861
857
|
self.rolemodelview.datamodel = SQLAInterface(self.role_model)
|
858
|
+
self.groupmodelview.datamodel = SQLAInterface(self.group_model)
|
862
859
|
self.actionmodelview.datamodel = SQLAInterface(self.action_model)
|
863
860
|
self.resourcemodelview.datamodel = SQLAInterface(self.resource_model)
|
864
861
|
self.permissionmodelview.datamodel = SQLAInterface(self.permission_model)
|
@@ -875,7 +872,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
875
872
|
try:
|
876
873
|
engine = self.get_session.get_bind(mapper=None, clause=None)
|
877
874
|
inspector = inspect(engine)
|
878
|
-
|
875
|
+
existing_tables = inspector.get_table_names()
|
876
|
+
if "ab_user" not in existing_tables or "ab_group" not in existing_tables:
|
879
877
|
log.info(const.LOGMSG_INF_SEC_NO_DB)
|
880
878
|
Base.metadata.create_all(engine)
|
881
879
|
log.info(const.LOGMSG_INF_SEC_ADD_DB)
|
@@ -1311,15 +1309,20 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1311
1309
|
|
1312
1310
|
def add_user(
|
1313
1311
|
self,
|
1314
|
-
username,
|
1315
|
-
first_name,
|
1316
|
-
last_name,
|
1317
|
-
email,
|
1318
|
-
role,
|
1319
|
-
password="",
|
1320
|
-
hashed_password="",
|
1312
|
+
username: str,
|
1313
|
+
first_name: str,
|
1314
|
+
last_name: str,
|
1315
|
+
email: str,
|
1316
|
+
role: list[Role] | Role | None = None,
|
1317
|
+
password: str = "",
|
1318
|
+
hashed_password: str = "",
|
1319
|
+
groups: list[Group] | None = None,
|
1321
1320
|
):
|
1322
1321
|
"""Create a user."""
|
1322
|
+
roles: list[Role] = []
|
1323
|
+
if role:
|
1324
|
+
roles = role if isinstance(role, list) else [role]
|
1325
|
+
|
1323
1326
|
try:
|
1324
1327
|
user = self.user_model()
|
1325
1328
|
user.first_name = first_name
|
@@ -1328,7 +1331,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1328
1331
|
user.email = email
|
1329
1332
|
user.active = True
|
1330
1333
|
self.get_session.add(user)
|
1331
|
-
user.roles =
|
1334
|
+
user.roles = roles
|
1335
|
+
user.groups = groups or []
|
1332
1336
|
if hashed_password:
|
1333
1337
|
user.password = hashed_password
|
1334
1338
|
else:
|
@@ -1692,7 +1696,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1692
1696
|
"""
|
1693
1697
|
if user is None:
|
1694
1698
|
user = g.user
|
1695
|
-
return user.roles
|
1699
|
+
return user.roles + [role for group in user.groups for role in group.roles]
|
1696
1700
|
|
1697
1701
|
"""
|
1698
1702
|
--------------------
|
@@ -30,6 +30,20 @@ def get_provider_info():
|
|
30
30
|
"fab": {
|
31
31
|
"description": "This section contains configs specific to FAB provider.",
|
32
32
|
"options": {
|
33
|
+
"access_denied_message": {
|
34
|
+
"description": "The message displayed when a user attempts to execute actions beyond their authorised privileges.\n",
|
35
|
+
"version_added": "2.1.0",
|
36
|
+
"type": "string",
|
37
|
+
"example": None,
|
38
|
+
"default": "Access is Denied",
|
39
|
+
},
|
40
|
+
"expose_hostname": {
|
41
|
+
"description": "Expose hostname in the web server\n",
|
42
|
+
"version_added": "2.1.0",
|
43
|
+
"type": "string",
|
44
|
+
"example": None,
|
45
|
+
"default": "False",
|
46
|
+
},
|
33
47
|
"auth_rate_limited": {
|
34
48
|
"description": "Boolean for enabling rate limiting on authentication endpoints.\n",
|
35
49
|
"version_added": "1.0.2",
|
@@ -79,6 +93,48 @@ def get_provider_info():
|
|
79
93
|
"example": None,
|
80
94
|
"default": "43200",
|
81
95
|
},
|
96
|
+
"enable_proxy_fix": {
|
97
|
+
"description": "Enable werkzeug ``ProxyFix`` middleware for reverse proxy\n",
|
98
|
+
"version_added": "2.1.0",
|
99
|
+
"type": "boolean",
|
100
|
+
"example": None,
|
101
|
+
"default": "False",
|
102
|
+
},
|
103
|
+
"proxy_fix_x_for": {
|
104
|
+
"description": "Number of values to trust for ``X-Forwarded-For``.\nSee `Werkzeug: X-Forwarded-For Proxy Fix\n<https://werkzeug.palletsprojects.com/en/2.3.x/middleware/proxy_fix/>`__ for more details.\n",
|
105
|
+
"version_added": "2.1.0",
|
106
|
+
"type": "integer",
|
107
|
+
"example": None,
|
108
|
+
"default": "1",
|
109
|
+
},
|
110
|
+
"proxy_fix_x_proto": {
|
111
|
+
"description": "Number of values to trust for ``X-Forwarded-Proto``.\nSee `Werkzeug: X-Forwarded-For Proxy Fix\n<https://werkzeug.palletsprojects.com/en/2.3.x/middleware/proxy_fix/>`__ for more details.\n",
|
112
|
+
"version_added": "2.1.0",
|
113
|
+
"type": "integer",
|
114
|
+
"example": None,
|
115
|
+
"default": "1",
|
116
|
+
},
|
117
|
+
"proxy_fix_x_host": {
|
118
|
+
"description": "Number of values to trust for ``X-Forwarded-Host``.\nSee `Werkzeug: X-Forwarded-For Proxy Fix\n<https://werkzeug.palletsprojects.com/en/2.3.x/middleware/proxy_fix/>`__ for more details.\n",
|
119
|
+
"version_added": "2.1.0",
|
120
|
+
"type": "integer",
|
121
|
+
"example": None,
|
122
|
+
"default": "1",
|
123
|
+
},
|
124
|
+
"proxy_fix_x_port": {
|
125
|
+
"description": "Number of values to trust for ``X-Forwarded-Port``.\nSee `Werkzeug: X-Forwarded-For Proxy Fix\n<https://werkzeug.palletsprojects.com/en/2.3.x/middleware/proxy_fix/>`__ for more details.\n",
|
126
|
+
"version_added": "2.1.0",
|
127
|
+
"type": "integer",
|
128
|
+
"example": None,
|
129
|
+
"default": "1",
|
130
|
+
},
|
131
|
+
"proxy_fix_x_prefix": {
|
132
|
+
"description": "Number of values to trust for ``X-Forwarded-Prefix``.\nSee `Werkzeug: X-Forwarded-For Proxy Fix\n<https://werkzeug.palletsprojects.com/en/2.3.x/middleware/proxy_fix/>`__ for more details.\n",
|
133
|
+
"version_added": "2.1.0",
|
134
|
+
"type": "integer",
|
135
|
+
"example": None,
|
136
|
+
"default": "1",
|
137
|
+
},
|
82
138
|
},
|
83
139
|
}
|
84
140
|
},
|
airflow/providers/fab/www/app.py
CHANGED
@@ -41,6 +41,7 @@ from airflow.providers.fab.www.extensions.init_views import (
|
|
41
41
|
init_error_handlers,
|
42
42
|
init_plugins,
|
43
43
|
)
|
44
|
+
from airflow.providers.fab.www.extensions.init_wsgi_middlewares import init_wsgi_middleware
|
44
45
|
from airflow.providers.fab.www.utils import get_session_lifetime_config
|
45
46
|
|
46
47
|
app: Flask | None = None
|
@@ -103,6 +104,7 @@ def create_app(enable_plugins: bool):
|
|
103
104
|
init_jinja_globals(flask_app, enable_plugins=enable_plugins)
|
104
105
|
init_xframe_protection(flask_app)
|
105
106
|
init_airflow_session_interface(flask_app)
|
107
|
+
init_wsgi_middleware(flask_app)
|
106
108
|
return flask_app
|
107
109
|
|
108
110
|
|
@@ -61,7 +61,7 @@ log = logging.getLogger(__name__)
|
|
61
61
|
|
62
62
|
|
63
63
|
def get_access_denied_message():
|
64
|
-
return conf.get("
|
64
|
+
return conf.get("fab", "access_denied_message")
|
65
65
|
|
66
66
|
|
67
67
|
def has_access_with_pk(f):
|
@@ -145,7 +145,7 @@ def _has_access(*, is_authorized: bool, func: Callable, args, kwargs):
|
|
145
145
|
return (
|
146
146
|
render_template(
|
147
147
|
"airflow/no_roles_permissions.html",
|
148
|
-
hostname=get_hostname() if conf.getboolean("
|
148
|
+
hostname=get_hostname() if conf.getboolean("fab", "EXPOSE_HOSTNAME") else "",
|
149
149
|
logout_url=get_fab_auth_manager().get_url_logout(),
|
150
150
|
),
|
151
151
|
403,
|
@@ -217,7 +217,7 @@ def has_access_dag(method: ResourceMethod, access_entity: DagAccessEntity | None
|
|
217
217
|
return (
|
218
218
|
render_template(
|
219
219
|
"airflow/no_roles_permissions.html",
|
220
|
-
hostname=get_hostname() if conf.getboolean("
|
220
|
+
hostname=get_hostname() if conf.getboolean("fab", "EXPOSE_HOSTNAME") else "",
|
221
221
|
logout_url=get_auth_manager().get_url_logout(),
|
222
222
|
),
|
223
223
|
403,
|
@@ -37,7 +37,7 @@ def init_jinja_globals(app, enable_plugins: bool):
|
|
37
37
|
elif server_timezone == "utc":
|
38
38
|
server_timezone = "UTC"
|
39
39
|
|
40
|
-
expose_hostname = conf.getboolean("
|
40
|
+
expose_hostname = conf.getboolean("fab", "EXPOSE_HOSTNAME")
|
41
41
|
hostname = get_hostname() if expose_hostname else "redact"
|
42
42
|
|
43
43
|
try:
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from typing import TYPE_CHECKING
|
21
|
+
|
22
|
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
23
|
+
|
24
|
+
from airflow.configuration import conf
|
25
|
+
|
26
|
+
if TYPE_CHECKING:
|
27
|
+
from flask import Flask
|
28
|
+
|
29
|
+
|
30
|
+
def init_wsgi_middleware(flask_app: Flask) -> None:
|
31
|
+
"""Handle X-Forwarded-* headers and base_url support."""
|
32
|
+
# Apply ProxyFix middleware
|
33
|
+
if conf.getboolean("fab", "ENABLE_PROXY_FIX"):
|
34
|
+
flask_app.wsgi_app = ProxyFix( # type: ignore
|
35
|
+
flask_app.wsgi_app,
|
36
|
+
x_for=conf.getint("fab", "PROXY_FIX_X_FOR", fallback=1),
|
37
|
+
x_proto=conf.getint("fab", "PROXY_FIX_X_PROTO", fallback=1),
|
38
|
+
x_host=conf.getint("fab", "PROXY_FIX_X_HOST", fallback=1),
|
39
|
+
x_port=conf.getint("fab", "PROXY_FIX_X_PORT", fallback=1),
|
40
|
+
x_prefix=conf.getint("fab", "PROXY_FIX_X_PREFIX", fallback=1),
|
41
|
+
)
|