apache-airflow-providers-fab 2.0.0rc1__py3-none-any.whl → 2.0.0rc2__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 (88) hide show
  1. airflow/providers/fab/LICENSE +0 -52
  2. airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -4
  3. airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
  4. airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
  5. airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -14
  6. airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -13
  7. airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
  8. airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
  9. airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
  10. airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
  11. airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +153 -0
  12. airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
  13. airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
  14. airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
  15. airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
  16. airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
  17. airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
  18. airflow/providers/fab/auth_manager/cli_commands/utils.py +17 -4
  19. airflow/providers/fab/auth_manager/fab_auth_manager.py +222 -119
  20. airflow/providers/fab/auth_manager/models/__init__.py +1 -1
  21. airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
  22. airflow/providers/fab/auth_manager/models/db.py +22 -5
  23. airflow/providers/fab/auth_manager/openapi/v1.yaml +9 -0
  24. airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
  25. airflow/providers/fab/auth_manager/security_manager/override.py +89 -561
  26. airflow/providers/fab/auth_manager/views/permissions.py +1 -1
  27. airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
  28. airflow/providers/fab/auth_manager/views/user.py +1 -1
  29. airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
  30. airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
  31. airflow/providers/fab/get_provider_info.py +26 -15
  32. airflow/providers/fab/www/airflow_flask_app.py +31 -0
  33. airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
  34. airflow/providers/fab/www/api_connexion/parameters.py +131 -0
  35. airflow/providers/fab/www/api_connexion/security.py +84 -0
  36. airflow/providers/fab/www/api_connexion/types.py +30 -0
  37. airflow/providers/fab/www/app.py +34 -9
  38. airflow/providers/fab/www/auth.py +350 -0
  39. airflow/providers/fab/www/constants.py +28 -0
  40. airflow/providers/fab/www/extensions/init_appbuilder.py +54 -9
  41. airflow/providers/fab/www/extensions/init_jinja_globals.py +5 -3
  42. airflow/providers/fab/www/extensions/init_security.py +19 -0
  43. airflow/providers/fab/www/extensions/init_session.py +64 -0
  44. airflow/providers/fab/www/extensions/init_views.py +111 -1
  45. airflow/providers/fab/www/package-lock.json +4967 -16517
  46. airflow/providers/fab/www/package.json +25 -104
  47. airflow/providers/fab/www/security/__init__.py +17 -0
  48. airflow/providers/fab/www/security/permissions.py +126 -0
  49. airflow/providers/fab/www/security_appless.py +44 -0
  50. airflow/providers/fab/www/security_manager.py +122 -0
  51. airflow/providers/fab/www/session.py +41 -0
  52. airflow/providers/fab/www/static/css/flash.css +57 -0
  53. airflow/providers/fab/www/static/dist/48f0ea180c40270a5b05.png +1 -0
  54. airflow/providers/fab/www/static/dist/649c0b07771e68fafdeb.png +1 -0
  55. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
  56. airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
  57. airflow/providers/fab/www/static/dist/f7490d556a6c42e49ba4.png +1 -0
  58. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
  59. airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
  60. airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
  61. airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
  62. airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
  63. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
  64. airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
  65. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.css +18 -0
  66. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js +2 -0
  67. airflow/providers/fab/www/static/dist/main.edb2d40dfbbc537916e3.js.LICENSE.txt +18 -0
  68. airflow/providers/fab/www/static/dist/manifest.json +20 -0
  69. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
  70. airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
  71. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js +2 -0
  72. airflow/providers/fab/www/static/dist/moment.624b1f00ba723d39ce06.js.LICENSE.txt +11 -0
  73. airflow/providers/fab/www/static/dist/oss-licenses.json +20 -0
  74. airflow/providers/fab/www/templates/airflow/main.html +10 -11
  75. airflow/providers/fab/www/templates/airflow/traceback.html +1 -5
  76. airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
  77. airflow/providers/fab/www/templates/appbuilder/navbar.html +7 -0
  78. airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
  79. airflow/providers/fab/www/utils.py +272 -0
  80. airflow/providers/fab/www/views.py +129 -0
  81. airflow/providers/fab/www/webpack.config.js +5 -40
  82. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/METADATA +24 -34
  83. apache_airflow_providers_fab-2.0.0rc2.dist-info/RECORD +125 -0
  84. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/WHEEL +1 -1
  85. airflow/providers/fab/auth_manager/decorators/auth.py +0 -127
  86. apache_airflow_providers_fab-2.0.0rc1.dist-info/RECORD +0 -78
  87. /airflow/providers/fab/{auth_manager/decorators → www/api_connexion}/__init__.py +0 -0
  88. {apache_airflow_providers_fab-2.0.0rc1.dist-info → apache_airflow_providers_fab-2.0.0rc2.dist-info}/entry_points.txt +0 -0
@@ -21,15 +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
- from collections.abc import Collection, Iterable, Mapping, Sequence
28
- from typing import TYPE_CHECKING, Any, Callable
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
29
  from flask import flash, g, has_request_context, session
34
30
  from flask_appbuilder import const
