apache-airflow-providers-fab 3.1.0rc1__py3-none-any.whl → 3.2.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.
Files changed (52) hide show
  1. airflow/providers/fab/__init__.py +1 -1
  2. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +3 -1
  3. airflow/providers/fab/auth_manager/api_fastapi/datamodels/roles.py +13 -7
  4. airflow/providers/fab/auth_manager/api_fastapi/datamodels/users.py +68 -0
  5. airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +485 -18
  6. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +2 -4
  7. airflow/providers/fab/auth_manager/api_fastapi/routes/users.py +133 -0
  8. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +1 -2
  9. airflow/providers/fab/auth_manager/api_fastapi/services/users.py +219 -0
  10. airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -2
  11. airflow/providers/fab/auth_manager/cli_commands/permissions_command.py +6 -2
  12. airflow/providers/fab/auth_manager/cli_commands/user_command.py +3 -3
  13. airflow/providers/fab/auth_manager/fab_auth_manager.py +18 -51
  14. airflow/providers/fab/auth_manager/models/__init__.py +6 -6
  15. airflow/providers/fab/auth_manager/security_manager/override.py +97 -84
  16. airflow/providers/fab/auth_manager/views/user.py +12 -0
  17. airflow/providers/fab/cli/__init__.py +18 -0
  18. airflow/providers/fab/{auth_manager/cli_commands → cli}/definition.py +50 -2
  19. airflow/providers/fab/get_provider_info.py +8 -0
  20. airflow/providers/fab/version_compat.py +1 -0
  21. airflow/providers/fab/www/app.py +2 -7
  22. airflow/providers/fab/www/extensions/init_appbuilder.py +3 -2
  23. airflow/providers/fab/www/extensions/init_views.py +11 -7
  24. airflow/providers/fab/www/package-lock.json +764 -572
  25. airflow/providers/fab/www/package.json +12 -9
  26. airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js → 743.8fb7d21632ed892227fe.js} +2 -2
  27. airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.js → airflowDefaultTheme.51e5d14856ee1ebc83ca.js} +1 -1
  28. airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.js → flash.865b6940c00b2a9041b3.js} +1 -1
  29. airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.js → loadingDots.07f5b9805847242736e1.js} +1 -1
  30. airflow/providers/fab/www/static/dist/main.8cffe40bcf7cca998f4e.js +2 -0
  31. airflow/providers/fab/www/static/dist/manifest.json +13 -13
  32. airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.js → materialIcons.4fe84ae36604d84dec78.js} +1 -1
  33. airflow/providers/fab/www/static/dist/moment.0ec3ee3fb60dc999b1fd.js +1 -0
  34. airflow/providers/fab/www/static/js/main.js +11 -0
  35. airflow/providers/fab/www/templates/airflow/main.html +1 -0
  36. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/METADATA +10 -10
  37. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/RECORD +50 -46
  38. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +1 -1
  39. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/NOTICE +1 -1
  40. airflow/providers/fab/www/static/dist/main.bc1f701c3d133e2a3bab.js +0 -2
  41. airflow/providers/fab/www/static/dist/moment.5b85b4f6be2fe9c405ac.js +0 -1
  42. /airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js.LICENSE.txt → 743.8fb7d21632ed892227fe.js.LICENSE.txt} +0 -0
  43. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.css → airflowDefaultTheme.51e5d14856ee1ebc83ca.css} +0 -0
  44. /airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.css → flash.865b6940c00b2a9041b3.css} +0 -0
  45. /airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.css → loadingDots.07f5b9805847242736e1.css} +0 -0
  46. /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.css → main.8cffe40bcf7cca998f4e.css} +0 -0
  47. /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.js.LICENSE.txt → main.8cffe40bcf7cca998f4e.js.LICENSE.txt} +0 -0
  48. /airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.css → materialIcons.4fe84ae36604d84dec78.css} +0 -0
  49. /airflow/providers/fab/www/static/dist/{runtime.254c277d91ce3ac79c64.js → runtime.45b36fb8335446865b53.js} +0 -0
  50. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/WHEEL +0 -0
  51. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/entry_points.txt +0 -0
  52. {apache_airflow_providers_fab-3.1.0rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -44,6 +44,7 @@ from flask_appbuilder.const import (
44
44
  from flask_appbuilder.models.sqla.interface import SQLAInterface
45
45
  from flask_appbuilder.security.api import SecurityApi
46
46
  from flask_appbuilder.security.registerviews import (
47
+ BaseRegisterUser,
47
48
  RegisterUserDBView,
48
49
  RegisterUserOAuthView,
49
50
  )
@@ -52,8 +53,10 @@ from flask_appbuilder.security.views import (
52
53
  AuthLDAPView,
53
54
  AuthOAuthView,
54
55
  AuthRemoteUserView,
56
+ AuthView,
55
57
  RegisterUserModelView,
56
58
  UserGroupModelView,
59
+ UserModelView,
57
60
  )
58
61
  from flask_babel import lazy_gettext
59
62
  from flask_jwt_extended import JWTManager
@@ -67,7 +70,6 @@ from sqlalchemy.orm import joinedload
67
70
  from werkzeug.security import check_password_hash, generate_password_hash
68
71
 
69
72
  from airflow.configuration import conf
70
- from airflow.providers.common.compat.sdk import AirflowException
71
73
  from airflow.providers.fab.auth_manager.models import (
72
74
  Action,
73
75
  Group,
@@ -101,15 +103,16 @@ from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
101
103
  from airflow.providers.fab.www.security import permissions
102
104
  from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
103
105
  from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
104
- from airflow.security.permissions import RESOURCE_BACKFILL
105
106
 
106
107
  if TYPE_CHECKING:
108
+ from authlib.integrations.flask_client import OAuth
109
+
107
110
  from airflow.providers.fab.www.security.permissions import (
108
111
  RESOURCE_ASSET,
109
112
  RESOURCE_ASSET_ALIAS,
110
113
  )
111
114
  from airflow.sdk import DAG
112
- from airflow.serialization.serialized_objects import SerializedDAG
115
+ from airflow.serialization.definitions.dag import SerializedDAG
113
116
  else:
114
117
  from airflow.providers.common.compat.security.permissions import (
115
118
  RESOURCE_ASSET,
@@ -149,6 +152,10 @@ log = logging.getLogger(__name__)
149
152
  MAX_NUM_DATABASE_USER_SESSIONS = 50000
150
153
 
151
154
 
155
+ class FabException(Exception):
156
+ """Custom exception for FAB security manager."""
157
+
158
+
152
159
  class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
153
160
  """
154
161
  This security manager overrides the default AirflowSecurityManager security manager.
@@ -160,11 +167,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
160
167
  :param appbuilder: The appbuilder.
161
168
  """
162
169
 
163
- auth_view = None
170
+ auth_view: AuthView | None = None
164
171
  """ The obj instance for authentication view """
165
- registeruser_view = None
172
+ registeruser_view: BaseRegisterUser | None = None
166
173
  """ The obj instance for registering user view """
167
- user_view = None
174
+ user_view: type[UserModelView] | None = None
168
175
  """ The obj instance for user view """
169
176
 
170
177
  """ Models """
@@ -208,9 +215,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
208
215
  security_api = SecurityApi
209
216
  """ Override if you want your own Security API login endpoint """
210
217
 
211
- jwt_manager = None
218
+ jwt_manager: JWTManager | None = None
212
219
  """ Flask-JWT-Extended """
213
- oauth = None
220
+ oauth: OAuth | None = None
214
221
  oauth_remotes: dict[str, Any]
215
222
  """ Initialized (remote_app) providers dict {'provider_name', OBJ } """
216
223
 
@@ -236,7 +243,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
236
243
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
237
244
  (permissions.ACTION_CAN_READ, RESOURCE_ASSET),
238
245
  (permissions.ACTION_CAN_READ, RESOURCE_ASSET_ALIAS),
239
- (permissions.ACTION_CAN_READ, RESOURCE_BACKFILL),
246
+ (permissions.ACTION_CAN_READ, permissions.RESOURCE_BACKFILL),
240
247
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
241
248
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_POOL),
242
249
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR),
@@ -305,12 +312,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
305
312
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_VARIABLE),
306
313
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
307
314
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
315
+ (permissions.ACTION_CAN_CREATE, permissions.RESOURCE_XCOM),
316
+ (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_XCOM),
308
317
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
309
318
  (permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
310
319
  (permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
311
- (permissions.ACTION_CAN_CREATE, RESOURCE_BACKFILL),
312
- (permissions.ACTION_CAN_EDIT, RESOURCE_BACKFILL),
313
- (permissions.ACTION_CAN_DELETE, RESOURCE_BACKFILL),
320
+ (permissions.ACTION_CAN_CREATE, permissions.RESOURCE_BACKFILL),
321
+ (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_BACKFILL),
322
+ (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_BACKFILL),
314
323
  ]
315
324
  # [END security_op_perms]
316
325
 
@@ -418,9 +427,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
418
427
  log.warning("JWT token is not validated!")
419
428
  return me
420
429
 
421
- raise AirflowException("OAuth signature verify failed")
430
+ raise FabException("OAuth signature verify failed")
422
431
 
423
- def register_views(self):
432
+ def register_views(self) -> None:
424
433
  """Register FAB auth manager related views."""
425
434
  if not current_app.config.get("FAB_ADD_SECURITY_VIEWS", True):
426
435
  return
@@ -457,7 +466,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
457
466
 
458
467
  # this needs to be done after the view is added, otherwise the blueprint
459
468
  # is not initialized
460
- if self.is_auth_limited:
469
+ if self.is_auth_limited and self.auth_view:
461
470
  self.limiter.limit(self.auth_rate_limit, methods=["POST"])(self.auth_view.blueprint)
462
471
 
463
472
  self.user_view = self.appbuilder.add_view(
@@ -480,14 +489,13 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
480
489
  )
481
490
  role_view.related_views = [self.user_view.__class__]
482
491
 
483
- if self.userstatschartview:
484
- self.appbuilder.add_view(
485
- self.userstatschartview,
486
- "User's Statistics",
487
- icon="fa-bar-chart-o",
488
- label=lazy_gettext("User's Statistics"),
489
- category="Security",
490
- )
492
+ self.appbuilder.add_view(
493
+ self.userstatschartview,
494
+ "User's Statistics",
495
+ icon="fa-bar-chart-o",
496
+ label=lazy_gettext("User's Statistics"),
497
+ category="Security",
498
+ )
491
499
  if self.auth_user_registration:
492
500
  self.appbuilder.add_view(
493
501
  self.registerusermodelview,
@@ -534,7 +542,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
534
542
  lm.user_loader(self.load_user)
535
543
  return lm
536
544
 
537
- def create_jwt_manager(self):
545
+ def create_jwt_manager(self) -> None:
538
546
  """Create the JWT manager."""
539
547
  jwt_manager = JWTManager()
540
548
  jwt_manager.init_app(current_app)
@@ -550,6 +558,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
550
558
  :param password: the clear text password to reset and save hashed on the db
551
559
  """
552
560
  user = self.get_user_by_id(userid)
561
+ if not user:
562
+ return False
553
563
  user.password = generate_password_hash(password)
554
564
  self.reset_user_sessions(user)
555
565
  return self.update_user(user)
@@ -591,16 +601,17 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
591
601
  "warning",
592
602
  )
593
603
 
594
- def load_user_jwt(self, _jwt_header, jwt_data):
604
+ def load_user_jwt(self, _jwt_header, jwt_data) -> User | None:
595
605
  identity = jwt_data["sub"]
596
606
  user = self.load_user(identity)
597
607
  if user and user.is_active:
598
608
  # Set flask g.user to JWT user, we can't do it on before request
599
609
  g.user = user
600
610
  return user
611
+ return None
601
612
 
602
613
  @property
603
- def auth_type(self):
614
+ def auth_type(self) -> int:
604
615
  """Get the auth type."""
605
616
  return current_app.config["AUTH_TYPE"]
606
617
 
@@ -737,11 +748,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
737
748
  def auth_remote_user_env_var(self) -> str:
738
749
  return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
739
750
 
740
- @property
741
- def auth_username_ci(self):
742
- """Get the auth username for CI."""
743
- return current_app.config.get("AUTH_USERNAME_CI", True)
744
-
745
751
  @property
746
752
  def auth_user_registration(self):
747
753
  """Will user self registration be allowed."""
@@ -763,15 +769,15 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
763
769
  return current_app.config["AUTH_ROLE_ADMIN"]
764
770
 
765
771
  @property
766
- def oauth_whitelists(self):
772
+ def oauth_whitelists(self) -> dict[str, list]:
767
773
  return self.oauth_allow_list
768
774
 
769
775
  @staticmethod
770
- def create_builtin_roles():
776
+ def create_builtin_roles() -> dict:
771
777
  return current_app.config.get("FAB_ROLES", {})
772
778
 
773
779
  @property
774
- def builtin_roles(self):
780
+ def builtin_roles(self) -> dict:
775
781
  """Get the builtin roles."""
776
782
  return self._builtin_roles
777
783
 
@@ -780,11 +786,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
780
786
  return current_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
781
787
 
782
788
  @property
783
- def auth_type_provider_name(self):
789
+ def auth_type_provider_name(self) -> str | None:
784
790
  provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
785
791
  return provider_to_auth_type.get(self.auth_type)
786
792
 
787
- def _init_config(self):
793
+ def _init_config(self) -> None:
788
794
  """
789
795
  Initialize config.
790
796
 
@@ -854,7 +860,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
854
860
  current_app.config.setdefault("AUTH_RATE_LIMITED", True)
855
861
  current_app.config.setdefault("AUTH_RATE_LIMIT", "5 per 40 second")
856
862
 
857
- def _init_auth(self):
863
+ def _init_auth(self) -> None:
858
864
  """
859
865
  Initialize authentication configuration.
860
866
 
@@ -877,7 +883,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
877
883
  self.oauth_allow_list[provider_name] = provider["whitelist"]
878
884
  self.oauth_remotes[provider_name] = obj_provider
879
885
 
880
- def _init_data_model(self):
886
+ def _init_data_model(self) -> None:
881
887
  user_data_model = SQLAInterface(self.user_model)
882
888
  if self.auth_type == const.AUTH_DB:
883
889
  self.userdbmodelview.datamodel = user_data_model
@@ -888,8 +894,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
888
894
  elif self.auth_type == const.AUTH_REMOTE_USER:
889
895
  self.userremoteusermodelview.datamodel = user_data_model
890
896
 
891
- if self.userstatschartview:
892
- self.userstatschartview.datamodel = user_data_model
897
+ self.userstatschartview.datamodel = user_data_model
893
898
  if self.auth_user_registration:
894
899
  self.registerusermodelview.datamodel = SQLAInterface(self.registeruser_model)
895
900
 
@@ -899,7 +904,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
899
904
  self.resourcemodelview.datamodel = SQLAInterface(self.resource_model)
900
905
  self.permissionmodelview.datamodel = SQLAInterface(self.permission_model)
901
906
 
902
- def create_db(self):
907
+ def create_db(self) -> None:
903
908
  """
904
909
  Create the database.
905
910
 
@@ -1052,7 +1057,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1052
1057
  for rolename, resource_actions_raw in access_control.items():
1053
1058
  role = self.find_role(rolename)
1054
1059
  if not role:
1055
- raise AirflowException(
1060
+ raise FabException(
1056
1061
  f"The access_control mapping for DAG '{dag_id}' includes a role named "
1057
1062
  f"'{rolename}', but that role does not exist"
1058
1063
  )
@@ -1066,7 +1071,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1066
1071
 
1067
1072
  for resource_name, actions in resource_actions.items():
1068
1073
  if resource_name not in self.RESOURCE_DETAILS_MAP:
1069
- raise AirflowException(
1074
+ raise FabException(
1070
1075
  f"The access_control map for DAG '{dag_id}' includes the following invalid "
1071
1076
  f"resource name: '{resource_name}'; "
1072
1077
  f"The set of valid resource names is: {self.RESOURCE_DETAILS_MAP.keys()}"
@@ -1078,7 +1083,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1078
1083
  invalid_actions = set(actions) - self.RESOURCE_DETAILS_MAP[resource_name]["actions"]
1079
1084
 
1080
1085
  if invalid_actions:
1081
- raise AirflowException(
1086
+ raise FabException(
1082
1087
  f"The access_control map for DAG '{dag_resource_name}' includes "
1083
1088
  f"the following invalid permissions: {invalid_actions}; "
1084
1089
  f"The set of valid permissions is: {self.RESOURCE_DETAILS_MAP[resource_name]['actions']}"
@@ -1089,7 +1094,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1089
1094
  if dag_perm:
1090
1095
  self.add_permission_to_role(role, dag_perm)
1091
1096
 
1092
- def add_permissions_view(self, base_action_names, resource_name): # Keep name for compatibility with FAB.
1097
+ def add_permissions_view(
1098
+ self, base_action_names, resource_name
1099
+ ) -> None: # Keep name for compatibility with FAB.
1093
1100
  """
1094
1101
  Add an action on a resource to the backend.
1095
1102
 
@@ -1114,6 +1121,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1114
1121
  else:
1115
1122
  # Permissions on this view exist but....
1116
1123
  admin_role = self.find_role(self.auth_role_admin)
1124
+ if not admin_role:
1125
+ admin_role = self.add_role(self.auth_role_admin)
1117
1126
  for action_name in base_action_names:
1118
1127
  # Check if base view permissions exist
1119
1128
  if not self.perms_include_action(perms, action_name):
@@ -1137,7 +1146,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1137
1146
  # Role Admin must have all permissions
1138
1147
  self.add_permission_to_role(admin_role, perm)
1139
1148
 
1140
- def add_permissions_menu(self, resource_name):
1149
+ def add_permissions_menu(self, resource_name) -> None:
1141
1150
  """
1142
1151
  Add menu_access to resource on permission_resource.
1143
1152
 
@@ -1150,6 +1159,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1150
1159
  perm = self.create_permission("menu_access", resource_name)
1151
1160
  if self.auth_role_admin not in self.builtin_roles:
1152
1161
  role_admin = self.find_role(self.auth_role_admin)
1162
+ if not role_admin:
1163
+ role_admin = self.add_role(self.auth_role_admin)
1153
1164
  self.add_permission_to_role(role_admin, perm)
1154
1165
 
1155
1166
  def sync_roles(self) -> None:
@@ -1204,6 +1215,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1204
1215
  perms = [p for p in perms if p.action and p.resource]
1205
1216
 
1206
1217
  admin = self.find_role("Admin")
1218
+ if not admin:
1219
+ admin = self.add_role("Admin")
1207
1220
  admin.permissions = list(set(admin.permissions) | set(perms))
1208
1221
 
1209
1222
  self.session.commit()
@@ -1231,7 +1244,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1231
1244
  if deleted_count:
1232
1245
  self.log.info("Deleted %s faulty permissions", deleted_count)
1233
1246
 
1234
- def perms_include_action(self, perms, action_name):
1247
+ def perms_include_action(self, perms, action_name) -> bool:
1235
1248
  return any(perm.action and perm.action.name == action_name for perm in perms)
1236
1249
 
1237
1250
  def bulk_sync_roles(self, roles: Iterable[dict[str, Any]]) -> None:
@@ -1288,9 +1301,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1288
1301
  except Exception as e:
1289
1302
  log.error(const.LOGMSG_ERR_SEC_ADD_ROLE, e)
1290
1303
  self.session.rollback()
1304
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_ROLE) from e
1291
1305
  return role
1292
1306
 
1293
- def find_role(self, name):
1307
+ def find_role(self, name) -> Role | None:
1294
1308
  """
1295
1309
  Find a role in the database.
1296
1310
 
@@ -1298,7 +1312,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1298
1312
  """
1299
1313
  return self.session.scalars(select(self.role_model).filter_by(name=name)).unique().one_or_none()
1300
1314
 
1301
- def get_all_roles(self):
1315
+ def get_all_roles(self) -> list[Role]:
1302
1316
  return self.session.scalars(select(self.role_model)).unique().all()
1303
1317
 
1304
1318
  def delete_role(self, role_name: str) -> None:
@@ -1313,7 +1327,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1313
1327
  self.session.execute(delete(Role).where(Role.name == role_name))
1314
1328
  self.session.commit()
1315
1329
  else:
1316
- raise AirflowException(f"Role named '{role_name}' does not exist")
1330
+ raise FabException(f"Role named '{role_name}' does not exist")
1317
1331
 
1318
1332
  def get_roles_from_keys(self, role_keys: list[str]) -> set[Role]:
1319
1333
  """
@@ -1340,7 +1354,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1340
1354
  )
1341
1355
  return _roles
1342
1356
 
1343
- def get_public_role(self):
1357
+ def get_public_role(self) -> Role | None:
1344
1358
  return (
1345
1359
  self.session.scalars(select(self.role_model).filter_by(name=self.auth_role_public))
1346
1360
  .unique()
@@ -1363,7 +1377,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1363
1377
  password: str = "",
1364
1378
  hashed_password: str = "",
1365
1379
  groups: list[Group] | None = None,
1366
- ):
1380
+ ) -> User:
1367
1381
  """Create a user."""
1368
1382
  roles: list[Role] = []
1369
1383
  if role:
@@ -1390,22 +1404,24 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1390
1404
  except Exception as e:
1391
1405
  log.error(const.LOGMSG_ERR_SEC_ADD_USER, e)
1392
1406
  self.session.rollback()
1393
- return False
1407
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_USER) from e
1394
1408
 
1395
- def load_user(self, pk: int) -> Any | None:
1409
+ def load_user(self, pk: int) -> User | None:
1396
1410
  user = self.get_user_by_id(int(pk))
1397
1411
  if user and user.is_active:
1398
1412
  return user
1399
1413
  return None
1400
1414
 
1401
- def get_user_by_id(self, pk):
1415
+ def get_user_by_id(self, pk) -> User | None:
1402
1416
  return self.session.get(self.user_model, pk)
1403
1417
 
1404
- def count_users(self):
1418
+ def count_users(self) -> int:
1405
1419
  """Return the number of users in the database."""
1406
1420
  return self.session.scalar(select(func.count(self.user_model.id)))
1407
1421
 
1408
- def add_register_user(self, username, first_name, last_name, email, password="", hashed_password=""):
1422
+ def add_register_user(
1423
+ self, username, first_name, last_name, email, password="", hashed_password=""
1424
+ ) -> RegisterUser | None:
1409
1425
  """
1410
1426
  Add a registration request for the user.
1411
1427
 
@@ -1430,16 +1446,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1430
1446
  self.session.rollback()
1431
1447
  return None
1432
1448
 
1433
- def find_user(self, username=None, email=None):
1449
+ def find_user(self, username=None, email=None) -> User | None:
1434
1450
  """Find user by username or email."""
1435
1451
  if username:
1436
1452
  try:
1437
- if self.auth_username_ci:
1438
- return self.session.scalars(
1439
- select(self.user_model).where(
1440
- func.lower(self.user_model.username) == func.lower(username)
1441
- )
1442
- ).one_or_none()
1443
1453
  return self.session.scalars(
1444
1454
  select(self.user_model).where(
1445
1455
  func.lower(self.user_model.username) == func.lower(username)
@@ -1447,13 +1457,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1447
1457
  ).one_or_none()
1448
1458
  except MultipleResultsFound:
1449
1459
  log.error("Multiple results found for user %s", username)
1450
- return None
1451
1460
  elif email:
1452
1461
  try:
1453
1462
  return self.session.scalars(select(self.user_model).filter_by(email=email)).one_or_none()
1454
1463
  except MultipleResultsFound:
1455
1464
  log.error("Multiple results found for user with email %s", email)
1456
- return None
1465
+ return None
1457
1466
 
1458
1467
  def update_user(self, user: User) -> bool:
1459
1468
  try:
@@ -1466,7 +1475,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1466
1475
  return False
1467
1476
  return True
1468
1477
 
1469
- def del_register_user(self, register_user):
1478
+ def del_register_user(self, register_user) -> bool:
1470
1479
  """
1471
1480
  Delete registration object from database.
1472
1481
 
@@ -1481,10 +1490,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1481
1490
  self.session.rollback()
1482
1491
  return False
1483
1492
 
1484
- def get_all_users(self):
1493
+ def get_all_users(self) -> list[User]:
1485
1494
  return self.session.scalars(select(self.user_model)).all()
1486
1495
 
1487
- def update_user_auth_stat(self, user, success=True):
1496
+ def update_user_auth_stat(self, user, success=True) -> None:
1488
1497
  """
1489
1498
  Update user authentication stats.
1490
1499
 
@@ -1524,7 +1533,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1524
1533
  """
1525
1534
  return self.session.scalars(select(self.action_model).filter_by(name=name)).one_or_none()
1526
1535
 
1527
- def create_action(self, name):
1536
+ def create_action(self, name) -> Action:
1528
1537
  """
1529
1538
  Add an action to the backend, model action.
1530
1539
 
@@ -1583,7 +1592,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1583
1592
  """
1584
1593
  return self.session.scalars(select(self.resource_model).filter_by(name=name)).one_or_none()
1585
1594
 
1586
- def create_resource(self, name) -> Resource | None:
1595
+ def create_resource(self, name) -> Resource:
1587
1596
  """
1588
1597
  Create a resource with the given name.
1589
1598
 
@@ -1600,6 +1609,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1600
1609
  except Exception as e:
1601
1610
  log.error(const.LOGMSG_ERR_SEC_ADD_VIEWMENU, e)
1602
1611
  self.session.rollback()
1612
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_VIEWMENU) from e
1603
1613
  return resource
1604
1614
 
1605
1615
  """
@@ -1738,7 +1748,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1738
1748
  self.session.rollback()
1739
1749
 
1740
1750
  @staticmethod
1741
- def get_user_roles(user=None):
1751
+ def get_user_roles(user=None) -> list[Role]:
1742
1752
  """
1743
1753
  Get all the roles associated with the user.
1744
1754
 
@@ -1755,7 +1765,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1755
1765
  --------------------
1756
1766
  """
1757
1767
 
1758
- def auth_user_ldap(self, username, password, rotate_session_id=True):
1768
+ def auth_user_ldap(self, username, password, rotate_session_id=True) -> User | None:
1759
1769
  """
1760
1770
  Authenticate user with LDAP.
1761
1771
 
@@ -1948,11 +1958,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1948
1958
  user = self.find_user(username=username)
1949
1959
  if user is None:
1950
1960
  user = self.find_user(email=username)
1951
- if user is None:
1961
+ if user is None or user.password is None:
1952
1962
  return False
1953
1963
  return check_password_hash(user.password, password)
1954
1964
 
1955
- def auth_user_db(self, username, password, rotate_session_id=True):
1965
+ def auth_user_db(self, username, password, rotate_session_id=True) -> User | None:
1956
1966
  """
1957
1967
  Authenticate user, auth db style.
1958
1968
 
@@ -1976,6 +1986,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1976
1986
  )
1977
1987
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
1978
1988
  return None
1989
+ if user.password is None:
1990
+ return None
1979
1991
  if check_password_hash(user.password, password):
1980
1992
  if rotate_session_id:
1981
1993
  self._rotate_session_id()
@@ -1985,7 +1997,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1985
1997
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
1986
1998
  return None
1987
1999
 
1988
- def set_oauth_session(self, provider, oauth_response):
2000
+ def set_oauth_session(self, provider, oauth_response) -> None:
1989
2001
  """Set the current session with OAuth user secrets."""
1990
2002
  # Get this provider key names for token_key and token_secret
1991
2003
  token_key = self.get_oauth_token_key_name(provider)
@@ -2019,7 +2031,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2019
2031
  if _provider["name"] == provider:
2020
2032
  return _provider.get("token_secret", "oauth_token_secret")
2021
2033
 
2022
- def auth_user_oauth(self, userinfo):
2034
+ def auth_user_oauth(self, userinfo, rotate_session_id=True) -> User | None:
2023
2035
  """
2024
2036
  Authenticate user with OAuth.
2025
2037
 
@@ -2073,7 +2085,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2073
2085
 
2074
2086
  # LOGIN SUCCESS (only if user is now registered)
2075
2087
  if user:
2076
- self._rotate_session_id()
2088
+ if rotate_session_id:
2089
+ self._rotate_session_id()
2077
2090
  self.update_user_auth_stat(user)
2078
2091
  return user
2079
2092
  return None
@@ -2217,7 +2230,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2217
2230
  # decode - if empty string, default to fallback, otherwise take first element
2218
2231
  return raw_value[0].decode("utf-8") or fallback
2219
2232
 
2220
- def auth_user_remote_user(self, username):
2233
+ def auth_user_remote_user(self, username) -> User | None:
2221
2234
  """
2222
2235
  REMOTE_USER user Authentication.
2223
2236
 
@@ -2253,7 +2266,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2253
2266
  ---------------
2254
2267
  """
2255
2268
 
2256
- def _rotate_session_id(self):
2269
+ def _rotate_session_id(self) -> None:
2257
2270
  """
2258
2271
  Rotate the session ID.
2259
2272
 
@@ -2261,7 +2274,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2261
2274
  database session backend.
2262
2275
  """
2263
2276
  if conf.get("fab", "SESSION_BACKEND") == "database":
2264
- session.sid = str(uuid.uuid4())
2277
+ session.sid = str(uuid.uuid4()) # type: ignore
2265
2278
 
2266
2279
  def _get_microsoft_jwks(self) -> list[dict[str, Any]]:
2267
2280
  import requests
@@ -2273,7 +2286,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2273
2286
  if verify_signature:
2274
2287
  from authlib.jose import JsonWebKey, jwt as authlib_jwt
2275
2288
 
2276
- keyset = JsonWebKey.import_key_set(self._get_microsoft_jwks())
2289
+ keyset = JsonWebKey.import_key_set(self._get_microsoft_jwks()) # type: ignore
2277
2290
  claims = authlib_jwt.decode(id_token, keyset)
2278
2291
  claims.validate()
2279
2292
  return claims
@@ -2415,7 +2428,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2415
2428
  except ldap.INVALID_CREDENTIALS:
2416
2429
  return False
2417
2430
 
2418
- def _ldap_calculate_user_roles(self, user_attributes: dict[str, list[bytes]]) -> list[str]:
2431
+ def _ldap_calculate_user_roles(self, user_attributes: dict[str, list[bytes]]) -> list[Role]:
2419
2432
  user_role_objects = set()
2420
2433
 
2421
2434
  # apply AUTH_ROLES_MAPPING
@@ -2495,7 +2508,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2495
2508
  else:
2496
2509
  getattr(log, level)(text.replace("<br>", "\n").replace("<b>", "*").replace("</b>", "*"))
2497
2510
 
2498
- def _oauth_calculate_user_roles(self, userinfo) -> list[str]:
2511
+ def _oauth_calculate_user_roles(self, userinfo) -> list[Role]:
2499
2512
  user_role_objects = set()
2500
2513
 
2501
2514
  # apply AUTH_ROLES_MAPPING
@@ -26,6 +26,7 @@ from flask_appbuilder.security.views import (
26
26
  UserOAuthModelView,
27
27
  UserRemoteUserModelView,
28
28
  )
29
+ from wtforms.validators import DataRequired
29
30
 
30
31
  from airflow.providers.fab.www.security import permissions
31
32
 
@@ -186,6 +187,17 @@ class CustomUserDBModelView(MultiResourceUserMixin, UserDBModelView):
186
187
  "conf_password",
187
188
  ]
188
189
 
190
+ edit_columns = [
191
+ "first_name",
192
+ "last_name",
193
+ "username",
194
+ "active",
195
+ "email",
196
+ "roles",
197
+ ]
198
+
199
+ validators_columns = {"roles": [DataRequired()]}
200
+
189
201
  base_permissions = [
190
202
  permissions.ACTION_CAN_CREATE,
191
203
  permissions.ACTION_CAN_READ,
@@ -0,0 +1,18 @@
1
+ #
2
+ # Licensed to the Apache Software Foundation (ASF) under one
3
+ # or more contributor license agreements. See the NOTICE file
4
+ # distributed with this work for additional information
5
+ # regarding copyright ownership. The ASF licenses this file
6
+ # to you under the Apache License, Version 2.0 (the
7
+ # "License"); you may not use this file except in compliance
8
+ # with the License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing,
13
+ # software distributed under the License is distributed on an
14
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ # KIND, either express or implied. See the License for the
16
+ # specific language governing permissions and limitations
17
+ # under the License.
18
+ from __future__ import annotations