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