35
31
  from flask_appbuilder.const import (
@@ -58,25 +54,21 @@ from flask_appbuilder.security.views import (
58
54
  AuthOAuthView,
59
55
  AuthOIDView,
60
56
  AuthRemoteUserView,
61
- AuthView,
62
57
  RegisterUserModelView,
63
58
  )
64
- from flask_appbuilder.views import expose
65
59
  from flask_babel import lazy_gettext
66
- from flask_jwt_extended import JWTManager, current_user as current_user_jwt
60
+ from flask_jwt_extended import JWTManager
67
61
  from flask_login import LoginManager
68
62
  from itsdangerous import want_bytes
69
63
  from markupsafe import Markup
70
- from sqlalchemy import and_, func, inspect, literal, or_, select
64
+ from sqlalchemy import func, inspect, or_, select
71
65
  from sqlalchemy.exc import MultipleResultsFound
72
66
  from sqlalchemy.orm import joinedload
73
67
  from werkzeug.security import check_password_hash, generate_password_hash
74
68
 
75
- from airflow import __version__ as airflow_version
76
- from airflow.api_fastapi.app import get_auth_manager
77
69
  from airflow.configuration import conf
78
70
  from airflow.exceptions import AirflowException
79
- from airflow.models import DagBag, DagModel
71
+ from airflow.models import DagBag
80
72
  from airflow.providers.fab.auth_manager.models import (
81
73
  Action,
82
74
  Permission,
@@ -84,7 +76,6 @@ from airflow.providers.fab.auth_manager.models import (
84
76
  Resource,
85
77
  Role,
86
78
  User,
87
- assoc_permission_role,
88
79
  )
89
80
  from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser
90
81
  from airflow.providers.fab.auth_manager.security_manager.constants import EXISTING_ROLES
@@ -107,14 +98,24 @@ from airflow.providers.fab.auth_manager.views.user_edit import (
107
98
  CustomUserInfoEditView,
108
99
  )
109
100
  from airflow.providers.fab.auth_manager.views.user_stats import CustomUserStatsChartView
110
- from airflow.security import permissions
111
- from airflow.www.security_manager import AirflowSecurityManagerV2
112
- 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
113
108
 
114
109
  if TYPE_CHECKING:
115
- from airflow.security.permissions import RESOURCE_ASSET
110
+ from airflow.providers.fab.www.security.permissions import (
111
+ RESOURCE_ASSET,
112
+ RESOURCE_ASSET_ALIAS,
113
+ )
116
114
  else:
117
- 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
+ )
118
119
 
119
120
  log = logging.getLogger(__name__)
120
121
 
@@ -127,29 +128,6 @@ log = logging.getLogger(__name__)
127
128
  MAX_NUM_DATABASE_USER_SESSIONS = 50000
128
129
 
129
130
 
130
- # The following logic patches the logout method within AuthView, so it supports POST method
131
- # to make CSRF protection effective. It is backward-compatible with Airflow versions <= 2.9.2 as it still
132
- # allows utilizing the GET method for them.
133
- # You could remove the patch and configure it when it is supported
134
- # natively by Flask-AppBuilder (https://github.com/dpgaspar/Flask-AppBuilder/issues/2248)
135
- if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
136
- "2.10.0"
137
- ):
138
- _methods = ["GET", "POST"]
139
- else:
140
- _methods = ["POST"]
141
-
142
-
143
- class _ModifiedAuthView(AuthView):
144
- @expose("/logout/", methods=_methods)
145
- def logout(self):
146
- return super().logout()
147
-
148
-
149
- for auth_view in [AuthDBView, AuthLDAPView, AuthOAuthView, AuthOIDView, AuthRemoteUserView]:
150
- auth_view.__bases__ = (_ModifiedAuthView,)
151
-
152
-
153
131
  class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
