apache-airflow-providers-fab 1.5.3rc1__py3-none-any.whl → 2.0.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 (105) hide show
  1. airflow/providers/fab/LICENSE +0 -52
  2. airflow/providers/fab/__init__.py +3 -3
  3. airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +4 -5
  4. airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +5 -5
  5. airflow/providers/fab/auth_manager/api/auth/backend/session.py +2 -2
  6. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +15 -15
  7. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +13 -14
  8. airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
  9. airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
  10. airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
  11. airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
  12. airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +153 -0
  13. airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
  14. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
  15. airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
  16. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
  17. airflow/providers/fab/auth_manager/cli_commands/db_command.py +1 -3
  18. airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
  19. airflow/providers/fab/auth_manager/cli_commands/utils.py +12 -11
  20. airflow/providers/fab/auth_manager/fab_auth_manager.py +238 -126
  21. airflow/providers/fab/auth_manager/models/__init__.py +1 -1
  22. airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
  23. airflow/providers/fab/auth_manager/models/db.py +22 -5
  24. airflow/providers/fab/auth_manager/openapi/v1.yaml +9 -0
  25. airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
  26. airflow/providers/fab/auth_manager/security_manager/override.py +186 -655
  27. airflow/providers/fab/auth_manager/views/permissions.py +1 -1
  28. airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
  29. airflow/providers/fab/auth_manager/views/user.py +1 -1
  30. airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
  31. airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
  32. airflow/providers/fab/get_provider_info.py +29 -34
  33. airflow/providers/fab/www/airflow_flask_app.py +31 -0
  34. airflow/providers/fab/www/api_connexion/__init__.py +17 -0
  35. airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
  36. airflow/providers/fab/www/api_connexion/parameters.py +131 -0
  37. airflow/providers/fab/www/api_connexion/security.py +84 -0
  38. airflow/providers/fab/www/api_connexion/types.py +30 -0
  39. airflow/providers/fab/www/app.py +120 -0
  40. airflow/providers/fab/www/auth.py +350 -0
  41. airflow/providers/fab/www/constants.py +28 -0
  42. airflow/providers/fab/www/extensions/__init__.py +16 -0
  43. airflow/providers/fab/www/extensions/init_appbuilder.py +606 -0
  44. airflow/providers/fab/www/extensions/init_jinja_globals.py +82 -0
  45. airflow/providers/fab/www/extensions/init_manifest_files.py +61 -0
  46. airflow/providers/fab/www/extensions/init_security.py +61 -0
  47. airflow/providers/fab/www/extensions/init_session.py +64 -0
  48. airflow/providers/fab/www/extensions/init_views.py +177 -0
  49. airflow/providers/fab/www/package-lock.json +8939 -0
  50. airflow/providers/fab/www/package.json +77 -0
  51. airflow/providers/fab/www/security/__init__.py +17 -0
  52. airflow/providers/fab/www/security/permissions.py +126 -0
  53. airflow/providers/fab/www/security_appless.py +44 -0
  54. airflow/providers/fab/www/security_manager.py +122 -0
  55. airflow/providers/fab/www/session.py +41 -0
  56. airflow/providers/fab/www/static/css/bootstrap-theme.css +6215 -0
  57. airflow/providers/fab/www/static/css/flash.css +57 -0
  58. airflow/providers/fab/www/static/css/loading-dots.css +60 -0
  59. airflow/providers/fab/www/static/css/main.css +676 -0
  60. airflow/providers/fab/www/static/css/material-icons.css +84 -0
  61. airflow/providers/fab/www/static/dist/48f0ea180c40270a5b05.png +1 -0
  62. airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -0
  63. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
  64. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
  65. airflow/providers/fab/www/static/dist/f7490d556a6c42e49ba4.png +1 -0
  66. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
  67. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
  68. airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
  69. airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
  70. airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
  71. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
  72. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
  73. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.css +18 -0
  74. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
  75. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
  76. airflow/providers/fab/www/static/dist/manifest.json +20 -0
  77. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
  78. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
  79. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js +2 -0
  80. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
  81. airflow/providers/fab/www/static/dist/oss-licenses.json +20 -0
  82. airflow/providers/fab/www/static/js/datetime_utils.js +134 -0
  83. airflow/providers/fab/www/static/js/main.js +324 -0
  84. airflow/providers/fab/www/static/sort_asc.png +0 -0
  85. airflow/providers/fab/www/static/sort_both.png +0 -0
  86. airflow/providers/fab/www/static/sort_desc.png +0 -0
  87. airflow/providers/fab/www/templates/airflow/_messages.html +30 -0
  88. airflow/providers/fab/www/templates/airflow/error.html +35 -0
  89. airflow/providers/fab/www/templates/airflow/main.html +78 -0
  90. airflow/providers/fab/www/templates/airflow/traceback.html +53 -0
  91. airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
  92. airflow/providers/fab/www/templates/appbuilder/index.html +20 -0
  93. airflow/providers/fab/www/templates/appbuilder/navbar.html +60 -0
  94. airflow/providers/fab/www/templates/appbuilder/navbar_menu.html +60 -0
  95. airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
  96. airflow/providers/fab/www/utils.py +288 -0
  97. airflow/providers/fab/www/views.py +129 -0
  98. airflow/providers/fab/www/webpack.config.js +213 -0
  99. {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/METADATA +30 -38
  100. apache_airflow_providers_fab-2.0.0.dist-info/RECORD +125 -0
  101. {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/WHEEL +1 -1
  102. airflow/providers/fab/auth_manager/decorators/auth.py +0 -126
  103. apache_airflow_providers_fab-1.5.3rc1.dist-info/RECORD +0 -51
  104. /airflow/providers/fab/{auth_manager/decorators → www}/__init__.py +0 -0
  105. {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -21,16 +21,11 @@ import copy
21
21
  import datetime
22
22
  import itertools
23
23
  import logging
24
- import os
25
- import random
26
24
  import uuid
27
- import warnings
28
- from typing import TYPE_CHECKING, Any, Callable, Collection, Container, Iterable, Mapping, Sequence
25
+ from collections.abc import Collection, Iterable, Mapping
26
+ from typing import TYPE_CHECKING, Any
29
27
 
30
28
  import jwt
31
- import packaging.version
32
- import re2
33
- from deprecated import deprecated
34
29
  from flask import flash, g, has_request_context, session
35
30
  from flask_appbuilder import const
36
31
  from flask_appbuilder.const import (
@@ -59,25 +54,21 @@ from flask_appbuilder.security.views import (
59
54
  AuthOAuthView,
60
55
  AuthOIDView,
61
56
  AuthRemoteUserView,
62
- AuthView,
63
57
  RegisterUserModelView,
64
58
  )
65
- from flask_appbuilder.views import expose
66
59
  from flask_babel import lazy_gettext
67
- from flask_jwt_extended import JWTManager, current_user as current_user_jwt
60
+ from flask_jwt_extended import JWTManager
68
61
  from flask_login import LoginManager
69
62
  from itsdangerous import want_bytes
70
63
  from markupsafe import Markup
71
- from sqlalchemy import and_, func, inspect, literal, or_, select
64
+ from sqlalchemy import func, inspect, or_, select
72
65
  from sqlalchemy.exc import MultipleResultsFound
73
- from sqlalchemy.orm import Session, joinedload
66
+ from sqlalchemy.orm import joinedload
74
67
  from werkzeug.security import check_password_hash, generate_password_hash
75
68
 
76
- from airflow import __version__ as airflow_version
77
- from airflow.auth.managers.utils.fab import get_method_from_fab_action_map
78
69
  from airflow.configuration import conf
79
- from airflow.exceptions import AirflowException, AirflowProviderDeprecationWarning, RemovedInAirflow3Warning
80
- from airflow.models import DagBag, DagModel
70
+ from airflow.exceptions import AirflowException
71
+ from airflow.models import DagBag
81
72
  from airflow.providers.fab.auth_manager.models import (
82
73
  Action,
83
74
  Permission,
@@ -85,7 +76,6 @@ from airflow.providers.fab.auth_manager.models import (
85
76
  Resource,
86
77
  Role,
87
78
  User,
88
- assoc_permission_role,
89
79
  )
90
80
  from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser
91
81
  from airflow.providers.fab.auth_manager.security_manager.constants import EXISTING_ROLES
@@ -108,17 +98,24 @@ from airflow.providers.fab.auth_manager.views.user_edit import (
108
98
  CustomUserInfoEditView,
109
99
  )
110
100
  from airflow.providers.fab.auth_manager.views.user_stats import CustomUserStatsChartView
111
- from airflow.security import permissions
112
- from airflow.utils.session import NEW_SESSION, provide_session
113
- from airflow.www.extensions.init_auth_manager import get_auth_manager
114
- from airflow.www.security_manager import AirflowSecurityManagerV2
115
- from airflow.www.session import AirflowDatabaseSessionInterface
101
+ from airflow.providers.fab.www.security import permissions
102
+ from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
103
+ from airflow.providers.fab.www.session import (
104
+ AirflowDatabaseSessionInterface,
105
+ AirflowDatabaseSessionInterface as FabAirflowDatabaseSessionInterface,
106
+ )
107
+ from airflow.security.permissions import RESOURCE_BACKFILL
116
108
 
117
109
  if TYPE_CHECKING:
118
- from airflow.auth.managers.base_auth_manager import ResourceMethod
119
- from airflow.security.permissions import RESOURCE_ASSET
110
+ from airflow.providers.fab.www.security.permissions import (
111
+ RESOURCE_ASSET,
112
+ RESOURCE_ASSET_ALIAS,
113
+ )
120
114
  else:
121
- from airflow.providers.common.compat.security.permissions import RESOURCE_ASSET
115
+ from airflow.providers.common.compat.security.permissions import (
116
+ RESOURCE_ASSET,
117
+ RESOURCE_ASSET_ALIAS,
118
+ )
122
119
 
123
120
  log = logging.getLogger(__name__)
124
121
 
@@ -131,29 +128,6 @@ log = logging.getLogger(__name__)
131
128
  MAX_NUM_DATABASE_USER_SESSIONS = 50000
132
129
 
133
130
 
134
- # The following logic patches the logout method within AuthView, so it supports POST method
135
- # to make CSRF protection effective. It is backward-compatible with Airflow versions <= 2.9.2 as it still
136
- # allows utilizing the GET method for them.
137
- # You could remove the patch and configure it when it is supported
138
- # natively by Flask-AppBuilder (https://github.com/dpgaspar/Flask-AppBuilder/issues/2248)
139
- if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
140
- "2.10.0"
141
- ):
142
- _methods = ["GET", "POST"]
143
- else:
144
- _methods = ["POST"]
145
-
146
-
147
- class _ModifiedAuthView(AuthView):
148
- @expose("/logout/", methods=_methods)
149
- def logout(self):
150
- return super().logout()
151
-
152
-
153
- for auth_view in [AuthDBView, AuthLDAPView, AuthOAuthView, AuthOIDView, AuthRemoteUserView]:
154
- auth_view.__bases__ = (_ModifiedAuthView,)
155
-
156
-
157
131
  class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
158
132
  """
159
133
  This security manager overrides the default AirflowSecurityManager security manager.
@@ -214,8 +188,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
214
188
 
215
189
  jwt_manager = None
216
190
  """ Flask-JWT-Extended """
217
- oid = None
218
- """ Flask-OpenID OpenID """
219
191
  oauth = None
220
192
  oauth_remotes: dict[str, Any]
221
193
  """ Initialized (remote_app) providers dict {'provider_name', OBJ } """
@@ -238,11 +210,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
238
210
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_DEPENDENCIES),
239
211
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_CODE),
240
212
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN),
213
+ (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_VERSION),
214
+ (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
241
215
  (permissions.ACTION_CAN_READ, RESOURCE_ASSET),
216
+ (permissions.ACTION_CAN_READ, RESOURCE_ASSET_ALIAS),
217
+ (permissions.ACTION_CAN_READ, RESOURCE_BACKFILL),
242
218
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
243
219
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_POOL),
244
220
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR),
245
- (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
246
221
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_JOB),
247
222
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_MY_PASSWORD),
248
223
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_MY_PASSWORD),
@@ -306,8 +281,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
306
281
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
307
282
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
308
283
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
309
- (permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
310
284
  (permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
285
+ (permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
286
+ (permissions.ACTION_CAN_CREATE, RESOURCE_BACKFILL),
287
+ (permissions.ACTION_CAN_EDIT, RESOURCE_BACKFILL),
288
+ (permissions.ACTION_CAN_DELETE, RESOURCE_BACKFILL),
311
289
  ]
312
290
  # [END security_op_perms]
313
291
 
@@ -554,7 +532,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
554
532
  return self.update_user(user)
555
533
 
556
534
  def reset_user_sessions(self, user: User) -> None:
557
- if isinstance(self.appbuilder.get_app.session_interface, AirflowDatabaseSessionInterface):
535
+ if isinstance(
536
+ self.appbuilder.get_app.session_interface, AirflowDatabaseSessionInterface
537
+ ) or isinstance(
538
+ self.appbuilder.get_app.session_interface,
539
+ FabAirflowDatabaseSessionInterface,
540
+ ):
558
541
  interface = self.appbuilder.get_app.session_interface
559
542
  session = interface.db.session
560
543
  user_session_model = interface.sql_session_model
@@ -726,34 +709,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
726
709
  """The JMESPATH role to use for user registration."""
727
710
  return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
728
711
 
729
- @property
730
- def auth_remote_user_env_var(self) -> str:
731
- return self.appbuilder.get_app.config["AUTH_REMOTE_USER_ENV_VAR"]
732
-
733
- @property
734
- def api_login_allow_multiple_providers(self):
735
- return self.appbuilder.get_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
736
-
737
712
  @property
738
713
  def auth_username_ci(self):
739
714
  """Get the auth username for CI."""
740
715
  return self.appbuilder.get_app.config.get("AUTH_USERNAME_CI", True)
741
716
 
742
- @property
743
- def auth_ldap_bind_first(self):
744
- """LDAP bind first."""
745
- return self.appbuilder.get_app.config["AUTH_LDAP_BIND_FIRST"]
746
-
747
- @property
748
- def openid_providers(self):
749
- """Openid providers."""
750
- return self.appbuilder.get_app.config["OPENID_PROVIDERS"]
751
-
752
- @property
753
- def auth_type_provider_name(self):
754
- provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
755
- return provider_to_auth_type.get(self.auth_type)
756
-
757
717
  @property
758
718
  def auth_user_registration(self):
759
719
  """Will user self registration be allowed."""
@@ -775,10 +735,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
775
735
  return self.appbuilder.get_app.config["AUTH_ROLE_ADMIN"]
776
736
 
777
737
  @property
778
- @deprecated(
779
- reason="The 'oauth_whitelists' property is deprecated. Please use 'oauth_allow_list' instead.",
780
- category=AirflowProviderDeprecationWarning,
781
- )
782
738
  def oauth_whitelists(self):
783
739
  return self.oauth_allow_list
784
740
 
@@ -791,43 +747,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
791
747
  """Get the builtin roles."""
792
748
  return self._builtin_roles
793
749
 
794
- def create_admin_standalone(self) -> tuple[str | None, str | None]:
795
- """Create an Admin user with a random password so that users can access airflow."""
796
- from airflow.configuration import AIRFLOW_HOME, make_group_other_inaccessible
797
-
798
- user_name = "admin"
799
-
800
- # We want a streamlined first-run experience, but we do not want to
801
- # use a preset password as people will inevitably run this on a public
802
- # server. Thus, we make a random password and store it in AIRFLOW_HOME,
803
- # with the reasoning that if you can read that directory, you can see
804
- # the database credentials anyway.
805
- password_path = os.path.join(AIRFLOW_HOME, "standalone_admin_password.txt")
806
-
807
- user_exists = self.find_user(user_name) is not None
808
- we_know_password = os.path.isfile(password_path)
809
-
810
- # If the user does not exist, make a random password and make it
811
- if not user_exists:
812
- print(f"FlaskAppBuilder Authentication Manager: Creating {user_name} user")
813
- if (role := self.find_role("Admin")) is None:
814
- raise AirflowException("Unable to find role 'Admin'")
815
- # password does not contain visually similar characters: ijlIJL1oO0
816
- password = "".join(random.choices("abcdefghkmnpqrstuvwxyzABCDEFGHKMNPQRSTUVWXYZ23456789", k=16))
817
- with open(password_path, "w") as file:
818
- file.write(password)
819
- make_group_other_inaccessible(password_path)
820
- self.add_user(user_name, "Admin", "User", "admin@example.com", role, password)
821
- print(f"FlaskAppBuilder Authentication Manager: Created {user_name} user")
822
- # If the user does exist, and we know its password, read the password
823
- elif user_exists and we_know_password:
824
- with open(password_path) as file:
825
- password = file.read().strip()
826
- # Otherwise we don't know the password
827
- else:
828
- password = None
829
- return user_name, password
830
-
831
750
  def _init_config(self):
832
751
  """
833
752
  Initialize config.
@@ -904,15 +823,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
904
823
  :meta private:
905
824
  """
906
825
  app = self.appbuilder.get_app
907
- if self.auth_type == AUTH_OID:
908
- from flask_openid import OpenID
909
-
910
- log.warning(
911
- "AUTH_OID is deprecated and will be removed in version 5. "
912
- "Migrate to other authentication methods."
913
- )
914
- self.oid = OpenID(app)
915
-
916
826
  if self.auth_type == AUTH_OAUTH:
917
827
  from authlib.integrations.flask_client import OAuth
918
828
 
@@ -985,91 +895,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
985
895
  log.exception(const.LOGMSG_ERR_SEC_CREATE_DB)
986
896
  exit(1)
987
897
 
988
- def get_readable_dags(self, user) -> Iterable[DagModel]:
989
- """Get the DAGs readable by authenticated user."""
990
- warnings.warn(
991
- "`get_readable_dags` has been deprecated. Please use `get_auth_manager().get_permitted_dag_ids` "
992
- "instead.",
993
- RemovedInAirflow3Warning,
994
- stacklevel=2,
995
- )
996
- with warnings.catch_warnings():
997
- warnings.simplefilter("ignore", RemovedInAirflow3Warning)
998
- return self.get_accessible_dags([permissions.ACTION_CAN_READ], user)
999
-
1000
- def get_editable_dags(self, user) -> Iterable[DagModel]:
1001
- """Get the DAGs editable by authenticated user."""
1002
- warnings.warn(
1003
- "`get_editable_dags` has been deprecated. Please use `get_auth_manager().get_permitted_dag_ids` "
1004
- "instead.",
1005
- RemovedInAirflow3Warning,
1006
- stacklevel=2,
1007
- )
1008
- with warnings.catch_warnings():
1009
- warnings.simplefilter("ignore", RemovedInAirflow3Warning)
1010
- return self.get_accessible_dags([permissions.ACTION_CAN_EDIT], user)
1011
-
1012
- @provide_session
1013
- def get_accessible_dags(
1014
- self,
1015
- user_actions: Container[str] | None,
1016
- user,
1017
- session: Session = NEW_SESSION,
1018
- ) -> Iterable[DagModel]:
1019
- warnings.warn(
1020
- "`get_accessible_dags` has been deprecated. Please use "
1021
- "`get_auth_manager().get_permitted_dag_ids` instead.",
1022
- RemovedInAirflow3Warning,
1023
- stacklevel=3,
1024
- )
1025
-
1026
- dag_ids = self.get_accessible_dag_ids(user, user_actions, session)
1027
- return session.scalars(select(DagModel).where(DagModel.dag_id.in_(dag_ids)))
1028
-
1029
- @provide_session
1030
- def get_accessible_dag_ids(
1031
- self,
1032
- user,
1033
- user_actions: Container[str] | None = None,
1034
- session: Session = NEW_SESSION,
1035
- ) -> set[str]:
1036
- warnings.warn(
1037
- "`get_accessible_dag_ids` has been deprecated. Please use "
1038
- "`get_auth_manager().get_permitted_dag_ids` instead.",
1039
- RemovedInAirflow3Warning,
1040
- stacklevel=3,
1041
- )
1042
- if not user_actions:
1043
- user_actions = [permissions.ACTION_CAN_EDIT, permissions.ACTION_CAN_READ]
1044
- method_from_fab_action_map = get_method_from_fab_action_map()
1045
- user_methods: Container[ResourceMethod] = [
1046
- method_from_fab_action_map[action]
1047
- for action in method_from_fab_action_map
1048
- if action in user_actions
1049
- ]
1050
- return get_auth_manager().get_permitted_dag_ids(user=user, methods=user_methods, session=session)
1051
-
1052
- @staticmethod
1053
- def get_readable_dag_ids(user=None) -> set[str]:
1054
- """Get the DAG IDs readable by authenticated user."""
1055
- return get_auth_manager().get_permitted_dag_ids(methods=["GET"], user=user)
1056
-
1057
- @staticmethod
1058
- def get_editable_dag_ids(user=None) -> set[str]:
1059
- """Get the DAG IDs editable by authenticated user."""
1060
- return get_auth_manager().get_permitted_dag_ids(methods=["PUT"], user=user)
1061
-
1062
- def can_access_some_dags(self, action: str, dag_id: str | None = None) -> bool:
1063
- """Check if user has read or write access to some dags."""
1064
- if dag_id and dag_id != "~":
1065
- root_dag_id = self._get_root_dag_id(dag_id)
1066
- return self.has_access(action, self._resource_name(root_dag_id, permissions.RESOURCE_DAG))
1067
-
1068
- user = g.user
1069
- if action == permissions.ACTION_CAN_READ:
1070
- return any(self.get_readable_dag_ids(user))
1071
- return any(self.get_editable_dag_ids(user))
1072
-
1073
898
  def get_all_permissions(self) -> set[tuple[str, str]]:
1074
899
  """Return all permissions as a set of tuples with the action and resource names."""
1075
900
  return set(
@@ -1096,8 +921,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1096
921
  dags = dagbag.dags.values()
1097
922
 
1098
923
  for dag in dags:
1099
- # TODO: Remove this when the minimum version of Airflow is bumped to 3.0
1100
- root_dag_id = (getattr(dag, "parent_dag", None) or dag).dag_id
924
+ root_dag_id = dag.dag_id
1101
925
  for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
1102
926
  dag_resource_name = self._resource_name(root_dag_id, resource_name)
1103
927
  for action_name in resource_values["actions"]:
@@ -1107,23 +931,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1107
931
  if dag.access_control is not None:
1108
932
  self.sync_perm_for_dag(root_dag_id, dag.access_control)
1109
933
 
1110
- def prefixed_dag_id(self, dag_id: str) -> str:
1111
- """Return the permission name for a DAG id."""
1112
- warnings.warn(
1113
- "`prefixed_dag_id` has been deprecated. "
1114
- "Please use `airflow.security.permissions.resource_name` instead.",
1115
- RemovedInAirflow3Warning,
1116
- stacklevel=2,
1117
- )
1118
- root_dag_id = self._get_root_dag_id(dag_id)
1119
- return self._resource_name(root_dag_id, permissions.RESOURCE_DAG)
1120
-
1121
- def is_dag_resource(self, resource_name: str) -> bool:
1122
- """Determine if a resource belongs to a DAG or all DAGs."""
1123
- if resource_name == permissions.RESOURCE_DAG:
1124
- return True
1125
- return resource_name.startswith(permissions.RESOURCE_DAG_PREFIX)
1126
-
1127
934
  def sync_perm_for_dag(
1128
935
  self,
1129
936
  dag_id: str,
@@ -1183,7 +990,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1183
990
  def _get_or_create_dag_permission(action_name: str, dag_resource_name: str) -> Permission | None:
1184
991
  perm = self.get_permission(action_name, dag_resource_name)
1185
992
  if not perm:
1186
- self.log.info("Creating new action '%s' on resource '%s'", action_name, dag_resource_name)
993
+ self.log.info(
994
+ "Creating new action '%s' on resource '%s'",
995
+ action_name,
996
+ dag_resource_name,
997
+ )
1187
998
  perm = self.create_permission(action_name, dag_resource_name)
1188
999
  return perm
1189
1000
 
@@ -1268,6 +1079,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1268
1079
  action = self.create_permission(action_name, resource_name)
1269
1080
  if self.auth_role_admin not in self.builtin_roles:
1270
1081
  admin_role = self.find_role(self.auth_role_admin)
1082
+ if not admin_role:
1083
+ admin_role = self.add_role(self.auth_role_admin)
1271
1084
  self.add_permission_to_role(admin_role, action)
1272
1085
  else:
1273
1086
  # Permissions on this view exist but....
@@ -1310,31 +1123,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1310
1123
  role_admin = self.find_role(self.auth_role_admin)
1311
1124
  self.add_permission_to_role(role_admin, perm)
1312
1125
 
1313
- def security_cleanup(self, baseviews, menus):
1314
- """
1315
- Cleanup all unused permissions from the database.
1316
-
1317
- :param baseviews: A list of BaseViews class
1318
- :param menus: Menu class
1319
- """
1320
- resources = self.get_all_resources()
1321
- roles = self.get_all_roles()
1322
- for resource in resources:
1323
- found = False
1324
- for baseview in baseviews:
1325
- if resource.name == baseview.class_permission_name:
1326
- found = True
1327
- break
1328
- if menus.find(resource.name):
1329
- found = True
1330
- if not found:
1331
- permissions = self.get_resource_permissions(resource)
1332
- for permission in permissions:
1333
- for role in roles:
1334
- self.remove_permission_from_role(role, permission)
1335
- self.delete_permission(permission.action.name, resource.name)
1336
- self.delete_resource(resource.name)
1337
-
1338
1126
  def sync_roles(self) -> None:
1339
1127
  """
1340
1128
  Initialize default and custom roles with related permissions.
@@ -1414,57 +1202,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1414
1202
  if deleted_count:
1415
1203
  self.log.info("Deleted %s faulty permissions", deleted_count)
1416
1204
 
1417
- def permission_exists_in_one_or_more_roles(
1418
- self, resource_name: str, action_name: str, role_ids: list[int]
1419
- ) -> bool:
1420
- """
1421
- Efficiently check if a certain permission exists on a list of role ids; used by `has_access`.
1422
-
1423
- :param resource_name: The view's name to check if exists on one of the roles
1424
- :param action_name: The permission name to check if exists
1425
- :param role_ids: a list of Role ids
1426
- :return: Boolean
1427
- """
1428
- q = (
1429
- self.appbuilder.get_session.query(self.permission_model)
1430
- .join(
1431
- assoc_permission_role,
1432
- and_(self.permission_model.id == assoc_permission_role.c.permission_view_id),
1433
- )
1434
- .join(self.role_model)
1435
- .join(self.action_model)
1436
- .join(self.resource_model)
1437
- .filter(
1438
- self.resource_model.name == resource_name,
1439
- self.action_model.name == action_name,
1440
- self.role_model.id.in_(role_ids),
1441
- )
1442
- .exists()
1443
- )
1444
- # Special case for MSSQL/Oracle (works on PG and MySQL > 8)
1445
- # Note: We need to keep MSSQL compatibility as long as this provider package
1446
- # might still be updated by Airflow prior 2.9.0 users with MSSQL
1447
- if self.appbuilder.get_session.bind.dialect.name in ("mssql", "oracle"):
1448
- return self.appbuilder.get_session.query(literal(True)).filter(q).scalar()
1449
- return self.appbuilder.get_session.query(q).scalar()
1450
-
1451
1205
  def perms_include_action(self, perms, action_name):
1452
1206
  return any(perm.action and perm.action.name == action_name for perm in perms)
1453
1207
 
1454
- def init_role(self, role_name, perms) -> None:
1455
- """
1456
- Initialize the role with actions and related resources.
1457
-
1458
- :param role_name:
1459
- :param perms:
1460
- """
1461
- warnings.warn(
1462
- "`init_role` has been deprecated. Please use `bulk_sync_roles` instead.",
1463
- RemovedInAirflow3Warning,
1464
- stacklevel=2,
1465
- )
1466
- self.bulk_sync_roles([{"role": role_name, "perms": perms}])
1467
-
1468
1208
  def bulk_sync_roles(self, roles: Iterable[dict[str, Any]]) -> None:
1469
1209
  """Sync the provided roles and permissions."""
1470
1210
  existing_roles = self._get_all_roles_with_permissions()
@@ -1483,15 +1223,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1483
1223
  if perm not in role.permissions:
1484
1224
  self.add_permission_to_role(role, perm)
1485
1225
 
1486
- def sync_resource_permissions(self, perms: Iterable[tuple[str, str]] | None = None) -> None:
1487
- """Populate resource-based permissions."""
1488
- if not perms:
1489
- return
1490
-
1491
- for action_name, resource_name in perms:
1492
- self.create_resource(resource_name)
1493
- self.create_permission(action_name, resource_name)
1494
-
1495
1226
  """
1496
1227
  -----------
1497
1228
  Role entity
@@ -1575,7 +1306,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1575
1306
  if fab_role:
1576
1307
  _roles.add(fab_role)
1577
1308
  else:
1578
- log.warning("Can't find role specified in AUTH_ROLES_MAPPING: %s", fab_role_name)
1309
+ log.warning(
1310
+ "Can't find role specified in AUTH_ROLES_MAPPING: %s",
1311
+ fab_role_name,
1312
+ )
1579
1313
  return _roles
1580
1314
 
1581
1315
  def get_public_role(self):
@@ -1683,13 +1417,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1683
1417
  log.error("Multiple results found for user with email %s", email)
1684
1418
  return None
1685
1419
 
1686
- def find_register_user(self, registration_hash):
1687
- return self.get_session.scalar(
1688
- select(self.registeruser_mode)
1689
- .where(self.registeruser_model.registration_hash == registration_hash)
1690
- .limit(1)
1691
- )
1692
-
1693
1420
  def update_user(self, user: User) -> bool:
1694
1421
  try:
1695
1422
  self.get_session.merge(user)
@@ -1839,38 +1566,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1839
1566
  self.get_session.rollback()
1840
1567
  return resource
1841
1568
 
1842
- def get_all_resources(self) -> list[Resource]:
1843
- """Get all existing resource records."""
1844
- return self.get_session.query(self.resource_model).all()
1845
-
1846
- def delete_resource(self, name: str) -> bool:
1847
- """
1848
- Delete a Resource from the backend.
1849
-
1850
- :param name:
1851
- name of the resource
1852
- """
1853
- resource = self.get_resource(name)
1854
- if not resource:
1855
- log.warning(const.LOGMSG_WAR_SEC_DEL_VIEWMENU, name)
1856
- return False
1857
- try:
1858
- perms = (
1859
- self.get_session.query(self.permission_model)
1860
- .filter(self.permission_model.resource == resource)
1861
- .all()
1862
- )
1863
- if perms:
1864
- log.warning(const.LOGMSG_WAR_SEC_DEL_VIEWMENU_PVM, resource, perms)
1865
- return False
1866
- self.get_session.delete(resource)
1867
- self.get_session.commit()
1868
- return True
1869
- except Exception as e:
1870
- log.error(const.LOGMSG_ERR_SEC_DEL_PERMISSION, e)
1871
- self.get_session.rollback()
1872
- return False
1873
-
1874
1569
  """
1875
1570
  ---------------
1876
1571
  Permission entity
@@ -2000,13 +1695,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2000
1695
  log.error(const.LOGMSG_ERR_SEC_DEL_PERMROLE, e)
2001
1696
  self.get_session.rollback()
2002
1697
 
2003
- def get_oid_identity_url(self, provider_name: str) -> str | None:
2004
- """Return the OIDC identity provider URL."""
2005
- for provider in self.openid_providers:
2006
- if provider.get("name") == provider_name:
2007
- return provider.get("url")
2008
- return None
2009
-
2010
1698
  @staticmethod
2011
1699
  def get_user_roles(user=None):
2012
1700
  """
@@ -2209,6 +1897,20 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2209
1897
  log.error(e)
2210
1898
  return None
2211
1899
 
1900
+ def check_password(self, username, password) -> bool:
1901
+ """
1902
+ Check if the password is correct for the username.
1903
+
1904
+ :param username: the username
1905
+ :param password: the password
1906
+ """
1907
+ user = self.find_user(username=username)
1908
+ if user is None:
1909
+ user = self.find_user(email=username)
1910
+ if user is None:
1911
+ return False
1912
+ return check_password_hash(user.password, password)
1913
+
2212
1914
  def auth_user_db(self, username, password):
2213
1915
  """
2214
1916
  Authenticate user, auth db style.
@@ -2240,31 +1942,99 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2240
1942
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
2241
1943
  return None
2242
1944
 
2243
- def oauth_user_info_getter(
2244
- self,
2245
- func: Callable[[AirflowSecurityManagerV2, str, dict[str, Any] | None], dict[str, Any]],
2246
- ):
1945
+ def set_oauth_session(self, provider, oauth_response):
1946
+ """Set the current session with OAuth user secrets."""
1947
+ # Get this provider key names for token_key and token_secret
1948
+ token_key = self.get_oauth_token_key_name(provider)
1949
+ token_secret = self.get_oauth_token_secret_name(provider)
1950
+ # Save users token on encrypted session cookie
1951
+ session["oauth"] = (
1952
+ oauth_response[token_key],
1953
+ oauth_response.get(token_secret, ""),
1954
+ )
1955
+ session["oauth_provider"] = provider
1956
+
1957
+ def get_oauth_token_key_name(self, provider):
2247
1958
  """
2248
- Get OAuth user info for all the providers.
1959
+ Return the token_key name for the oauth provider.
2249
1960
 
2250
- Receives provider and response return a dict with the information returned from the provider.
2251
- The returned user info dict should have its keys with the same name as the User Model.
1961
+ If none is configured defaults to oauth_token
1962
+ this is configured using OAUTH_PROVIDERS and token_key key.
1963
+ """
1964
+ for _provider in self.oauth_providers:
1965
+ if _provider["name"] == provider:
1966
+ return _provider.get("token_key", "oauth_token")
1967
+
1968
+ def get_oauth_token_secret_name(self, provider):
1969
+ """
1970
+ Get the ``token_secret`` name for the oauth provider.
1971
+
1972
+ If none is configured, defaults to ``oauth_secret``. This is configured
1973
+ using ``OAUTH_PROVIDERS`` and ``token_secret``.
1974
+ """
1975
+ for _provider in self.oauth_providers:
1976
+ if _provider["name"] == provider:
1977
+ return _provider.get("token_secret", "oauth_token_secret")
2252
1978
 
2253
- Use it like this an example for GitHub ::
1979
+ def auth_user_oauth(self, userinfo):
1980
+ """
1981
+ Authenticate user with OAuth.
2254
1982
 
2255
- @appbuilder.sm.oauth_user_info_getter
2256
- def my_oauth_user_info(sm, provider, response=None):
2257
- if provider == "github":
2258
- me = sm.oauth_remotes[provider].get("user")
2259
- return {"username": me.data.get("login")}
2260
- return {}
1983
+ :userinfo: dict with user information
1984
+ (keys are the same as User model columns)
2261
1985
  """
1986
+ # extract the username from `userinfo`
1987
+ if "username" in userinfo:
1988
+ username = userinfo["username"]
1989
+ elif "email" in userinfo:
1990
+ username = userinfo["email"]
1991
+ else:
1992
+ log.error("OAUTH userinfo does not have username or email %s", userinfo)
1993
+ return None
2262
1994
 
2263
- def wraps(provider: str, response: dict[str, Any] | None = None) -> dict[str, Any]:
2264
- return func(self, provider, response)
1995
+ # If username is empty, go away
1996
+ if (username is None) or username == "":
1997
+ return None
2265
1998
 
2266
- self.oauth_user_info = wraps
2267
- return wraps
1999
+ # Search the DB for this user
2000
+ user = self.find_user(username=username)
2001
+
2002
+ # If user is not active, go away
2003
+ if user and (not user.is_active):
2004
+ return None
2005
+
2006
+ # If user is not registered, and not self-registration, go away
2007
+ if (not user) and (not self.auth_user_registration):
2008
+ return None
2009
+
2010
+ # Sync the user's roles
2011
+ if user and self.auth_roles_sync_at_login:
2012
+ user.roles = self._oauth_calculate_user_roles(userinfo)
2013
+ log.debug("Calculated new roles for user=%r as: %s", username, user.roles)
2014
+
2015
+ # If the user is new, register them
2016
+ if (not user) and self.auth_user_registration:
2017
+ user = self.add_user(
2018
+ username=username,
2019
+ first_name=userinfo.get("first_name", ""),
2020
+ last_name=userinfo.get("last_name", ""),
2021
+ email=userinfo.get("email", "") or f"{username}@email.notfound",
2022
+ role=self._oauth_calculate_user_roles(userinfo),
2023
+ )
2024
+ log.debug("New user registered: %s", user)
2025
+
2026
+ # If user registration failed, go away
2027
+ if not user:
2028
+ log.error("Error creating a new OAuth user %s", username)
2029
+ return None
2030
+
2031
+ # LOGIN SUCCESS (only if user is now registered)
2032
+ if user:
2033
+ self._rotate_session_id()
2034
+ self.update_user_auth_stat(user)
2035
+ return user
2036
+ else:
2037
+ return None
2268
2038
 
2269
2039
  def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str, Any]:
2270
2040
  """
@@ -2387,183 +2157,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2387
2157
  log.debug("Token Get: %s", token)
2388
2158
  return token
2389
2159
 
2390
- def check_authorization(
2391
- self,
2392
- perms: Sequence[tuple[str, str]] | None = None,
2393
- dag_id: str | None = None,
2394
- ) -> bool:
2395
- """Check the logged-in user has the specified permissions."""
2396
- if not perms:
2397
- return True
2398
-
2399
- for perm in perms:
2400
- if perm in (
2401
- (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
2402
- (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG),
2403
- (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_DAG),
2404
- ):
2405
- can_access_all_dags = self.has_access(*perm)
2406
- if not can_access_all_dags:
2407
- action = perm[0]
2408
- if not self.can_access_some_dags(action, dag_id):
2409
- return False
2410
- elif not self.has_access(*perm):
2411
- return False
2412
-
2413
- return True
2414
-
2415
- def set_oauth_session(self, provider, oauth_response):
2416
- """Set the current session with OAuth user secrets."""
2417
- # Get this provider key names for token_key and token_secret
2418
- token_key = self.get_oauth_token_key_name(provider)
2419
- token_secret = self.get_oauth_token_secret_name(provider)
2420
- # Save users token on encrypted session cookie
2421
- session["oauth"] = (
2422
- oauth_response[token_key],
2423
- oauth_response.get(token_secret, ""),
2424
- )
2425
- session["oauth_provider"] = provider
2426
-
2427
- def get_oauth_token_key_name(self, provider):
2428
- """
2429
- Return the token_key name for the oauth provider.
2430
-
2431
- If none is configured defaults to oauth_token
2432
- this is configured using OAUTH_PROVIDERS and token_key key.
2433
- """
2434
- for _provider in self.oauth_providers:
2435
- if _provider["name"] == provider:
2436
- return _provider.get("token_key", "oauth_token")
2437
-
2438
- def get_oauth_token_secret_name(self, provider):
2439
- """
2440
- Get the ``token_secret`` name for the oauth provider.
2441
-
2442
- If none is configured, defaults to ``oauth_secret``. This is configured
2443
- using ``OAUTH_PROVIDERS`` and ``token_secret``.
2444
- """
2445
- for _provider in self.oauth_providers:
2446
- if _provider["name"] == provider:
2447
- return _provider.get("token_secret", "oauth_token_secret")
2448
-
2449
- def auth_user_oauth(self, userinfo):
2450
- """
2451
- Authenticate user with OAuth.
2452
-
2453
- :userinfo: dict with user information
2454
- (keys are the same as User model columns)
2455
- """
2456
- # extract the username from `userinfo`
2457
- if "username" in userinfo:
2458
- username = userinfo["username"]
2459
- elif "email" in userinfo:
2460
- username = userinfo["email"]
2461
- else:
2462
- log.error("OAUTH userinfo does not have username or email %s", userinfo)
2463
- return None
2464
-
2465
- # If username is empty, go away
2466
- if (username is None) or username == "":
2467
- return None
2468
-
2469
- # Search the DB for this user
2470
- user = self.find_user(username=username)
2471
-
2472
- # If user is not active, go away
2473
- if user and (not user.is_active):
2474
- return None
2475
-
2476
- # If user is not registered, and not self-registration, go away
2477
- if (not user) and (not self.auth_user_registration):
2478
- return None
2479
-
2480
- # Sync the user's roles
2481
- if user and self.auth_roles_sync_at_login:
2482
- user.roles = self._oauth_calculate_user_roles(userinfo)
2483
- log.debug("Calculated new roles for user=%r as: %s", username, user.roles)
2484
-
2485
- # If the user is new, register them
2486
- if (not user) and self.auth_user_registration:
2487
- user = self.add_user(
2488
- username=username,
2489
- first_name=userinfo.get("first_name", ""),
2490
- last_name=userinfo.get("last_name", ""),
2491
- email=userinfo.get("email", "") or f"{username}@email.notfound",
2492
- role=self._oauth_calculate_user_roles(userinfo),
2493
- )
2494
- log.debug("New user registered: %s", user)
2495
-
2496
- # If user registration failed, go away
2497
- if not user:
2498
- log.error("Error creating a new OAuth user %s", username)
2499
- return None
2500
-
2501
- # LOGIN SUCCESS (only if user is now registered)
2502
- if user:
2503
- self._rotate_session_id()
2504
- self.update_user_auth_stat(user)
2505
- return user
2506
- else:
2507
- return None
2508
-
2509
- def auth_user_oid(self, email):
2510
- """
2511
- Openid user Authentication.
2512
-
2513
- :param email: user's email to authenticate
2514
- """
2515
- user = self.find_user(email=email)
2516
- if user is None or (not user.is_active):
2517
- log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, email)
2518
- return None
2519
- else:
2520
- self._rotate_session_id()
2521
- self.update_user_auth_stat(user)
2522
- return user
2523
-
2524
- def auth_user_remote_user(self, username):
2525
- """
2526
- REMOTE_USER user Authentication.
2527
-
2528
- :param username: user's username for remote auth
2529
- """
2530
- user = self.find_user(username=username)
2531
-
2532
- # User does not exist, create one if auto user registration.
2533
- if user is None and self.auth_user_registration:
2534
- user = self.add_user(
2535
- # All we have is REMOTE_USER, so we set
2536
- # the other fields to blank.
2537
- username=username,
2538
- first_name=username,
2539
- last_name="-",
2540
- email=username + "@email.notfound",
2541
- role=self.find_role(self.auth_user_registration_role),
2542
- )
2543
-
2544
- # If user does not exist on the DB and not auto user registration,
2545
- # or user is inactive, go away.
2546
- elif user is None or (not user.is_active):
2547
- log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
2548
- return None
2549
-
2550
- self._rotate_session_id()
2551
- self.update_user_auth_stat(user)
2552
- return user
2553
-
2554
- def get_user_menu_access(self, menu_names: list[str] | None = None) -> set[str]:
2555
- if get_auth_manager().is_logged_in():
2556
- return self._get_user_permission_resources(g.user, "menu_access", resource_names=menu_names)
2557
- elif current_user_jwt:
2558
- return self._get_user_permission_resources(
2559
- # the current_user_jwt is a lazy proxy, so we need to ignore type checking
2560
- current_user_jwt, # type: ignore[arg-type]
2561
- "menu_access",
2562
- resource_names=menu_names,
2563
- )
2564
- else:
2565
- return self._get_user_permission_resources(None, "menu_access", resource_names=menu_names)
2566
-
2567
2160
  @staticmethod
2568
2161
  def ldap_extract_list(ldap_dict: dict[str, list[bytes]], field_name: str) -> list[str]:
2569
2162
  raw_list = ldap_dict.get(field_name, [])
@@ -2589,7 +2182,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2589
2182
  We need to do this upon successful authentication when using the
2590
2183
  database session backend.
2591
2184
  """
2592
- if conf.get("webserver", "SESSION_BACKEND") == "database":
2185
+ if conf.get("fab", "SESSION_BACKEND") == "database":
2593
2186
  session.sid = str(uuid.uuid4())
2594
2187
 
2595
2188
  def _get_microsoft_jwks(self) -> list[dict[str, Any]]:
@@ -2658,7 +2251,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2658
2251
 
2659
2252
  # perform the LDAP search
2660
2253
  log.debug(
2661
- "LDAP search for %r with fields %s in scope %r", filter_str, request_fields, self.auth_ldap_search
2254
+ "LDAP search for %r with fields %s in scope %r",
2255
+ filter_str,
2256
+ request_fields,
2257
+ self.auth_ldap_search,
2662
2258
  )
2663
2259
  raw_search_result = con.search_s(
2664
2260
  self.auth_ldap_search, ldap.SCOPE_SUBTREE, filter_str, request_fields
@@ -2721,77 +2317,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2721
2317
 
2722
2318
  return list(user_role_objects)
2723
2319
 
2724
- def _oauth_calculate_user_roles(self, userinfo) -> list[str]:
2725
- user_role_objects = set()
2726
-
2727
- # apply AUTH_ROLES_MAPPING
2728
- if self.auth_roles_mapping:
2729
- user_role_keys = userinfo.get("role_keys", [])
2730
- user_role_objects.update(self.get_roles_from_keys(user_role_keys))
2731
-
2732
- # apply AUTH_USER_REGISTRATION_ROLE
2733
- if self.auth_user_registration:
2734
- registration_role_name = self.auth_user_registration_role
2735
-
2736
- # if AUTH_USER_REGISTRATION_ROLE_JMESPATH is set,
2737
- # use it for the registration role
2738
- if self.auth_user_registration_role_jmespath:
2739
- import jmespath
2740
-
2741
- registration_role_name = jmespath.search(self.auth_user_registration_role_jmespath, userinfo)
2742
-
2743
- # lookup registration role in flask db
2744
- fab_role = self.find_role(registration_role_name)
2745
- if fab_role:
2746
- user_role_objects.add(fab_role)
2747
- else:
2748
- log.warning("Can't find AUTH_USER_REGISTRATION role: %s", registration_role_name)
2749
-
2750
- return list(user_role_objects)
2751
-
2752
- def _get_user_permission_resources(
2753
- self, user: User | None, action_name: str, resource_names: list[str] | None = None
2754
- ) -> set[str]:
2755
- """
2756
- Get resource names with a certain action name that a user has access to.
2757
-
2758
- Mainly used to fetch all menu permissions on a single db call, will also
2759
- check public permissions and builtin roles
2760
- """
2761
- if not resource_names:
2762
- resource_names = []
2763
-
2764
- db_role_ids = []
2765
- if user is None:
2766
- # include public role
2767
- roles = [self.get_public_role()]
2768
- else:
2769
- roles = user.roles
2770
- # First check against builtin (statically configured) roles
2771
- # because no database query is needed
2772
- result = set()
2773
- for role in roles:
2774
- if role.name in self.builtin_roles:
2775
- for resource_name in resource_names:
2776
- if self._has_access_builtin_roles(role, action_name, resource_name):
2777
- result.add(resource_name)
2778
- else:
2779
- db_role_ids.append(role.id)
2780
- # Then check against database-stored roles
2781
- role_resource_names = [
2782
- perm.resource.name for perm in self.filter_roles_by_perm_with_action(action_name, db_role_ids)
2783
- ]
2784
- result.update(role_resource_names)
2785
- return result
2786
-
2787
- def _has_access_builtin_roles(self, role, action_name: str, resource_name: str) -> bool:
2788
- """Check permission on builtin role."""
2789
- perms = self.builtin_roles.get(role.name, [])
2790
- for _resource_name, _action_name in perms:
2791
- if re2.match(_resource_name, resource_name) and re2.match(_action_name, action_name):
2792
- return True
2793
- return False
2794
-
2795
2320
  def _merge_perm(self, action_name: str, resource_name: str) -> None:
2796
2321
  """
2797
2322
  Add the new (action, resource) to assoc_permission_role if it doesn't exist.
@@ -2831,7 +2356,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2831
2356
  (action_name, resource_name): viewmodel
2832
2357
  for action_name, resource_name, viewmodel in (
2833
2358
  self.appbuilder.get_session.execute(
2834
- select(self.action_model.name, self.resource_model.name, self.permission_model)
2359
+ select(
2360
+ self.action_model.name,
2361
+ self.resource_model.name,
2362
+ self.permission_model,
2363
+ )
2835
2364
  .join(self.permission_model.action)
2836
2365
  .join(self.permission_model.resource)
2837
2366
  .where(~self.resource_model.name.like(f"{permissions.RESOURCE_DAG_PREFIX}%"))
@@ -2839,32 +2368,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2839
2368
  )
2840
2369
  }
2841
2370
 
2842
- def filter_roles_by_perm_with_action(self, action_name: str, role_ids: list[int]):
2843
- """Find roles with permission."""
2844
- return (
2845
- self.appbuilder.get_session.query(self.permission_model)
2846
- .join(
2847
- assoc_permission_role,
2848
- and_(self.permission_model.id == assoc_permission_role.c.permission_view_id),
2849
- )
2850
- .join(self.role_model)
2851
- .join(self.action_model)
2852
- .join(self.resource_model)
2853
- .filter(
2854
- self.action_model.name == action_name,
2855
- self.role_model.id.in_(role_ids),
2856
- )
2857
- ).all()
2858
-
2859
- def _get_root_dag_id(self, dag_id: str) -> str:
2860
- # TODO: The "root_dag_id" check can be remove when the minimum version of Airflow is bumped to 3.0
2861
- if "." in dag_id and hasattr(DagModel, "root_dag_id"):
2862
- dm = self.appbuilder.get_session.execute(
2863
- select(DagModel.dag_id, DagModel.root_dag_id).where(DagModel.dag_id == dag_id)
2864
- ).one()
2865
- return dm.root_dag_id or dm.dag_id
2866
- return dag_id
2867
-
2868
2371
  @staticmethod
2869
2372
  def _cli_safe_flash(text: str, level: str) -> None:
2870
2373
  """Show a flash in a web context or prints a message if not."""
@@ -2872,3 +2375,31 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2872
2375
  flash(Markup(text), level)
2873
2376
  else:
2874
2377
  getattr(log, level)(text.replace("<br>", "\n").replace("<b>", "*").replace("</b>", "*"))
2378
+
2379
+ def _oauth_calculate_user_roles(self, userinfo) -> list[str]:
2380
+ user_role_objects = set()
2381
+
2382
+ # apply AUTH_ROLES_MAPPING
2383
+ if self.auth_roles_mapping:
2384
+ user_role_keys = userinfo.get("role_keys", [])
2385
+ user_role_objects.update(self.get_roles_from_keys(user_role_keys))
2386
+
2387
+ # apply AUTH_USER_REGISTRATION_ROLE
2388
+ if self.auth_user_registration:
2389
+ registration_role_name = self.auth_user_registration_role
2390
+
2391
+ # if AUTH_USER_REGISTRATION_ROLE_JMESPATH is set,
2392
+ # use it for the registration role
2393
+ if self.auth_user_registration_role_jmespath:
2394
+ import jmespath
2395
+
2396
+ registration_role_name = jmespath.search(self.auth_user_registration_role_jmespath, userinfo)
2397
+
2398
+ # lookup registration role in flask db
2399
+ fab_role = self.find_role(registration_role_name)
2400
+ if fab_role:
2401
+ user_role_objects.add(fab_role)
2402
+ else:
2403
+ log.warning("Can't find AUTH_USER_REGISTRATION role: %s", registration_role_name)
2404
+
2405
+ return list(user_role_objects)