apache-airflow-providers-fab 2.0.2rc1__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.
Files changed (36) hide show
  1. airflow/providers/fab/__init__.py +3 -3
  2. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +4 -0
  3. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +4 -0
  4. airflow/providers/fab/auth_manager/fab_auth_manager.py +2 -32
  5. airflow/providers/fab/auth_manager/models/__init__.py +56 -6
  6. airflow/providers/fab/auth_manager/models/anonymous_user.py +5 -1
  7. airflow/providers/fab/auth_manager/security_manager/override.py +30 -38
  8. airflow/providers/fab/get_provider_info.py +56 -0
  9. airflow/providers/fab/www/app.py +2 -0
  10. airflow/providers/fab/www/auth.py +3 -3
  11. airflow/providers/fab/www/extensions/init_jinja_globals.py +1 -1
  12. airflow/providers/fab/www/extensions/init_wsgi_middlewares.py +41 -0
  13. airflow/providers/fab/www/package-lock.json +1267 -528
  14. airflow/providers/fab/www/package.json +8 -8
  15. airflow/providers/fab/www/security/permissions.py +0 -13
  16. airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.js → main.eb83be09d97c23018bcb.js} +1 -1
  17. airflow/providers/fab/www/static/dist/manifest.json +11 -11
  18. airflow/providers/fab/www/static/dist/{moment.624b1f00ba723d39ce06.js → moment.75a9286ff6019fefe5d9.js} +1 -1
  19. airflow/providers/fab/www/views.py +1 -1
  20. {apache_airflow_providers_fab-2.0.2rc1.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/METADATA +11 -11
  21. {apache_airflow_providers_fab-2.0.2rc1.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/RECORD +36 -35
  22. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.feec4a4075c2f3d6ae01.css → airflowDefaultTheme.a26736fa84b3356edac0.css} +0 -0
  23. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.feec4a4075c2f3d6ae01.js → airflowDefaultTheme.a26736fa84b3356edac0.js} +0 -0
  24. /airflow/providers/fab/www/static/dist/{flash.137b30cff85b5588e661.css → flash.fbcc531a39479aa27065.css} +0 -0
  25. /airflow/providers/fab/www/static/dist/{flash.137b30cff85b5588e661.js → flash.fbcc531a39479aa27065.js} +0 -0
  26. /airflow/providers/fab/www/static/dist/{loadingDots.48ab7d5b04e66f2686b0.css → loadingDots.e1fc82c3ac3f9af3771e.css} +0 -0
  27. /airflow/providers/fab/www/static/dist/{loadingDots.48ab7d5b04e66f2686b0.js → loadingDots.e1fc82c3ac3f9af3771e.js} +0 -0
  28. /airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.css → main.eb83be09d97c23018bcb.css} +0 -0
  29. /airflow/providers/fab/www/static/dist/{main.edb2d40dfbbc537916e3.js.LICENSE.txt → main.eb83be09d97c23018bcb.js.LICENSE.txt} +0 -0
  30. /airflow/providers/fab/www/static/dist/{materialIcons.57390fa60d8f61175334.css → materialIcons.b21138ea09d0cdf9ffc4.css} +0 -0
  31. /airflow/providers/fab/www/static/dist/{materialIcons.57390fa60d8f61175334.js → materialIcons.b21138ea09d0cdf9ffc4.js} +0 -0
  32. /airflow/providers/fab/www/static/dist/{moment.624b1f00ba723d39ce06.js.LICENSE.txt → moment.75a9286ff6019fefe5d9.js.LICENSE.txt} +0 -0
  33. {apache_airflow_providers_fab-2.0.2rc1.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/WHEEL +0 -0
  34. {apache_airflow_providers_fab-2.0.2rc1.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/entry_points.txt +0 -0
  35. {apache_airflow_providers_fab-2.0.2rc1.dist-info → apache_airflow_providers_fab-2.1.0.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +0 -0
  36. {apache_airflow_providers_fab-2.0.2rc1.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.2"
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.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.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:
@@ -566,7 +566,7 @@ class FabAuthManager(BaseAuthManager[User]):
566
566
 
567
567
  if details and details.id:
568
568
  # Check whether the user has permissions to access a specific DAG
569
- resource_dag_name = self._resource_name(details.id, RESOURCE_DAG)
569
+ resource_dag_name = permissions.resource_name(details.id, RESOURCE_DAG)
570
570
  return self._is_authorized(method=method, resource_type=resource_dag_name, user=user)
571
571
 
572
572
  return False
@@ -592,7 +592,7 @@ class FabAuthManager(BaseAuthManager[User]):
592
592
 
593
593
  if details and details.id:
594
594
  # Check whether the user has permissions to access a specific DAG Run permission on a DAG Level
595
- resource_dag_name = self._resource_name(details.id, RESOURCE_DAG_RUN)
595
+ resource_dag_name = permissions.resource_name(details.id, RESOURCE_DAG_RUN)
596
596
  return self._is_authorized(method=method, resource_type=resource_dag_name, user=user)
597
597
 
598
598
  return False
@@ -624,19 +624,6 @@ class FabAuthManager(BaseAuthManager[User]):
624
624
  raise AirflowException(f"Unknown DAG access entity: {dag_access_entity}")
625
625
  return _MAP_DAG_ACCESS_ENTITY_TO_FAB_RESOURCE_TYPE[dag_access_entity]
626
626
 
627
- def _resource_name(self, dag_id: str, resource_type: str) -> str:
628
- """
629
- Return the FAB resource name for a DAG id.
630
-
631
- :param dag_id: the DAG id
632
-
633
- :meta private:
634
- """
635
- root_dag_id = self._get_root_dag_id(dag_id)
636
- if hasattr(permissions, "resource_name"):
637
- return getattr(permissions, "resource_name")(root_dag_id, resource_type)
638
- return getattr(permissions, "resource_name_for_dag")(root_dag_id)
639
-
640
627
  @staticmethod
641
628
  def _get_user_permissions(user: User):
642
629
  """
@@ -651,23 +638,6 @@ class FabAuthManager(BaseAuthManager[User]):
651
638
  return []
652
639
  return getattr(user, "perms") or []
653
640
 
654
- def _get_root_dag_id(self, dag_id: str) -> str:
655
- """
656
- Return the root DAG id in case of sub DAG, return the DAG id otherwise.
657
-
658
- :param dag_id: the DAG id
659
-
660
- :meta private:
661
- """
662
- if not self.appbuilder:
663
- raise AirflowException("AppBuilder is not initialized.")
664
-
665
- if "." in dag_id and hasattr(DagModel, "root_dag_id"):
666
- return self.appbuilder.get_session.scalar(
667
- select(DagModel.dag_id, DagModel.root_dag_id).where(DagModel.dag_id == dag_id).limit(1)
668
- )
669
- return dag_id
670
-
671
641
  def _sync_appbuilder_roles(self):
672
642
  """
673
643
  Sync appbuilder roles to DB.
@@ -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("permission_view_id", Integer, ForeignKey("ab_permission_view.id")),
106
- Column("role_id", Integer, ForeignKey("ab_role.id")),
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("Permission", secondary=assoc_permission_role, backref="role", lazy="joined")
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("Role", secondary=assoc_user_role, backref="user", lazy="selectin")
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-group",
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
- if "ab_user" not in inspector.get_table_names():
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)
@@ -921,15 +919,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
921
919
  dags = dagbag.dags.values()
922
920
 
923
921
  for dag in dags:
924
- root_dag_id = dag.dag_id
925
922
  for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
926
- dag_resource_name = self._resource_name(root_dag_id, resource_name)
923
+ dag_resource_name = permissions.resource_name(dag.dag_id, resource_name)
927
924
  for action_name in resource_values["actions"]:
928
925
  if (action_name, dag_resource_name) not in perms:
929
926
  self._merge_perm(action_name, dag_resource_name)
930
927
 
931
928
  if dag.access_control is not None:
932
- self.sync_perm_for_dag(root_dag_id, dag.access_control)
929
+ self.sync_perm_for_dag(dag.dag_id, dag.access_control)
933
930
 
934
931
  def sync_perm_for_dag(
935
932
  self,
@@ -949,7 +946,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
949
946
  :return:
950
947
  """
951
948
  for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
952
- dag_resource_name = self._resource_name(dag_id, resource_name)
949
+ dag_resource_name = permissions.resource_name(dag_id, resource_name)
953
950
  for dag_action_name in resource_values["actions"]:
954
951
  self.create_permission(dag_action_name, dag_resource_name)
955
952
 
@@ -962,17 +959,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
962
959
  dag_id,
963
960
  )
964
961
 
965
- def _resource_name(self, dag_id: str, resource_name: str) -> str:
966
- """
967
- Get the resource name from permissions.
968
-
969
- This method is to keep compatibility with new FAB versions
970
- running with old airflow versions.
971
- """
972
- if hasattr(permissions, "resource_name"):
973
- return getattr(permissions, "resource_name")(dag_id, resource_name)
974
- return getattr(permissions, "resource_name_for_dag")(dag_id)
975
-
976
962
  def _sync_dag_view_permissions(
977
963
  self,
978
964
  dag_id: str,
@@ -1000,7 +986,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1000
986
 
1001
987
  # Revoking stale permissions for all possible DAG level resources
1002
988
  for resource_name in self.RESOURCE_DETAILS_MAP.keys():
1003
- dag_resource_name = self._resource_name(dag_id, resource_name)
989
+ dag_resource_name = permissions.resource_name(dag_id, resource_name)
1004
990
  if resource := self.get_resource(dag_resource_name):
1005
991
  existing_dag_perms = self.get_resource_permissions(resource)
1006
992
  for perm in existing_dag_perms:
@@ -1043,7 +1029,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1043
1029
  f"The set of valid resource names is: {self.RESOURCE_DETAILS_MAP.keys()}"
1044
1030
  )
1045
1031
 
1046
- dag_resource_name = self._resource_name(dag_id, resource_name)
1032
+ dag_resource_name = permissions.resource_name(dag_id, resource_name)
1047
1033
  self.log.debug("Syncing DAG-level permissions for DAG '%s'", dag_resource_name)
1048
1034
 
1049
1035
  invalid_actions = set(actions) - self.RESOURCE_DETAILS_MAP[resource_name]["actions"]
@@ -1323,15 +1309,20 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1323
1309
 
1324
1310
  def add_user(
1325
1311
  self,
1326
- username,
1327
- first_name,
1328
- last_name,
1329
- email,
1330
- role,
1331
- password="",
1332
- 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,
1333
1320
  ):
1334
1321
  """Create a user."""
1322
+ roles: list[Role] = []
1323
+ if role:
1324
+ roles = role if isinstance(role, list) else [role]
1325
+
1335
1326
  try:
1336
1327
  user = self.user_model()
1337
1328
  user.first_name = first_name
@@ -1340,7 +1331,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1340
1331
  user.email = email
1341
1332
  user.active = True
1342
1333
  self.get_session.add(user)
1343
- user.roles = role if isinstance(role, list) else [role]
1334
+ user.roles = roles
1335
+ user.groups = groups or []
1344
1336
  if hashed_password:
1345
1337
  user.password = hashed_password
1346
1338
  else:
@@ -1704,7 +1696,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1704
1696
  """
1705
1697
  if user is None:
1706
1698
  user = g.user
1707
- return user.roles
1699
+ return user.roles + [role for group in user.groups for role in group.roles]
1708
1700
 
1709
1701
  """
1710
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
  },
@@ -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("webserver", "access_denied_message")
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("webserver", "EXPOSE_HOSTNAME") else "",
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("webserver", "EXPOSE_HOSTNAME") else "",
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("webserver", "EXPOSE_HOSTNAME")
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
+ )