154
132
  """
155
133
  This security manager overrides the default AirflowSecurityManager security manager.
@@ -210,8 +188,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
210
188
 
211
189
  jwt_manager = None
212
190
  """ Flask-JWT-Extended """
213
- oid = None
214
- """ Flask-OpenID OpenID """
215
191
  oauth = None
216
192
  oauth_remotes: dict[str, Any]
217
193
  """ Initialized (remote_app) providers dict {'provider_name', OBJ } """
@@ -234,11 +210,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
234
210
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_DEPENDENCIES),
235
211
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_CODE),
236
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),
237
215
  (permissions.ACTION_CAN_READ, RESOURCE_ASSET),
216
+ (permissions.ACTION_CAN_READ, RESOURCE_ASSET_ALIAS),
217
+ (permissions.ACTION_CAN_READ, RESOURCE_BACKFILL),
238
218
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
239
219
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_POOL),
240
220
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR),
241
- (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
242
221
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_JOB),
243
222
  (permissions.ACTION_CAN_READ, permissions.RESOURCE_MY_PASSWORD),
244
223
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_MY_PASSWORD),
@@ -302,8 +281,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
302
281
  (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
303
282
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
304
283
  (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
305
- (permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
306
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),
307
289
  ]
308
290
  # [END security_op_perms]
309
291
 
@@ -550,7 +532,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
550
532
  return self.update_user(user)
551
533
 
552
534
  def reset_user_sessions(self, user: User) -> None:
553
- 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
+ ):
554
541
  interface = self.appbuilder.get_app.session_interface
555
542
  session = interface.db.session
556
543
  user_session_model = interface.sql_session_model
@@ -572,6 +559,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
572
559
  session_details = interface.serializer.loads(want_bytes(s.data))
573
560
  if session_details.get("_user_id") == user.id:
574
561
  session.delete(s)
562
+ session.commit()
575
563
  else:
576
564
  self._cli_safe_flash(
577
565
  "Since you are using `securecookie` session backend mechanism, we cannot prevent "
@@ -716,39 +704,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
716
704
  """The mapping of auth roles."""
717
705
  return self.appbuilder.get_app.config["AUTH_ROLES_MAPPING"]
718
706
 
719
- @property
720
- def auth_user_registration_role_jmespath(self) -> str:
721
- """The JMESPATH role to use for user registration."""
722
- return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
723
-
724
- @property
725
- def auth_remote_user_env_var(self) -> str:
726
- return self.appbuilder.get_app.config["AUTH_REMOTE_USER_ENV_VAR"]
727
-
728
- @property
729
- def api_login_allow_multiple_providers(self):
730
- return self.appbuilder.get_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
731
-
732
707
  @property
733
708
  def auth_username_ci(self):
734
709
  """Get the auth username for CI."""
735
710
  return self.appbuilder.get_app.config.get("AUTH_USERNAME_CI", True)
736
711
 
737
- @property
738
- def auth_ldap_bind_first(self):
739
- """LDAP bind first."""
740
- return self.appbuilder.get_app.config["AUTH_LDAP_BIND_FIRST"]
741
-
742
- @property
743
- def openid_providers(self):
744
- """Openid providers."""
745
- return self.appbuilder.get_app.config["OPENID_PROVIDERS"]
746
-
747
- @property
748
- def auth_type_provider_name(self):
749
- provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
750
- return provider_to_auth_type.get(self.auth_type)
751
-
752
712
  @property
753
713
  def auth_user_registration(self):
754
714
  """Will user self registration be allowed."""
@@ -778,43 +738,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
778
738
  """Get the builtin roles."""
779
739
  return self._builtin_roles
780
740
 
781
- def create_admin_standalone(self) -> tuple[str | None, str | None]:
782
- """Create an Admin user with a random password so that users can access airflow."""
783
- from airflow.configuration import AIRFLOW_HOME, make_group_other_inaccessible
784
-
785
- user_name = "admin"
786
-
787
- # We want a streamlined first-run experience, but we do not want to
788
- # use a preset password as people will inevitably run this on a public
789
- # server. Thus, we make a random password and store it in AIRFLOW_HOME,
790
- # with the reasoning that if you can read that directory, you can see
791
- # the database credentials anyway.
792
- password_path = os.path.join(AIRFLOW_HOME, "standalone_admin_password.txt")
793
-
794
- user_exists = self.find_user(user_name) is not None
795
- we_know_password = os.path.isfile(password_path)
796
-
797
- # If the user does not exist, make a random password and make it
798
- if not user_exists:
799
- print(f"FlaskAppBuilder Authentication Manager: Creating {user_name} user")
800
- if (role := self.find_role("Admin")) is None:
801
- raise AirflowException("Unable to find role 'Admin'")
802
- # password does not contain visually similar characters: ijlIJL1oO0
803
- password = "".join(random.choices("abcdefghkmnpqrstuvwxyzABCDEFGHKMNPQRSTUVWXYZ23456789", k=16))
804
- with open(password_path, "w") as file:
805
- file.write(password)
806
- make_group_other_inaccessible(password_path)
807
- self.add_user(user_name, "Admin", "User", "admin@example.com", role, password)
808
- print(f"FlaskAppBuilder Authentication Manager: Created {user_name} user")
809
- # If the user does exist, and we know its password, read the password
810
- elif user_exists and we_know_password:
811
- with open(password_path) as file:
812
- password = file.read().strip()
813
- # Otherwise we don't know the password
814
- else:
815
- password = None
816
- return user_name, password
817
-
818
741
  def _init_config(self):
819
742
  """
820
743
  Initialize config.
@@ -834,6 +757,24 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
834
757
  app.config.setdefault("AUTH_ROLES_SYNC_AT_LOGIN", False)
835
758
  app.config.setdefault("AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS", False)
836
759
 
760
+ from packaging.version import Version
761
+ from werkzeug import __version__ as werkzeug_version
762
+
763
+ parsed_werkzeug_version = Version(werkzeug_version)
764
+ if parsed_werkzeug_version < Version("3.0.0"):
765
+ app.config.setdefault(
766
+ "AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
767
+ "pbkdf2:sha256:150000$Z3t6fmj2$22da622d94a1f8118"
768
+ "c0976a03d2f18f680bfff877c9a965db9eedc51bc0be87c",
769
+ )
770
+ else:
771
+ app.config.setdefault(
772
+ "AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
773
+ "scrypt:32768:8:1$wiDa0ruWlIPhp9LM$6e409d093e62ad54df2af895d0e125b05ff6cf6414"
774
+ "8350189ffc4bcc71286edf1b8ad94a442c00f890224bf2b32153d0750c89ee9"
775
+ "401e62f9dcee5399065e4e5",
776
+ )
777
+
837
778
  # LDAP Config
