apache-airflow-providers-fab 3.1.1rc1__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 (49) 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/user_command.py +3 -3
  12. airflow/providers/fab/auth_manager/fab_auth_manager.py +18 -51
  13. airflow/providers/fab/auth_manager/models/__init__.py +6 -6
  14. airflow/providers/fab/auth_manager/security_manager/override.py +90 -77
  15. airflow/providers/fab/auth_manager/views/user.py +12 -0
  16. airflow/providers/fab/cli/__init__.py +18 -0
  17. airflow/providers/fab/{auth_manager/cli_commands → cli}/definition.py +50 -2
  18. airflow/providers/fab/get_provider_info.py +8 -0
  19. airflow/providers/fab/www/app.py +2 -7
  20. airflow/providers/fab/www/extensions/init_appbuilder.py +3 -2
  21. airflow/providers/fab/www/package-lock.json +669 -531
  22. airflow/providers/fab/www/package.json +9 -9
  23. airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js → 743.8fb7d21632ed892227fe.js} +2 -2
  24. airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.js → airflowDefaultTheme.51e5d14856ee1ebc83ca.js} +1 -1
  25. airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.js → flash.865b6940c00b2a9041b3.js} +1 -1
  26. airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.js → loadingDots.07f5b9805847242736e1.js} +1 -1
  27. airflow/providers/fab/www/static/dist/main.8cffe40bcf7cca998f4e.js +2 -0
  28. airflow/providers/fab/www/static/dist/manifest.json +13 -13
  29. airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.js → materialIcons.4fe84ae36604d84dec78.js} +1 -1
  30. airflow/providers/fab/www/static/dist/moment.0ec3ee3fb60dc999b1fd.js +1 -0
  31. airflow/providers/fab/www/static/js/main.js +11 -0
  32. airflow/providers/fab/www/templates/airflow/main.html +1 -0
  33. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/METADATA +10 -10
  34. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/RECORD +47 -43
  35. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +1 -1
  36. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/NOTICE +1 -1
  37. airflow/providers/fab/www/static/dist/main.bc1f701c3d133e2a3bab.js +0 -2
  38. airflow/providers/fab/www/static/dist/moment.5b85b4f6be2fe9c405ac.js +0 -1
  39. /airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js.LICENSE.txt → 743.8fb7d21632ed892227fe.js.LICENSE.txt} +0 -0
  40. /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.css → airflowDefaultTheme.51e5d14856ee1ebc83ca.css} +0 -0
  41. /airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.css → flash.865b6940c00b2a9041b3.css} +0 -0
  42. /airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.css → loadingDots.07f5b9805847242736e1.css} +0 -0
  43. /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.css → main.8cffe40bcf7cca998f4e.css} +0 -0
  44. /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.js.LICENSE.txt → main.8cffe40bcf7cca998f4e.js.LICENSE.txt} +0 -0
  45. /airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.css → materialIcons.4fe84ae36604d84dec78.css} +0 -0
  46. /airflow/providers/fab/www/static/dist/{runtime.254c277d91ce3ac79c64.js → runtime.45b36fb8335446865b53.js} +0 -0
  47. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/WHEEL +0 -0
  48. {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/entry_points.txt +0 -0
  49. {apache_airflow_providers_fab-3.1.1rc1.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,
@@ -103,6 +105,8 @@ from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
103
105
  from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
104
106
 
105
107
  if TYPE_CHECKING:
108
+ from authlib.integrations.flask_client import OAuth
109
+
106
110
  from airflow.providers.fab.www.security.permissions import (
107
111
  RESOURCE_ASSET,
108
112
  RESOURCE_ASSET_ALIAS,
@@ -148,6 +152,10 @@ log = logging.getLogger(__name__)
148
152
  MAX_NUM_DATABASE_USER_SESSIONS = 50000
149
153
 
150
154
 
155
+ class FabException(Exception):
156
+ """Custom exception for FAB security manager."""
157
+
158
+
151
159
  class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
152
160
  """
153
161
  This security manager overrides the default AirflowSecurityManager security manager.
@@ -159,11 +167,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
159
167
  :param appbuilder: The appbuilder.
160
168
  """
161
169
 
162
- auth_view = None
170
+ auth_view: AuthView | None = None
163
171
  """ The obj instance for authentication view """
164
- registeruser_view = None
172
+ registeruser_view: BaseRegisterUser | None = None
165
173
  """ The obj instance for registering user view """
166
- user_view = None
174
+ user_view: type[UserModelView] | None = None
167
175
  """ The obj instance for user view """
168
176
 
169
177
  """ Models """
@@ -207,9 +215,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
207
215
  security_api = SecurityApi
208
216
  """ Override if you want your own Security API login endpoint """
209
217
 
210
- jwt_manager = None
218
+ jwt_manager: JWTManager | None = None
211
219
  """ Flask-JWT-Extended """
212
- oauth = None
220
+ oauth: OAuth | None = None
213
221
  oauth_remotes: dict[str, Any]
214
222
  """ Initialized (remote_app) providers dict {'provider_name', OBJ } """
215
223
 
@@ -304,6 +312,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
304
312
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_VARIABLE),
305
313
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
306
314
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
315
+ (permissions.ACTION_CAN_CREATE, permissions.RESOURCE_XCOM),
316
+ (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_XCOM),
307
317
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
308
318
  (permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
309
319
  (permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
@@ -417,9 +427,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
417
427
  log.warning("JWT token is not validated!")
418
428
  return me
419
429
 
420
- raise AirflowException("OAuth signature verify failed")
430
+ raise FabException("OAuth signature verify failed")
421
431
 
422
- def register_views(self):
432
+ def register_views(self) -> None:
423
433
  """Register FAB auth manager related views."""
424
434
  if not current_app.config.get("FAB_ADD_SECURITY_VIEWS", True):
425
435
  return
@@ -456,7 +466,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
456
466
 
457
467
  # this needs to be done after the view is added, otherwise the blueprint
458
468
  # is not initialized
459
- if self.is_auth_limited:
469
+ if self.is_auth_limited and self.auth_view:
460
470
  self.limiter.limit(self.auth_rate_limit, methods=["POST"])(self.auth_view.blueprint)
461
471
 
462
472
  self.user_view = self.appbuilder.add_view(
@@ -479,14 +489,13 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
479
489
  )
480
490
  role_view.related_views = [self.user_view.__class__]
481
491
 
482
- if self.userstatschartview:
483
- self.appbuilder.add_view(
484
- self.userstatschartview,
485
- "User's Statistics",
486
- icon="fa-bar-chart-o",
487
- label=lazy_gettext("User's Statistics"),
488
- category="Security",
489
- )
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
+ )
490
499
  if self.auth_user_registration:
491
500
  self.appbuilder.add_view(
492
501
  self.registerusermodelview,
@@ -533,7 +542,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
533
542
  lm.user_loader(self.load_user)
534
543
  return lm
535
544
 
536
- def create_jwt_manager(self):
545
+ def create_jwt_manager(self) -> None:
537
546
  """Create the JWT manager."""
538
547
  jwt_manager = JWTManager()
539
548
  jwt_manager.init_app(current_app)
@@ -549,6 +558,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
549
558
  :param password: the clear text password to reset and save hashed on the db
550
559
  """
551
560
  user = self.get_user_by_id(userid)
561
+ if not user:
562
+ return False
552
563
  user.password = generate_password_hash(password)
553
564
  self.reset_user_sessions(user)
554
565
  return self.update_user(user)
@@ -590,16 +601,17 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
590
601
  "warning",
591
602
  )
592
603
 
593
- def load_user_jwt(self, _jwt_header, jwt_data):
604
+ def load_user_jwt(self, _jwt_header, jwt_data) -> User | None:
594
605
  identity = jwt_data["sub"]
595
606
  user = self.load_user(identity)
596
607
  if user and user.is_active:
597
608
  # Set flask g.user to JWT user, we can't do it on before request
598
609
  g.user = user
599
610
  return user
611
+ return None
600
612
 
601
613
  @property
602
- def auth_type(self):
614
+ def auth_type(self) -> int:
603
615
  """Get the auth type."""
604
616
  return current_app.config["AUTH_TYPE"]
605
617
 
@@ -736,11 +748,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
736
748
  def auth_remote_user_env_var(self) -> str:
737
749
  return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
738
750
 
739
- @property
740
- def auth_username_ci(self):
741
- """Get the auth username for CI."""
742
- return current_app.config.get("AUTH_USERNAME_CI", True)
743
-
744
751
  @property
745
752
  def auth_user_registration(self):
746
753
  """Will user self registration be allowed."""
@@ -762,15 +769,15 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
762
769
  return current_app.config["AUTH_ROLE_ADMIN"]
763
770
 
764
771
  @property
765
- def oauth_whitelists(self):
772
+ def oauth_whitelists(self) -> dict[str, list]:
766
773
  return self.oauth_allow_list
767
774
 
768
775
  @staticmethod
769
- def create_builtin_roles():
776
+ def create_builtin_roles() -> dict:
770
777
  return current_app.config.get("FAB_ROLES", {})
771
778
 
772
779
  @property
773
- def builtin_roles(self):
780
+ def builtin_roles(self) -> dict:
774
781
  """Get the builtin roles."""
775
782
  return self._builtin_roles
776
783
 
@@ -779,11 +786,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
779
786
  return current_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
780
787
 
781
788
  @property
782
- def auth_type_provider_name(self):
789
+ def auth_type_provider_name(self) -> str | None:
783
790
  provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
784
791
  return provider_to_auth_type.get(self.auth_type)
785
792
 
786
- def _init_config(self):
793
+ def _init_config(self) -> None:
787
794
  """
788
795
  Initialize config.
789
796
 
@@ -853,7 +860,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
853
860
  current_app.config.setdefault("AUTH_RATE_LIMITED", True)
854
861
  current_app.config.setdefault("AUTH_RATE_LIMIT", "5 per 40 second")
855
862
 
856
- def _init_auth(self):
863
+ def _init_auth(self) -> None:
857
864
  """
858
865
  Initialize authentication configuration.
859
866
 
@@ -876,7 +883,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
876
883
  self.oauth_allow_list[provider_name] = provider["whitelist"]
877
884
  self.oauth_remotes[provider_name] = obj_provider
878
885
 
879
- def _init_data_model(self):
886
+ def _init_data_model(self) -> None:
880
887
  user_data_model = SQLAInterface(self.user_model)
881
888
  if self.auth_type == const.AUTH_DB:
882
889
  self.userdbmodelview.datamodel = user_data_model
@@ -887,8 +894,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
887
894
  elif self.auth_type == const.AUTH_REMOTE_USER:
888
895
  self.userremoteusermodelview.datamodel = user_data_model
889
896
 
890
- if self.userstatschartview:
891
- self.userstatschartview.datamodel = user_data_model
897
+ self.userstatschartview.datamodel = user_data_model
892
898
  if self.auth_user_registration:
893
899
  self.registerusermodelview.datamodel = SQLAInterface(self.registeruser_model)
894
900
 
@@ -898,7 +904,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
898
904
  self.resourcemodelview.datamodel = SQLAInterface(self.resource_model)
899
905
  self.permissionmodelview.datamodel = SQLAInterface(self.permission_model)
900
906
 
901
- def create_db(self):
907
+ def create_db(self) -> None:
902
908
  """
903
909
  Create the database.
904
910
 
@@ -1051,7 +1057,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1051
1057
  for rolename, resource_actions_raw in access_control.items():
1052
1058
  role = self.find_role(rolename)
1053
1059
  if not role:
1054
- raise AirflowException(
1060
+ raise FabException(
1055
1061
  f"The access_control mapping for DAG '{dag_id}' includes a role named "
1056
1062
  f"'{rolename}', but that role does not exist"
1057
1063
  )
@@ -1065,7 +1071,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1065
1071
 
1066
1072
  for resource_name, actions in resource_actions.items():
1067
1073
  if resource_name not in self.RESOURCE_DETAILS_MAP:
1068
- raise AirflowException(
1074
+ raise FabException(
1069
1075
  f"The access_control map for DAG '{dag_id}' includes the following invalid "
1070
1076
  f"resource name: '{resource_name}'; "
1071
1077
  f"The set of valid resource names is: {self.RESOURCE_DETAILS_MAP.keys()}"
@@ -1077,7 +1083,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1077
1083
  invalid_actions = set(actions) - self.RESOURCE_DETAILS_MAP[resource_name]["actions"]
1078
1084
 
1079
1085
  if invalid_actions:
1080
- raise AirflowException(
1086
+ raise FabException(
1081
1087
  f"The access_control map for DAG '{dag_resource_name}' includes "
1082
1088
  f"the following invalid permissions: {invalid_actions}; "
1083
1089
  f"The set of valid permissions is: {self.RESOURCE_DETAILS_MAP[resource_name]['actions']}"
@@ -1088,7 +1094,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1088
1094
  if dag_perm:
1089
1095
  self.add_permission_to_role(role, dag_perm)
1090
1096
 
1091
- 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.
1092
1100
  """
1093
1101
  Add an action on a resource to the backend.
1094
1102
 
@@ -1113,6 +1121,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1113
1121
  else:
1114
1122
  # Permissions on this view exist but....
1115
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)
1116
1126
  for action_name in base_action_names:
1117
1127
  # Check if base view permissions exist
1118
1128
  if not self.perms_include_action(perms, action_name):
@@ -1136,7 +1146,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1136
1146
  # Role Admin must have all permissions
1137
1147
  self.add_permission_to_role(admin_role, perm)
1138
1148
 
1139
- def add_permissions_menu(self, resource_name):
1149
+ def add_permissions_menu(self, resource_name) -> None:
1140
1150
  """
1141
1151
  Add menu_access to resource on permission_resource.
1142
1152
 
@@ -1149,6 +1159,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1149
1159
  perm = self.create_permission("menu_access", resource_name)
1150
1160
  if self.auth_role_admin not in self.builtin_roles:
1151
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)
1152
1164
  self.add_permission_to_role(role_admin, perm)
1153
1165
 
1154
1166
  def sync_roles(self) -> None:
@@ -1203,6 +1215,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1203
1215
  perms = [p for p in perms if p.action and p.resource]
1204
1216
 
1205
1217
  admin = self.find_role("Admin")
1218
+ if not admin:
1219
+ admin = self.add_role("Admin")
1206
1220
  admin.permissions = list(set(admin.permissions) | set(perms))
1207
1221
 
1208
1222
  self.session.commit()
@@ -1230,7 +1244,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1230
1244
  if deleted_count:
1231
1245
  self.log.info("Deleted %s faulty permissions", deleted_count)
1232
1246
 
1233
- def perms_include_action(self, perms, action_name):
1247
+ def perms_include_action(self, perms, action_name) -> bool:
1234
1248
  return any(perm.action and perm.action.name == action_name for perm in perms)
1235
1249
 
1236
1250
  def bulk_sync_roles(self, roles: Iterable[dict[str, Any]]) -> None:
@@ -1287,9 +1301,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1287
1301
  except Exception as e:
1288
1302
  log.error(const.LOGMSG_ERR_SEC_ADD_ROLE, e)
1289
1303
  self.session.rollback()
1304
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_ROLE) from e
1290
1305
  return role
1291
1306
 
1292
- def find_role(self, name):
1307
+ def find_role(self, name) -> Role | None:
1293
1308
  """
1294
1309
  Find a role in the database.
1295
1310
 
@@ -1297,7 +1312,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1297
1312
  """
1298
1313
  return self.session.scalars(select(self.role_model).filter_by(name=name)).unique().one_or_none()
1299
1314
 
1300
- def get_all_roles(self):
1315
+ def get_all_roles(self) -> list[Role]:
1301
1316
  return self.session.scalars(select(self.role_model)).unique().all()
1302
1317
 
1303
1318
  def delete_role(self, role_name: str) -> None:
@@ -1312,7 +1327,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1312
1327
  self.session.execute(delete(Role).where(Role.name == role_name))
1313
1328
  self.session.commit()
1314
1329
  else:
1315
- raise AirflowException(f"Role named '{role_name}' does not exist")
1330
+ raise FabException(f"Role named '{role_name}' does not exist")
1316
1331
 
1317
1332
  def get_roles_from_keys(self, role_keys: list[str]) -> set[Role]:
1318
1333
  """
@@ -1339,7 +1354,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1339
1354
  )
1340
1355
  return _roles
1341
1356
 
1342
- def get_public_role(self):
1357
+ def get_public_role(self) -> Role | None:
1343
1358
  return (
1344
1359
  self.session.scalars(select(self.role_model).filter_by(name=self.auth_role_public))
1345
1360
  .unique()
@@ -1362,7 +1377,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1362
1377
  password: str = "",
1363
1378
  hashed_password: str = "",
1364
1379
  groups: list[Group] | None = None,
1365
- ):
1380
+ ) -> User:
1366
1381
  """Create a user."""
1367
1382
  roles: list[Role] = []
1368
1383
  if role:
@@ -1389,22 +1404,24 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1389
1404
  except Exception as e:
1390
1405
  log.error(const.LOGMSG_ERR_SEC_ADD_USER, e)
1391
1406
  self.session.rollback()
1392
- return False
1407
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_USER) from e
1393
1408
 
1394
- def load_user(self, pk: int) -> Any | None:
1409
+ def load_user(self, pk: int) -> User | None:
1395
1410
  user = self.get_user_by_id(int(pk))
1396
1411
  if user and user.is_active:
1397
1412
  return user
1398
1413
  return None
1399
1414
 
1400
- def get_user_by_id(self, pk):
1415
+ def get_user_by_id(self, pk) -> User | None:
1401
1416
  return self.session.get(self.user_model, pk)
1402
1417
 
1403
- def count_users(self):
1418
+ def count_users(self) -> int:
1404
1419
  """Return the number of users in the database."""
1405
1420
  return self.session.scalar(select(func.count(self.user_model.id)))
1406
1421
 
1407
- 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:
1408
1425
  """
1409
1426
  Add a registration request for the user.
1410
1427
 
@@ -1429,16 +1446,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1429
1446
  self.session.rollback()
1430
1447
  return None
1431
1448
 
1432
- def find_user(self, username=None, email=None):
1449
+ def find_user(self, username=None, email=None) -> User | None:
1433
1450
  """Find user by username or email."""
1434
1451
  if username:
1435
1452
  try:
1436
- if self.auth_username_ci:
1437
- return self.session.scalars(
1438
- select(self.user_model).where(
1439
- func.lower(self.user_model.username) == func.lower(username)
1440
- )
1441
- ).one_or_none()
1442
1453
  return self.session.scalars(
1443
1454
  select(self.user_model).where(
1444
1455
  func.lower(self.user_model.username) == func.lower(username)
@@ -1446,13 +1457,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1446
1457
  ).one_or_none()
1447
1458
  except MultipleResultsFound:
1448
1459
  log.error("Multiple results found for user %s", username)
1449
- return None
1450
1460
  elif email:
1451
1461
  try:
1452
1462
  return self.session.scalars(select(self.user_model).filter_by(email=email)).one_or_none()
1453
1463
  except MultipleResultsFound:
1454
1464
  log.error("Multiple results found for user with email %s", email)
1455
- return None
1465
+ return None
1456
1466
 
1457
1467
  def update_user(self, user: User) -> bool:
1458
1468
  try:
@@ -1465,7 +1475,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1465
1475
  return False
1466
1476
  return True
1467
1477
 
1468
- def del_register_user(self, register_user):
1478
+ def del_register_user(self, register_user) -> bool:
1469
1479
  """
1470
1480
  Delete registration object from database.
1471
1481
 
@@ -1480,10 +1490,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1480
1490
  self.session.rollback()
1481
1491
  return False
1482
1492
 
1483
- def get_all_users(self):
1493
+ def get_all_users(self) -> list[User]:
1484
1494
  return self.session.scalars(select(self.user_model)).all()
1485
1495
 
1486
- def update_user_auth_stat(self, user, success=True):
1496
+ def update_user_auth_stat(self, user, success=True) -> None:
1487
1497
  """
1488
1498
  Update user authentication stats.
1489
1499
 
@@ -1523,7 +1533,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1523
1533
  """
1524
1534
  return self.session.scalars(select(self.action_model).filter_by(name=name)).one_or_none()
1525
1535
 
1526
- def create_action(self, name):
1536
+ def create_action(self, name) -> Action:
1527
1537
  """
1528
1538
  Add an action to the backend, model action.
1529
1539
 
@@ -1582,7 +1592,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1582
1592
  """
1583
1593
  return self.session.scalars(select(self.resource_model).filter_by(name=name)).one_or_none()
1584
1594
 
1585
- def create_resource(self, name) -> Resource | None:
1595
+ def create_resource(self, name) -> Resource:
1586
1596
  """
1587
1597
  Create a resource with the given name.
1588
1598
 
@@ -1599,6 +1609,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1599
1609
  except Exception as e:
1600
1610
  log.error(const.LOGMSG_ERR_SEC_ADD_VIEWMENU, e)
1601
1611
  self.session.rollback()
1612
+ raise FabException(const.LOGMSG_ERR_SEC_ADD_VIEWMENU) from e
1602
1613
  return resource
1603
1614
 
1604
1615
  """
@@ -1737,7 +1748,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1737
1748
  self.session.rollback()
1738
1749
 
1739
1750
  @staticmethod
1740
- def get_user_roles(user=None):
1751
+ def get_user_roles(user=None) -> list[Role]:
1741
1752
  """
1742
1753
  Get all the roles associated with the user.
1743
1754
 
@@ -1754,7 +1765,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1754
1765
  --------------------
1755
1766
  """
1756
1767
 
1757
- 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:
1758
1769
  """
1759
1770
  Authenticate user with LDAP.
1760
1771
 
@@ -1947,11 +1958,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1947
1958
  user = self.find_user(username=username)
1948
1959
  if user is None:
1949
1960
  user = self.find_user(email=username)
1950
- if user is None:
1961
+ if user is None or user.password is None:
1951
1962
  return False
1952
1963
  return check_password_hash(user.password, password)
1953
1964
 
1954
- 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:
1955
1966
  """
1956
1967
  Authenticate user, auth db style.
1957
1968
 
@@ -1975,6 +1986,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1975
1986
  )
1976
1987
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
1977
1988
  return None
1989
+ if user.password is None:
1990
+ return None
1978
1991
  if check_password_hash(user.password, password):
1979
1992
  if rotate_session_id:
1980
1993
  self._rotate_session_id()
@@ -1984,7 +1997,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1984
1997
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
1985
1998
  return None
1986
1999
 
1987
- def set_oauth_session(self, provider, oauth_response):
2000
+ def set_oauth_session(self, provider, oauth_response) -> None:
1988
2001
  """Set the current session with OAuth user secrets."""
1989
2002
  # Get this provider key names for token_key and token_secret
1990
2003
  token_key = self.get_oauth_token_key_name(provider)
@@ -2018,7 +2031,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2018
2031
  if _provider["name"] == provider:
2019
2032
  return _provider.get("token_secret", "oauth_token_secret")
2020
2033
 
2021
- def auth_user_oauth(self, userinfo, rotate_session_id=True):
2034
+ def auth_user_oauth(self, userinfo, rotate_session_id=True) -> User | None:
2022
2035
  """
2023
2036
  Authenticate user with OAuth.
2024
2037
 
@@ -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