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