apache-airflow-providers-fab 1.5.3rc1__py3-none-any.whl → 2.0.0b1__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.
- airflow/providers/fab/LICENSE +0 -52
- airflow/providers/fab/__init__.py +3 -3
- airflow/providers/fab/auth_manager/api/auth/backend/basic_auth.py +3 -3
- airflow/providers/fab/auth_manager/api/auth/backend/kerberos_auth.py +4 -4
- airflow/providers/fab/auth_manager/api/auth/backend/session.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/role_and_permission_endpoint.py +14 -13
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +12 -12
- airflow/providers/fab/auth_manager/api_fastapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/login.py +32 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v1-generated.yaml +152 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +51 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/__init__.py +16 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +58 -0
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -4
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/utils.py +10 -9
- airflow/providers/fab/auth_manager/fab_auth_manager.py +231 -126
- airflow/providers/fab/auth_manager/models/__init__.py +1 -1
- airflow/providers/fab/auth_manager/models/anonymous_user.py +1 -1
- airflow/providers/fab/auth_manager/models/db.py +22 -5
- airflow/providers/fab/auth_manager/schemas/user_schema.py +1 -1
- airflow/providers/fab/auth_manager/security_manager/override.py +71 -632
- airflow/providers/fab/auth_manager/views/permissions.py +1 -1
- airflow/providers/fab/auth_manager/views/roles_list.py +1 -1
- airflow/providers/fab/auth_manager/views/user.py +1 -1
- airflow/providers/fab/auth_manager/views/user_edit.py +1 -1
- airflow/providers/fab/auth_manager/views/user_stats.py +1 -1
- airflow/providers/fab/get_provider_info.py +22 -16
- airflow/providers/fab/www/airflow_flask_app.py +31 -0
- airflow/providers/fab/www/api_connexion/__init__.py +17 -0
- airflow/providers/fab/www/api_connexion/exceptions.py +197 -0
- airflow/providers/fab/www/api_connexion/parameters.py +131 -0
- airflow/providers/fab/www/api_connexion/security.py +84 -0
- airflow/providers/fab/www/api_connexion/types.py +30 -0
- airflow/providers/fab/www/app.py +112 -0
- airflow/providers/fab/www/auth.py +350 -0
- airflow/providers/fab/www/constants.py +28 -0
- airflow/providers/fab/www/extensions/__init__.py +16 -0
- airflow/providers/fab/www/extensions/init_appbuilder.py +602 -0
- airflow/providers/fab/www/extensions/init_jinja_globals.py +82 -0
- airflow/providers/fab/www/extensions/init_manifest_files.py +61 -0
- airflow/providers/fab/www/extensions/init_security.py +61 -0
- airflow/providers/fab/www/extensions/init_session.py +64 -0
- airflow/providers/fab/www/extensions/init_views.py +177 -0
- airflow/providers/fab/www/package-lock.json +10127 -0
- airflow/providers/fab/www/package.json +81 -0
- airflow/providers/fab/www/security/__init__.py +17 -0
- airflow/providers/fab/www/security/permissions.py +126 -0
- airflow/providers/fab/www/security_appless.py +44 -0
- airflow/providers/fab/www/security_manager.py +122 -0
- airflow/providers/fab/www/session.py +41 -0
- airflow/providers/fab/www/static/css/bootstrap-theme.css +6215 -0
- airflow/providers/fab/www/static/css/flash.css +57 -0
- airflow/providers/fab/www/static/css/loading-dots.css +60 -0
- airflow/providers/fab/www/static/css/main.css +676 -0
- airflow/providers/fab/www/static/css/material-icons.css +84 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.css +33 -0
- airflow/providers/fab/www/static/dist/airflowDefaultTheme.feec4a4075c2f3d6ae01.js +1 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.css +18 -0
- airflow/providers/fab/www/static/dist/flash.137b30cff85b5588e661.js +1 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.css +5 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js +2 -0
- airflow/providers/fab/www/static/dist/jquery-ui.min.js.LICENSE.txt +4 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.css +18 -0
- airflow/providers/fab/www/static/dist/loadingDots.48ab7d5b04e66f2686b0.js +1 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.css +18 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js +2 -0
- airflow/providers/fab/www/static/dist/main.ec1d38d994d72bb083cd.js.LICENSE.txt +18 -0
- airflow/providers/fab/www/static/dist/manifest.json +17 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.css +18 -0
- airflow/providers/fab/www/static/dist/materialIcons.57390fa60d8f61175334.js +1 -0
- airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js +2 -0
- airflow/providers/fab/www/static/dist/moment.4d28b37c229bdfc54575.js.LICENSE.txt +11 -0
- airflow/providers/fab/www/static/dist/oss-licenses.json +29 -0
- airflow/providers/fab/www/static/js/datetime_utils.js +134 -0
- airflow/providers/fab/www/static/js/main.js +324 -0
- airflow/providers/fab/www/static/sort_asc.png +0 -0
- airflow/providers/fab/www/static/sort_both.png +0 -0
- airflow/providers/fab/www/static/sort_desc.png +0 -0
- airflow/providers/fab/www/templates/airflow/_messages.html +30 -0
- airflow/providers/fab/www/templates/airflow/error.html +35 -0
- airflow/providers/fab/www/templates/airflow/main.html +78 -0
- airflow/providers/fab/www/templates/airflow/traceback.html +53 -0
- airflow/providers/fab/www/templates/appbuilder/flash.html +34 -0
- airflow/providers/fab/www/templates/appbuilder/index.html +20 -0
- airflow/providers/fab/www/templates/appbuilder/navbar.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_menu.html +60 -0
- airflow/providers/fab/www/templates/appbuilder/navbar_right.html +64 -0
- airflow/providers/fab/www/utils.py +272 -0
- airflow/providers/fab/www/views.py +129 -0
- airflow/providers/fab/www/webpack.config.js +213 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/METADATA +18 -36
- apache_airflow_providers_fab-2.0.0b1.dist-info/RECORD +122 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/WHEEL +1 -1
- airflow/providers/fab/auth_manager/decorators/auth.py +0 -126
- apache_airflow_providers_fab-1.5.3rc1.dist-info/RECORD +0 -51
- /airflow/providers/fab/{auth_manager/decorators → www}/__init__.py +0 -0
- {apache_airflow_providers_fab-1.5.3rc1.dist-info → apache_airflow_providers_fab-2.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -24,13 +24,10 @@ import logging
|
|
24
24
|
import os
|
25
25
|
import random
|
26
26
|
import uuid
|
27
|
-
import
|
28
|
-
from typing import TYPE_CHECKING, Any
|
27
|
+
from collections.abc import Collection, Iterable, Mapping
|
28
|
+
from typing import TYPE_CHECKING, Any
|
29
29
|
|
30
30
|
import jwt
|
31
|
-
import packaging.version
|
32
|
-
import re2
|
33
|
-
from deprecated import deprecated
|
34
31
|
from flask import flash, g, has_request_context, session
|
35
32
|
from flask_appbuilder import const
|
36
33
|
from flask_appbuilder.const import (
|
@@ -59,25 +56,21 @@ from flask_appbuilder.security.views import (
|
|
59
56
|
AuthOAuthView,
|
60
57
|
AuthOIDView,
|
61
58
|
AuthRemoteUserView,
|
62
|
-
AuthView,
|
63
59
|
RegisterUserModelView,
|
64
60
|
)
|
65
|
-
from flask_appbuilder.views import expose
|
66
61
|
from flask_babel import lazy_gettext
|
67
|
-
from flask_jwt_extended import JWTManager
|
62
|
+
from flask_jwt_extended import JWTManager
|
68
63
|
from flask_login import LoginManager
|
69
64
|
from itsdangerous import want_bytes
|
70
65
|
from markupsafe import Markup
|
71
|
-
from sqlalchemy import
|
66
|
+
from sqlalchemy import func, inspect, or_, select
|
72
67
|
from sqlalchemy.exc import MultipleResultsFound
|
73
|
-
from sqlalchemy.orm import
|
68
|
+
from sqlalchemy.orm import joinedload
|
74
69
|
from werkzeug.security import check_password_hash, generate_password_hash
|
75
70
|
|
76
|
-
from airflow import __version__ as airflow_version
|
77
|
-
from airflow.auth.managers.utils.fab import get_method_from_fab_action_map
|
78
71
|
from airflow.configuration import conf
|
79
|
-
from airflow.exceptions import AirflowException
|
80
|
-
from airflow.models import DagBag
|
72
|
+
from airflow.exceptions import AirflowException
|
73
|
+
from airflow.models import DagBag
|
81
74
|
from airflow.providers.fab.auth_manager.models import (
|
82
75
|
Action,
|
83
76
|
Permission,
|
@@ -85,7 +78,6 @@ from airflow.providers.fab.auth_manager.models import (
|
|
85
78
|
Resource,
|
86
79
|
Role,
|
87
80
|
User,
|
88
|
-
assoc_permission_role,
|
89
81
|
)
|
90
82
|
from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser
|
91
83
|
from airflow.providers.fab.auth_manager.security_manager.constants import EXISTING_ROLES
|
@@ -108,17 +100,24 @@ from airflow.providers.fab.auth_manager.views.user_edit import (
|
|
108
100
|
CustomUserInfoEditView,
|
109
101
|
)
|
110
102
|
from airflow.providers.fab.auth_manager.views.user_stats import CustomUserStatsChartView
|
111
|
-
from airflow.security import permissions
|
112
|
-
from airflow.
|
113
|
-
from airflow.www.
|
114
|
-
|
115
|
-
|
103
|
+
from airflow.providers.fab.www.security import permissions
|
104
|
+
from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
105
|
+
from airflow.providers.fab.www.session import (
|
106
|
+
AirflowDatabaseSessionInterface,
|
107
|
+
AirflowDatabaseSessionInterface as FabAirflowDatabaseSessionInterface,
|
108
|
+
)
|
109
|
+
from airflow.security.permissions import RESOURCE_BACKFILL
|
116
110
|
|
117
111
|
if TYPE_CHECKING:
|
118
|
-
from airflow.
|
119
|
-
|
112
|
+
from airflow.providers.fab.www.security.permissions import (
|
113
|
+
RESOURCE_ASSET,
|
114
|
+
RESOURCE_ASSET_ALIAS,
|
115
|
+
)
|
120
116
|
else:
|
121
|
-
from airflow.providers.common.compat.security.permissions import
|
117
|
+
from airflow.providers.common.compat.security.permissions import (
|
118
|
+
RESOURCE_ASSET,
|
119
|
+
RESOURCE_ASSET_ALIAS,
|
120
|
+
)
|
122
121
|
|
123
122
|
log = logging.getLogger(__name__)
|
124
123
|
|
@@ -131,29 +130,6 @@ log = logging.getLogger(__name__)
|
|
131
130
|
MAX_NUM_DATABASE_USER_SESSIONS = 50000
|
132
131
|
|
133
132
|
|
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
133
|
class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
158
134
|
"""
|
159
135
|
This security manager overrides the default AirflowSecurityManager security manager.
|
@@ -214,8 +190,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
214
190
|
|
215
191
|
jwt_manager = None
|
216
192
|
""" Flask-JWT-Extended """
|
217
|
-
oid = None
|
218
|
-
""" Flask-OpenID OpenID """
|
219
193
|
oauth = None
|
220
194
|
oauth_remotes: dict[str, Any]
|
221
195
|
""" Initialized (remote_app) providers dict {'provider_name', OBJ } """
|
@@ -238,11 +212,14 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
238
212
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_DEPENDENCIES),
|
239
213
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_CODE),
|
240
214
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN),
|
215
|
+
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_VERSION),
|
216
|
+
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
|
241
217
|
(permissions.ACTION_CAN_READ, RESOURCE_ASSET),
|
218
|
+
(permissions.ACTION_CAN_READ, RESOURCE_ASSET_ALIAS),
|
219
|
+
(permissions.ACTION_CAN_READ, RESOURCE_BACKFILL),
|
242
220
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
|
243
221
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_POOL),
|
244
222
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_IMPORT_ERROR),
|
245
|
-
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_WARNING),
|
246
223
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_JOB),
|
247
224
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_MY_PASSWORD),
|
248
225
|
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_MY_PASSWORD),
|
@@ -306,8 +283,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
306
283
|
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
|
307
284
|
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
|
308
285
|
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
|
309
|
-
(permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
|
310
286
|
(permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
|
287
|
+
(permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
|
288
|
+
(permissions.ACTION_CAN_CREATE, RESOURCE_BACKFILL),
|
289
|
+
(permissions.ACTION_CAN_EDIT, RESOURCE_BACKFILL),
|
290
|
+
(permissions.ACTION_CAN_DELETE, RESOURCE_BACKFILL),
|
311
291
|
]
|
312
292
|
# [END security_op_perms]
|
313
293
|
|
@@ -554,7 +534,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
554
534
|
return self.update_user(user)
|
555
535
|
|
556
536
|
def reset_user_sessions(self, user: User) -> None:
|
557
|
-
if isinstance(
|
537
|
+
if isinstance(
|
538
|
+
self.appbuilder.get_app.session_interface, AirflowDatabaseSessionInterface
|
539
|
+
) or isinstance(
|
540
|
+
self.appbuilder.get_app.session_interface,
|
541
|
+
FabAirflowDatabaseSessionInterface,
|
542
|
+
):
|
558
543
|
interface = self.appbuilder.get_app.session_interface
|
559
544
|
session = interface.db.session
|
560
545
|
user_session_model = interface.sql_session_model
|
@@ -721,39 +706,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
721
706
|
"""The mapping of auth roles."""
|
722
707
|
return self.appbuilder.get_app.config["AUTH_ROLES_MAPPING"]
|
723
708
|
|
724
|
-
@property
|
725
|
-
def auth_user_registration_role_jmespath(self) -> str:
|
726
|
-
"""The JMESPATH role to use for user registration."""
|
727
|
-
return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
|
728
|
-
|
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
709
|
@property
|
738
710
|
def auth_username_ci(self):
|
739
711
|
"""Get the auth username for CI."""
|
740
712
|
return self.appbuilder.get_app.config.get("AUTH_USERNAME_CI", True)
|
741
713
|
|
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
714
|
@property
|
758
715
|
def auth_user_registration(self):
|
759
716
|
"""Will user self registration be allowed."""
|
@@ -774,14 +731,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
774
731
|
"""Get the admin role."""
|
775
732
|
return self.appbuilder.get_app.config["AUTH_ROLE_ADMIN"]
|
776
733
|
|
777
|
-
@property
|
778
|
-
@deprecated(
|
779
|
-
reason="The 'oauth_whitelists' property is deprecated. Please use 'oauth_allow_list' instead.",
|
780
|
-
category=AirflowProviderDeprecationWarning,
|
781
|
-
)
|
782
|
-
def oauth_whitelists(self):
|
783
|
-
return self.oauth_allow_list
|
784
|
-
|
785
734
|
def create_builtin_roles(self):
|
786
735
|
"""Return FAB builtin roles."""
|
787
736
|
return self.appbuilder.get_app.config.get("FAB_ROLES", {})
|
@@ -904,15 +853,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
904
853
|
:meta private:
|
905
854
|
"""
|
906
855
|
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
856
|
if self.auth_type == AUTH_OAUTH:
|
917
857
|
from authlib.integrations.flask_client import OAuth
|
918
858
|
|
@@ -985,91 +925,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
985
925
|
log.exception(const.LOGMSG_ERR_SEC_CREATE_DB)
|
986
926
|
exit(1)
|
987
927
|
|
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
928
|
def get_all_permissions(self) -> set[tuple[str, str]]:
|
1074
929
|
"""Return all permissions as a set of tuples with the action and resource names."""
|
1075
930
|
return set(
|
@@ -1096,8 +951,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1096
951
|
dags = dagbag.dags.values()
|
1097
952
|
|
1098
953
|
for dag in dags:
|
1099
|
-
|
1100
|
-
root_dag_id = (getattr(dag, "parent_dag", None) or dag).dag_id
|
954
|
+
root_dag_id = dag.dag_id
|
1101
955
|
for resource_name, resource_values in self.RESOURCE_DETAILS_MAP.items():
|
1102
956
|
dag_resource_name = self._resource_name(root_dag_id, resource_name)
|
1103
957
|
for action_name in resource_values["actions"]:
|
@@ -1107,23 +961,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1107
961
|
if dag.access_control is not None:
|
1108
962
|
self.sync_perm_for_dag(root_dag_id, dag.access_control)
|
1109
963
|
|
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
964
|
def sync_perm_for_dag(
|
1128
965
|
self,
|
1129
966
|
dag_id: str,
|
@@ -1183,7 +1020,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1183
1020
|
def _get_or_create_dag_permission(action_name: str, dag_resource_name: str) -> Permission | None:
|
1184
1021
|
perm = self.get_permission(action_name, dag_resource_name)
|
1185
1022
|
if not perm:
|
1186
|
-
self.log.info(
|
1023
|
+
self.log.info(
|
1024
|
+
"Creating new action '%s' on resource '%s'",
|
1025
|
+
action_name,
|
1026
|
+
dag_resource_name,
|
1027
|
+
)
|
1187
1028
|
perm = self.create_permission(action_name, dag_resource_name)
|
1188
1029
|
return perm
|
1189
1030
|
|
@@ -1268,6 +1109,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1268
1109
|
action = self.create_permission(action_name, resource_name)
|
1269
1110
|
if self.auth_role_admin not in self.builtin_roles:
|
1270
1111
|
admin_role = self.find_role(self.auth_role_admin)
|
1112
|
+
if not admin_role:
|
1113
|
+
admin_role = self.add_role(self.auth_role_admin)
|
1271
1114
|
self.add_permission_to_role(admin_role, action)
|
1272
1115
|
else:
|
1273
1116
|
# Permissions on this view exist but....
|
@@ -1310,31 +1153,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1310
1153
|
role_admin = self.find_role(self.auth_role_admin)
|
1311
1154
|
self.add_permission_to_role(role_admin, perm)
|
1312
1155
|
|
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
1156
|
def sync_roles(self) -> None:
|
1339
1157
|
"""
|
1340
1158
|
Initialize default and custom roles with related permissions.
|
@@ -1414,57 +1232,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1414
1232
|
if deleted_count:
|
1415
1233
|
self.log.info("Deleted %s faulty permissions", deleted_count)
|
1416
1234
|
|
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
1235
|
def perms_include_action(self, perms, action_name):
|
1452
1236
|
return any(perm.action and perm.action.name == action_name for perm in perms)
|
1453
1237
|
|
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
1238
|
def bulk_sync_roles(self, roles: Iterable[dict[str, Any]]) -> None:
|
1469
1239
|
"""Sync the provided roles and permissions."""
|
1470
1240
|
existing_roles = self._get_all_roles_with_permissions()
|
@@ -1483,15 +1253,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1483
1253
|
if perm not in role.permissions:
|
1484
1254
|
self.add_permission_to_role(role, perm)
|
1485
1255
|
|
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
1256
|
"""
|
1496
1257
|
-----------
|
1497
1258
|
Role entity
|
@@ -1575,7 +1336,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1575
1336
|
if fab_role:
|
1576
1337
|
_roles.add(fab_role)
|
1577
1338
|
else:
|
1578
|
-
log.warning(
|
1339
|
+
log.warning(
|
1340
|
+
"Can't find role specified in AUTH_ROLES_MAPPING: %s",
|
1341
|
+
fab_role_name,
|
1342
|
+
)
|
1579
1343
|
return _roles
|
1580
1344
|
|
1581
1345
|
def get_public_role(self):
|
@@ -1683,13 +1447,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1683
1447
|
log.error("Multiple results found for user with email %s", email)
|
1684
1448
|
return None
|
1685
1449
|
|
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
1450
|
def update_user(self, user: User) -> bool:
|
1694
1451
|
try:
|
1695
1452
|
self.get_session.merge(user)
|
@@ -1839,38 +1596,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
1839
1596
|
self.get_session.rollback()
|
1840
1597
|
return resource
|
1841
1598
|
|
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
1599
|
"""
|
1875
1600
|
---------------
|
1876
1601
|
Permission entity
|
@@ -2000,13 +1725,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2000
1725
|
log.error(const.LOGMSG_ERR_SEC_DEL_PERMROLE, e)
|
2001
1726
|
self.get_session.rollback()
|
2002
1727
|
|
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
1728
|
@staticmethod
|
2011
1729
|
def get_user_roles(user=None):
|
2012
1730
|
"""
|
@@ -2209,6 +1927,20 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2209
1927
|
log.error(e)
|
2210
1928
|
return None
|
2211
1929
|
|
1930
|
+
def check_password(self, username, password) -> bool:
|
1931
|
+
"""
|
1932
|
+
Check if the password is correct for the username.
|
1933
|
+
|
1934
|
+
:param username: the username
|
1935
|
+
:param password: the password
|
1936
|
+
"""
|
1937
|
+
user = self.find_user(username=username)
|
1938
|
+
if user is None:
|
1939
|
+
user = self.find_user(email=username)
|
1940
|
+
if user is None:
|
1941
|
+
return False
|
1942
|
+
return check_password_hash(user.password, password)
|
1943
|
+
|
2212
1944
|
def auth_user_db(self, username, password):
|
2213
1945
|
"""
|
2214
1946
|
Authenticate user, auth db style.
|
@@ -2240,32 +1972,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2240
1972
|
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
2241
1973
|
return None
|
2242
1974
|
|
2243
|
-
def oauth_user_info_getter(
|
2244
|
-
self,
|
2245
|
-
func: Callable[[AirflowSecurityManagerV2, str, dict[str, Any] | None], dict[str, Any]],
|
2246
|
-
):
|
2247
|
-
"""
|
2248
|
-
Get OAuth user info for all the providers.
|
2249
|
-
|
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.
|
2252
|
-
|
2253
|
-
Use it like this an example for GitHub ::
|
2254
|
-
|
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 {}
|
2261
|
-
"""
|
2262
|
-
|
2263
|
-
def wraps(provider: str, response: dict[str, Any] | None = None) -> dict[str, Any]:
|
2264
|
-
return func(self, provider, response)
|
2265
|
-
|
2266
|
-
self.oauth_user_info = wraps
|
2267
|
-
return wraps
|
2268
|
-
|
2269
1975
|
def get_oauth_user_info(self, provider: str, resp: dict[str, Any]) -> dict[str, Any]:
|
2270
1976
|
"""
|
2271
1977
|
There are different OAuth APIs with different ways to retrieve user info.
|
@@ -2387,183 +2093,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2387
2093
|
log.debug("Token Get: %s", token)
|
2388
2094
|
return token
|
2389
2095
|
|
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
2096
|
@staticmethod
|
2568
2097
|
def ldap_extract_list(ldap_dict: dict[str, list[bytes]], field_name: str) -> list[str]:
|
2569
2098
|
raw_list = ldap_dict.get(field_name, [])
|
@@ -2658,7 +2187,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2658
2187
|
|
2659
2188
|
# perform the LDAP search
|
2660
2189
|
log.debug(
|
2661
|
-
"LDAP search for %r with fields %s in scope %r",
|
2190
|
+
"LDAP search for %r with fields %s in scope %r",
|
2191
|
+
filter_str,
|
2192
|
+
request_fields,
|
2193
|
+
self.auth_ldap_search,
|
2662
2194
|
)
|
2663
2195
|
raw_search_result = con.search_s(
|
2664
2196
|
self.auth_ldap_search, ldap.SCOPE_SUBTREE, filter_str, request_fields
|
@@ -2721,77 +2253,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2721
2253
|
|
2722
2254
|
return list(user_role_objects)
|
2723
2255
|
|
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
2256
|
def _merge_perm(self, action_name: str, resource_name: str) -> None:
|
2796
2257
|
"""
|
2797
2258
|
Add the new (action, resource) to assoc_permission_role if it doesn't exist.
|
@@ -2831,7 +2292,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2831
2292
|
(action_name, resource_name): viewmodel
|
2832
2293
|
for action_name, resource_name, viewmodel in (
|
2833
2294
|
self.appbuilder.get_session.execute(
|
2834
|
-
select(
|
2295
|
+
select(
|
2296
|
+
self.action_model.name,
|
2297
|
+
self.resource_model.name,
|
2298
|
+
self.permission_model,
|
2299
|
+
)
|
2835
2300
|
.join(self.permission_model.action)
|
2836
2301
|
.join(self.permission_model.resource)
|
2837
2302
|
.where(~self.resource_model.name.like(f"{permissions.RESOURCE_DAG_PREFIX}%"))
|
@@ -2839,32 +2304,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
2839
2304
|
)
|
2840
2305
|
}
|
2841
2306
|
|
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
2307
|
@staticmethod
|
2869
2308
|
def _cli_safe_flash(text: str, level: str) -> None:
|
2870
2309
|
"""Show a flash in a web context or prints a message if not."""
|