838
779
  if self.auth_type == AUTH_LDAP:
839
780
  if "AUTH_LDAP_SERVER" not in app.config:
@@ -945,27 +886,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
945
886
  log.exception(const.LOGMSG_ERR_SEC_CREATE_DB)
946
887
  exit(1)
947
888
 
948
- @staticmethod
949
- def get_readable_dag_ids(user=None) -> set[str]:
950
- """Get the DAG IDs readable by authenticated user."""
951
- return get_auth_manager().get_permitted_dag_ids(methods=["GET"], user=user)
952
-
953
- @staticmethod
954
- def get_editable_dag_ids(user=None) -> set[str]:
955
- """Get the DAG IDs editable by authenticated user."""
956
- return get_auth_manager().get_permitted_dag_ids(methods=["PUT"], user=user)
957
-
958
- def can_access_some_dags(self, action: str, dag_id: str | None = None) -> bool:
959
- """Check if user has read or write access to some dags."""
960
- if dag_id and dag_id != "~":
961
- root_dag_id = self._get_root_dag_id(dag_id)
962
- return self.has_access(action, self._resource_name(root_dag_id, permissions.RESOURCE_DAG))
963
-
964
- user = g.user
965
- if action == permissions.ACTION_CAN_READ:
966
- return any(self.get_readable_dag_ids(user))
967
- return any(self.get_editable_dag_ids(user))
968
-
969
889
  def get_all_permissions(self) -> set[tuple[str, str]]:
970
890
  """Return all permissions as a set of tuples with the action and resource names."""
971
891
  return set(
@@ -992,8 +912,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
992
912
  dags = dagbag.dags.values()
993
913
 
994
914
  for dag in dags:
995
- # TODO: Remove this when the minimum version of Airflow is bumped to 3.0
996
- root_dag_id = (getattr(dag, "parent_dag", None) or dag).dag_id
915
+ root_dag_id = dag.dag_id
997
916
  for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
998
917
  dag_resource_name = self._resource_name(root_dag_id, resource_name)
999
918
  for action_name in resource_values["actions"]:
@@ -1003,12 +922,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1003
922
  if dag.access_control is not None:
1004
923
  self.sync_perm_for_dag(root_dag_id, dag.access_control)
1005
924
 
1006
- def is_dag_resource(self, resource_name: str) -> bool:
1007
- """Determine if a resource belongs to a DAG or all DAGs."""
1008
- if resource_name == permissions.RESOURCE_DAG:
1009
- return True
1010
- return resource_name.startswith(permissions.RESOURCE_DAG_PREFIX)
1011
-
1012
925
  def sync_perm_for_dag(
1013
926
  self,
1014
927
  dag_id: str,
@@ -1068,7 +981,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1068
981
  def _get_or_create_dag_permission(action_name: str, dag_resource_name: str) -> Permission | None:
1069
982
  perm = self.get_permission(action_name, dag_resource_name)
1070
983
  if not perm:
1071
- self.log.info("Creating new action '%s' on resource '%s'", action_name, dag_resource_name)
984
+ self.log.info(
985
+ "Creating new action '%s' on resource '%s'",
986
+ action_name,
987
+ dag_resource_name,
988
+ )
1072
989
  perm = self.create_permission(action_name, dag_resource_name)
1073
990
  return perm
1074
991
 
@@ -1153,6 +1070,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1153
1070
  action = self.create_permission(action_name, resource_name)
1154
1071
  if self.auth_role_admin not in self.builtin_roles:
1155
1072
  admin_role = self.find_role(self.auth_role_admin)
1073
+ if not admin_role:
1074
+ admin_role = self.add_role(self.auth_role_admin)
1156
1075
  self.add_permission_to_role(admin_role, action)
1157
1076
  else:
1158
1077
  # Permissions on this view exist but....
@@ -1195,31 +1114,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1195
1114
  role_admin = self.find_role(self.auth_role_admin)
1196
1115
  self.add_permission_to_role(role_admin, perm)
1197
1116
 
1198
- def security_cleanup(self, baseviews, menus):
1199
- """
1200
- Cleanup all unused permissions from the database.
1201
-
1202
- :param baseviews: A list of BaseViews class
1203
- :param menus: Menu class
1204
- """
1205
- resources = self.get_all_resources()
1206
- roles = self.get_all_roles()
1207
- for resource in resources:
1208
- found = False
1209
- for baseview in baseviews:
1210
- if resource.name == baseview.class_permission_name:
1211
- found = True
1212
- break
1213
- if menus.find(resource.name):
1214
- found = True
1215
- if not found:
1216
- permissions = self.get_resource_permissions(resource)
1217
- for permission in permissions:
1218
- for role in roles:
1219
- self.remove_permission_from_role(role, permission)
1220
- self.delete_permission(permission.action.name, resource.name)
1221
- self.delete_resource(resource.name)
1222
-
1223
1117
  def sync_roles(self) -> None:
1224
1118
  """
1225
1119
  Initialize default and custom roles with related permissions.
@@ -1299,40 +1193,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1299
1193
  if deleted_count:
1300
1194
  self.log.info("Deleted %s faulty permissions", deleted_count)
1301
1195
 
1302
- def permission_exists_in_one_or_more_roles(
1303
- self, resource_name: str, action_name: str, role_ids: list[int]
1304
- ) -> bool:
1305
- """
1306
- Efficiently check if a certain permission exists on a list of role ids; used by `has_access`.
1307
-
1308
- :param resource_name: The view's name to check if exists on one of the roles
1309
- :param action_name: The permission name to check if exists
1310
- :param role_ids: a list of Role ids
1311
- :return: Boolean
1312
- """
1313
- q = (
1314
- self.appbuilder.get_session.query(self.permission_model)
1315
- .join(
1316
- assoc_permission_role,
1317
- and_(self.permission_model.id == assoc_permission_role.c.permission_view_id),
1318
- )
1319
- .join(self.role_model)
1320
- .join(self.action_model)
1321
- .join(self.resource_model)
1322
- .filter(
1323
- self.resource_model.name == resource_name,
1324
- self.action_model.name == action_name,
1325
- self.role_model.id.in_(role_ids),
1326
- )
1327
- .exists()
1328
- )
1329
- # Special case for MSSQL/Oracle (works on PG and MySQL > 8)
1330
- # Note: We need to keep MSSQL compatibility as long as this provider package
1331
- # might still be updated by Airflow prior 2.9.0 users with MSSQL
1332
- if self.appbuilder.get_session.bind.dialect.name in ("mssql", "oracle"):
1333
- return self.appbuilder.get_session.query(literal(True)).filter(q).scalar()
1334
- return self.appbuilder.get_session.query(q).scalar()
1335
-
1336
1196
  def perms_include_action(self, perms, action_name):
1337
1197
  return any(perm.action and perm.action.name == action_name for perm in perms)
1338
1198
 
@@ -1354,15 +1214,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1354
1214
  if perm not in role.permissions:
1355
1215
  self.add_permission_to_role(role, perm)
1356
1216
 
1357
- def sync_resource_permissions(self, perms: Iterable[tuple[str, str]] | None = None) -> None:
1358
- """Populate resource-based permissions."""
1359
- if not perms:
1360
- return
1361
-
1362
- for action_name, resource_name in perms:
1363
- self.create_resource(resource_name)
1364
- self.create_permission(action_name, resource_name)
1365
-
1366
1217
  """
1367
1218
  -----------
1368
1219
  Role entity
@@ -1446,7 +1297,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1446
1297
  if fab_role:
1447
1298
  _roles.add(fab_role)
1448
1299
  else:
1449
- log.warning("Can't find role specified in AUTH_ROLES_MAPPING: %s", fab_role_name)
1300
+ log.warning(
1301
+ "Can't find role specified in AUTH_ROLES_MAPPING: %s",
1302
+ fab_role_name,
1303
+ )
1450
1304
  return _roles
1451
1305
 
1452
1306
  def get_public_role(self):
@@ -1554,13 +1408,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1554
1408
  log.error("Multiple results found for user with email %s", email)
1555
1409
  return None
1556
1410
 
1557
- def find_register_user(self, registration_hash):
1558
- return self.get_session.scalar(
1559
- select(self.registeruser_mode)
1560
- .where(self.registeruser_model.registration_hash == registration_hash)
1561
- .limit(1)
1562
- )
1563
-
1564
1411
  def update_user(self, user: User) -> bool:
1565
1412
  try:
1566
1413
  self.get_session.merge(user)
@@ -1710,38 +1557,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1710
1557
  self.get_session.rollback()
1711
1558
  return resource
1712
1559
 
1713
- def get_all_resources(self) -> list[Resource]:
1714
- """Get all existing resource records."""
1715
- return self.get_session.query(self.resource_model).all()
1716
-
1717
- def delete_resource(self, name: str) -> bool:
1718
- """
1719
- Delete a Resource from the backend.
1720
-
1721
- :param name:
1722
- name of the resource
1723
- """
1724
- resource = self.get_resource(name)
1725
- if not resource:
1726
- log.warning(const.LOGMSG_WAR_SEC_DEL_VIEWMENU, name)
1727
- return False
1728
- try:
1729
- perms = (
1730
- self.get_session.query(self.permission_model)
1731
- .filter(self.permission_model.resource == resource)
1732
- .all()
1733
- )
1734
- if perms:
1735
- log.warning(const.LOGMSG_WAR_SEC_DEL_VIEWMENU_PVM, resource, perms)
1736
- return False
1737
- self.get_session.delete(resource)
1738
- self.get_session.commit()
1739
- return True
1740
- except Exception as e:
1741
- log.error(const.LOGMSG_ERR_SEC_DEL_PERMISSION, e)
1742
- self.get_session.rollback()
1743
- return False
1744
-
1745
1560
  """
1746
1561
  ---------------
1747
1562
  Permission entity
@@ -1871,13 +1686,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
1871
1686
  log.error(const.LOGMSG_ERR_SEC_DEL_PERMROLE, e)
1872
1687
  self.get_session.rollback()
1873
1688
 
1874
- def get_oid_identity_url(self, provider_name: str) -> str | None:
1875
- """Return the OIDC identity provider URL."""
1876
- for provider in self.openid_providers:
1877
- if provider.get("name") == provider_name:
1878
- return provider.get("url")
1879
- return None
1880
-
1881
1689
  @staticmethod
1882
1690
  def get_user_roles(user=None):
1883
1691
  """
@@ -2080,6 +1888,20 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2080
1888
  log.error(e)
2081
1889
  return None
2082
1890
 
1891
+ def check_password(self, username, password) -> bool:
1892
+ """
1893
+ Check if the password is correct for the username.
1894
+
1895
+ :param username: the username
1896
+ :param password: the password
1897
+ """
1898
+ user = self.find_user(username=username)
1899
+ if user is None:
1900
+ user = self.find_user(email=username)
1901
+ if user is None:
1902
+ return False
1903
+ return check_password_hash(user.password, password)
1904
+
2083
1905
  def auth_user_db(self, username, password):
2084
1906
  """
2085
1907
  Authenticate user, auth db style.
@@ -2097,8 +1919,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2097
1919
  if user is None or (not user.is_active):
2098
1920
  # Balance failure and success
2099
1921
  check_password_hash(
2100
- "pbkdf2:sha256:150000$Z3t6fmj2$22da622d94a1f8118"
2101
- "c0976a03d2f18f680bfff877c9a965db9eedc51bc0be87c",
1922
+ self.appbuilder.get_app.config["AUTH_DB_FAKE_PASSWORD_HASH_CHECK"],
2102
1923
  "password",
2103
1924
  )
2104
1925
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
@@ -2112,32 +1933,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2112
1933
  log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
2113
1934
  return None
2114
1935
 
2115
- def oauth_user_info_getter(
2116
- self,
2117
- func: Callable[[AirflowSecurityManagerV2, str, dict[str, Any] | None], dict[str, Any]],
2118
- ):
2119
- """
2120
- Get OAuth user info for all the providers.
2121
-
2122
- Receives provider and response return a dict with the information returned from the provider.
2123
- The returned user info dict should have its keys with the same name as the User Model.
2124
-
2125
- Use it like this an example for GitHub ::
2126
-
2127
- @appbuilder.sm.oauth_user_info_getter
2128
- def my_oauth_user_info(sm, provider, response=None):
2129
- if provider == "github":
2130
- me = sm.oauth_remotes[provider].get("user")
2131
- return {"username": me.data.get("login")}
2132
- return {}
2133
- """
2134
-
2135
- def wraps(provider: str, response: dict[str, Any] | None = None) -> dict[str, Any]:
2136
- return func(self, provider, response)
2137
-
2138
- self.oauth_user_info = wraps
2139
- return wraps
2140
-
2141
1936
  def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str, Any]:
2142
1937
  """
2143
1938
  There are different OAuth APIs with different ways to retrieve user info.
@@ -2259,183 +2054,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2259
2054
  log.debug("Token Get: %s", token)
2260
2055
  return token
2261
2056
 
2262
- def check_authorization(
2263
- self,
2264
- perms: Sequence[tuple[str, str]] | None = None,
2265
- dag_id: str | None = None,
2266
- ) -> bool:
2267
- """Check the logged-in user has the specified permissions."""
2268
- if not perms:
2269
- return True
2270
-
2271
- for perm in perms:
2272
- if perm in (
2273
- (permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
2274
- (permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG),
2275
- (permissions.ACTION_CAN_DELETE, permissions.RESOURCE_DAG),
2276
- ):
2277
- can_access_all_dags = self.has_access(*perm)
2278
- if not can_access_all_dags:
2279
- action = perm[0]
2280
- if not self.can_access_some_dags(action, dag_id):
2281
- return False
2282
- elif not self.has_access(*perm):
2283
- return False
2284
-
2285
- return True
2286
-
2287
- def set_oauth_session(self, provider, oauth_response):
2288
- """Set the current session with OAuth user secrets."""
2289
- # Get this provider key names for token_key and token_secret
2290
- token_key = self.get_oauth_token_key_name(provider)
2291
- token_secret = self.get_oauth_token_secret_name(provider)
2292
- # Save users token on encrypted session cookie
2293
- session["oauth"] = (
2294
- oauth_response[token_key],
2295
- oauth_response.get(token_secret, ""),
2296
- )
2297
- session["oauth_provider"] = provider
2298
-
2299
- def get_oauth_token_key_name(self, provider):
2300
- """
2301
- Return the token_key name for the oauth provider.
2302
-
2303
- If none is configured defaults to oauth_token
2304
- this is configured using OAUTH_PROVIDERS and token_key key.
2305
- """
2306
- for _provider in self.oauth_providers:
2307
- if _provider["name"] == provider:
2308
- return _provider.get("token_key", "oauth_token")
2309
-
2310
- def get_oauth_token_secret_name(self, provider):
2311
- """
2312
- Get the ``token_secret`` name for the oauth provider.
2313
-
2314
- If none is configured, defaults to ``oauth_secret``. This is configured
2315
- using ``OAUTH_PROVIDERS`` and ``token_secret``.
2316
- """
2317
- for _provider in self.oauth_providers:
2318
- if _provider["name"] == provider:
2319
- return _provider.get("token_secret", "oauth_token_secret")
2320
-
2321
- def auth_user_oauth(self, userinfo):
2322
- """
2323
- Authenticate user with OAuth.
2324
-
2325
- :userinfo: dict with user information
2326
- (keys are the same as User model columns)
2327
- """
2328
- # extract the username from `userinfo`
2329
- if "username" in userinfo:
2330
- username = userinfo["username"]
2331
- elif "email" in userinfo:
2332
- username = userinfo["email"]
2333
- else:
2334
- log.error("OAUTH userinfo does not have username or email %s", userinfo)
2335
- return None
2336
-
2337
- # If username is empty, go away
2338
- if (username is None) or username == "":
2339
- return None
2340
-
2341
- # Search the DB for this user
2342
- user = self.find_user(username=username)
2343
-
2344
- # If user is not active, go away
2345
- if user and (not user.is_active):
2346
- return None
2347
-
2348
- # If user is not registered, and not self-registration, go away
2349
- if (not user) and (not self.auth_user_registration):
2350
- return None
2351
-
2352
- # Sync the user's roles
2353
- if user and self.auth_roles_sync_at_login:
2354
- user.roles = self._oauth_calculate_user_roles(userinfo)
2355
- log.debug("Calculated new roles for user=%r as: %s", username, user.roles)
2356
-
2357
- # If the user is new, register them
2358
- if (not user) and self.auth_user_registration:
2359
- user = self.add_user(
2360
- username=username,
2361
- first_name=userinfo.get("first_name", ""),
2362
- last_name=userinfo.get("last_name", ""),
2363
- email=userinfo.get("email", "") or f"{username}@email.notfound",
2364
- role=self._oauth_calculate_user_roles(userinfo),
2365
- )
2366
- log.debug("New user registered: %s", user)
2367
-
2368
- # If user registration failed, go away
2369
- if not user:
2370
- log.error("Error creating a new OAuth user %s", username)
2371
- return None
2372
-
2373
- # LOGIN SUCCESS (only if user is now registered)
2374
- if user:
2375
- self._rotate_session_id()
2376
- self.update_user_auth_stat(user)
2377
- return user
2378
- else:
2379
- return None
2380
-
2381
- def auth_user_oid(self, email):
2382
- """
2383
- Openid user Authentication.
2384
-
2385
- :param email: user's email to authenticate
2386
- """
2387
- user = self.find_user(email=email)
2388
- if user is None or (not user.is_active):
2389
- log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, email)
2390
- return None
2391
- else:
2392
- self._rotate_session_id()
2393
- self.update_user_auth_stat(user)
2394
- return user
2395
-
2396
- def auth_user_remote_user(self, username):
2397
- """
2398
- REMOTE_USER user Authentication.
2399
-
2400
- :param username: user's username for remote auth
2401
- """
2402
- user = self.find_user(username=username)
2403
-
2404
- # User does not exist, create one if auto user registration.
2405
- if user is None and self.auth_user_registration:
2406
- user = self.add_user(
2407
- # All we have is REMOTE_USER, so we set
2408
- # the other fields to blank.
2409
- username=username,
2410
- first_name=username,
2411
- last_name="-",
2412
- email=username + "@email.notfound",
2413
- role=self.find_role(self.auth_user_registration_role),
2414
- )
2415
-
2416
- # If user does not exist on the DB and not auto user registration,
2417
- # or user is inactive, go away.
2418
- elif user is None or (not user.is_active):
2419
- log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
2420
- return None
2421
-
2422
- self._rotate_session_id()
2423
- self.update_user_auth_stat(user)
2424
- return user
2425
-
2426
- def get_user_menu_access(self, menu_names: list[str] | None = None) -> set[str]:
2427
- if get_auth_manager().is_logged_in():
2428
- return self._get_user_permission_resources(g.user, "menu_access", resource_names=menu_names)
2429
- elif current_user_jwt:
2430
- return self._get_user_permission_resources(
2431
- # the current_user_jwt is a lazy proxy, so we need to ignore type checking
2432
- current_user_jwt, # type: ignore[arg-type]
2433
- "menu_access",
2434
- resource_names=menu_names,
2435
- )
2436
- else:
2437
- return self._get_user_permission_resources(None, "menu_access", resource_names=menu_names)
2438
-
2439
2057
  @staticmethod
2440
2058
  def ldap_extract_list(ldap_dict: dict[str, list[bytes]], field_name: str) -> list[str]:
2441
2059
  raw_list = ldap_dict.get(field_name, [])
@@ -2530,7 +2148,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2530
2148
 
2531
2149
  # perform the LDAP search
2532
2150
  log.debug(
2533
- "LDAP search for %r with fields %s in scope %r", filter_str, request_fields, self.auth_ldap_search
2151
+ "LDAP search for %r with fields %s in scope %r",
2152
+ filter_str,
2153
+ request_fields,
2154
+ self.auth_ldap_search,
2534
2155
  )
2535
2156
  raw_search_result = con.search_s(
2536
2157
  self.auth_ldap_search, ldap.SCOPE_SUBTREE, filter_str, request_fields
@@ -2593,77 +2214,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2593
2214
 
2594
2215
  return list(user_role_objects)
2595
2216
 
2596
- def _oauth_calculate_user_roles(self, userinfo) -> list[str]:
2597
- user_role_objects = set()
2598
-
2599
- # apply AUTH_ROLES_MAPPING
2600
- if self.auth_roles_mapping:
2601
- user_role_keys = userinfo.get("role_keys", [])
2602
- user_role_objects.update(self.get_roles_from_keys(user_role_keys))
2603
-
2604
- # apply AUTH_USER_REGISTRATION_ROLE
2605
- if self.auth_user_registration:
2606
- registration_role_name = self.auth_user_registration_role
2607
-
2608
- # if AUTH_USER_REGISTRATION_ROLE_JMESPATH is set,
2609
- # use it for the registration role
2610
- if self.auth_user_registration_role_jmespath:
2611
- import jmespath
2612
-
2613
- registration_role_name = jmespath.search(self.auth_user_registration_role_jmespath, userinfo)
2614
-
2615
- # lookup registration role in flask db
2616
- fab_role = self.find_role(registration_role_name)
2617
- if fab_role:
2618
- user_role_objects.add(fab_role)
2619
- else:
2620
- log.warning("Can't find AUTH_USER_REGISTRATION role: %s", registration_role_name)
2621
-
2622
- return list(user_role_objects)
2623
-
2624
- def _get_user_permission_resources(
2625
- self, user: User | None, action_name: str, resource_names: list[str] | None = None
2626
- ) -> set[str]:
2627
- """
2628
- Get resource names with a certain action name that a user has access to.
2629
-
2630
- Mainly used to fetch all menu permissions on a single db call, will also
2631
- check public permissions and builtin roles
2632
- """
2633
- if not resource_names:
2634
- resource_names = []
2635
-
2636
- db_role_ids = []
2637
- if user is None:
2638
- # include public role
2639
- roles = [self.get_public_role()]
2640
- else:
2641
- roles = user.roles
2642
- # First check against builtin (statically configured) roles
2643
- # because no database query is needed
2644
- result = set()
2645
- for role in roles:
2646
- if role.name in self.builtin_roles:
2647
- for resource_name in resource_names:
2648
- if self._has_access_builtin_roles(role, action_name, resource_name):
2649
- result.add(resource_name)
2650
- else:
2651
- db_role_ids.append(role.id)
2652
- # Then check against database-stored roles
2653
- role_resource_names = [
2654
- perm.resource.name for perm in self.filter_roles_by_perm_with_action(action_name, db_role_ids)
2655
- ]
2656
- result.update(role_resource_names)
2657
- return result
2658
-
2659
- def _has_access_builtin_roles(self, role, action_name: str, resource_name: str) -> bool:
2660
- """Check permission on builtin role."""
2661
- perms = self.builtin_roles.get(role.name, [])
2662
- for _resource_name, _action_name in perms:
2663
- if re2.match(_resource_name, resource_name) and re2.match(_action_name, action_name):
2664
- return True
2665
- return False
2666
-
2667
2217
  def _merge_perm(self, action_name: str, resource_name: str) -> None:
2668
2218
  """
2669
2219
  Add the new (action, resource) to assoc_permission_role if it doesn't exist.
@@ -2703,7 +2253,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2703
2253
  (action_name, resource_name): viewmodel
2704
2254
  for action_name, resource_name, viewmodel in (
2705
2255
  self.appbuilder.get_session.execute(
2706
- select(self.action_model.name, self.resource_model.name, self.permission_model)
2256
+ select(
2257
+ self.action_model.name,
2258
+ self.resource_model.name,
2259
+ self.permission_model,
2260
+ )
2707
2261
  .join(self.permission_model.action)
2708
2262
  .join(self.permission_model.resource)
2709
2263
  .where(~self.resource_model.name.like(f"{permissions.RESOURCE_DAG_PREFIX}%"))
@@ -2711,32 +2265,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
2711
2265
  )
2712
2266
  }
2713
2267
 
2714
- def filter_roles_by_perm_with_action(self, action_name: str, role_ids: list[int]):
2715
- """Find roles with permission."""
2716
- return (
2717
- self.appbuilder.get_session.query(self.permission_model)
2718
- .join(
2719
- assoc_permission_role,
2720
- and_(self.permission_model.id == assoc_permission_role.c.permission_view_id),
2721
- )
2722
- .join(self.role_model)
2723
- .join(self.action_model)
2724
- .join(self.resource_model)
2725
- .filter(
2726
- self.action_model.name == action_name,
2727
- self.role_model.id.in_(role_ids),
2728
- )
2729
- ).all()
2730
-
2731
- def _get_root_dag_id(self, dag_id: str) -> str:
2732
- # TODO: The "root_dag_id" check can be remove when the minimum version of Airflow is bumped to 3.0
2733
- if "." in dag_id and hasattr(DagModel, "root_dag_id"):
2734
- dm = self.appbuilder.get_session.execute(
2735
- select(DagModel.dag_id, DagModel.root_dag_id).where(DagModel.dag_id == dag_id)
2736
- ).one()
2737
- return dm.root_dag_id or dm.dag_id
2738
- return dag_id
2739
-
2740
2268
  @staticmethod
2741
2269
  def _cli_safe_flash(text: str, level: str) -> None:
2742
2270
  """Show a flash in a web context or prints a message if not."""