apache-airflow-providers-fab 3.1.1rc1__py3-none-any.whl → 3.2.0rc1__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/__init__.py +1 -1
- airflow/providers/fab/auth_manager/api_endpoints/user_endpoint.py +3 -1
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/roles.py +13 -7
- airflow/providers/fab/auth_manager/api_fastapi/datamodels/users.py +68 -0
- airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml +485 -18
- airflow/providers/fab/auth_manager/api_fastapi/routes/login.py +2 -4
- airflow/providers/fab/auth_manager/api_fastapi/routes/users.py +133 -0
- airflow/providers/fab/auth_manager/api_fastapi/services/login.py +1 -2
- airflow/providers/fab/auth_manager/api_fastapi/services/users.py +219 -0
- airflow/providers/fab/auth_manager/cli_commands/db_command.py +2 -2
- airflow/providers/fab/auth_manager/cli_commands/user_command.py +3 -3
- airflow/providers/fab/auth_manager/fab_auth_manager.py +18 -51
- airflow/providers/fab/auth_manager/models/__init__.py +6 -6
- airflow/providers/fab/auth_manager/security_manager/override.py +90 -77
- airflow/providers/fab/auth_manager/views/user.py +12 -0
- airflow/providers/fab/cli/__init__.py +18 -0
- airflow/providers/fab/{auth_manager/cli_commands → cli}/definition.py +50 -2
- airflow/providers/fab/get_provider_info.py +8 -0
- airflow/providers/fab/www/app.py +2 -7
- airflow/providers/fab/www/extensions/init_appbuilder.py +3 -2
- airflow/providers/fab/www/package-lock.json +669 -531
- airflow/providers/fab/www/package.json +9 -9
- airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js → 743.8fb7d21632ed892227fe.js} +2 -2
- airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.js → airflowDefaultTheme.51e5d14856ee1ebc83ca.js} +1 -1
- airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.js → flash.865b6940c00b2a9041b3.js} +1 -1
- airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.js → loadingDots.07f5b9805847242736e1.js} +1 -1
- airflow/providers/fab/www/static/dist/main.8cffe40bcf7cca998f4e.js +2 -0
- airflow/providers/fab/www/static/dist/manifest.json +13 -13
- airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.js → materialIcons.4fe84ae36604d84dec78.js} +1 -1
- airflow/providers/fab/www/static/dist/moment.0ec3ee3fb60dc999b1fd.js +1 -0
- airflow/providers/fab/www/static/js/main.js +11 -0
- airflow/providers/fab/www/templates/airflow/main.html +1 -0
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/METADATA +10 -10
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/RECORD +47 -43
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/3rd-party-licenses/LICENSES-ui.txt +1 -1
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/NOTICE +1 -1
- airflow/providers/fab/www/static/dist/main.bc1f701c3d133e2a3bab.js +0 -2
- airflow/providers/fab/www/static/dist/moment.5b85b4f6be2fe9c405ac.js +0 -1
- /airflow/providers/fab/www/static/dist/{743.0c0bf201ae17e66a9a3f.js.LICENSE.txt → 743.8fb7d21632ed892227fe.js.LICENSE.txt} +0 -0
- /airflow/providers/fab/www/static/dist/{airflowDefaultTheme.ef6fc04c9b6920cd75c9.css → airflowDefaultTheme.51e5d14856ee1ebc83ca.css} +0 -0
- /airflow/providers/fab/www/static/dist/{flash.eaaf777ec1b3628cf7be.css → flash.865b6940c00b2a9041b3.css} +0 -0
- /airflow/providers/fab/www/static/dist/{loadingDots.76f4332c0a932c3dc08f.css → loadingDots.07f5b9805847242736e1.css} +0 -0
- /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.css → main.8cffe40bcf7cca998f4e.css} +0 -0
- /airflow/providers/fab/www/static/dist/{main.bc1f701c3d133e2a3bab.js.LICENSE.txt → main.8cffe40bcf7cca998f4e.js.LICENSE.txt} +0 -0
- /airflow/providers/fab/www/static/dist/{materialIcons.ad07a489b2f0fc1a96bf.css → materialIcons.4fe84ae36604d84dec78.css} +0 -0
- /airflow/providers/fab/www/static/dist/{runtime.254c277d91ce3ac79c64.js → runtime.45b36fb8335446865b53.js} +0 -0
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/entry_points.txt +0 -0
- {apache_airflow_providers_fab-3.1.1rc1.dist-info → apache_airflow_providers_fab-3.2.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -44,6 +44,7 @@ from flask_appbuilder.const import (
|
|
|
44
44
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|
45
45
|
from flask_appbuilder.security.api import SecurityApi
|
|
46
46
|
from flask_appbuilder.security.registerviews import (
|
|
47
|
+
BaseRegisterUser,
|
|
47
48
|
RegisterUserDBView,
|
|
48
49
|
RegisterUserOAuthView,
|
|
49
50
|
)
|
|
@@ -52,8 +53,10 @@ from flask_appbuilder.security.views import (
|
|
|
52
53
|
AuthLDAPView,
|
|
53
54
|
AuthOAuthView,
|
|
54
55
|
AuthRemoteUserView,
|
|
56
|
+
AuthView,
|
|
55
57
|
RegisterUserModelView,
|
|
56
58
|
UserGroupModelView,
|
|
59
|
+
UserModelView,
|
|
57
60
|
)
|
|
58
61
|
from flask_babel import lazy_gettext
|
|
59
62
|
from flask_jwt_extended import JWTManager
|
|
@@ -67,7 +70,6 @@ from sqlalchemy.orm import joinedload
|
|
|
67
70
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
68
71
|
|
|
69
72
|
from airflow.configuration import conf
|
|
70
|
-
from airflow.providers.common.compat.sdk import AirflowException
|
|
71
73
|
from airflow.providers.fab.auth_manager.models import (
|
|
72
74
|
Action,
|
|
73
75
|
Group,
|
|
@@ -103,6 +105,8 @@ from airflow.providers.fab.www.security_manager import AirflowSecurityManagerV2
|
|
|
103
105
|
from airflow.providers.fab.www.session import AirflowDatabaseSessionInterface
|
|
104
106
|
|
|
105
107
|
if TYPE_CHECKING:
|
|
108
|
+
from authlib.integrations.flask_client import OAuth
|
|
109
|
+
|
|
106
110
|
from airflow.providers.fab.www.security.permissions import (
|
|
107
111
|
RESOURCE_ASSET,
|
|
108
112
|
RESOURCE_ASSET_ALIAS,
|
|
@@ -148,6 +152,10 @@ log = logging.getLogger(__name__)
|
|
|
148
152
|
MAX_NUM_DATABASE_USER_SESSIONS = 50000
|
|
149
153
|
|
|
150
154
|
|
|
155
|
+
class FabException(Exception):
|
|
156
|
+
"""Custom exception for FAB security manager."""
|
|
157
|
+
|
|
158
|
+
|
|
151
159
|
class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
152
160
|
"""
|
|
153
161
|
This security manager overrides the default AirflowSecurityManager security manager.
|
|
@@ -159,11 +167,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
159
167
|
:param appbuilder: The appbuilder.
|
|
160
168
|
"""
|
|
161
169
|
|
|
162
|
-
auth_view = None
|
|
170
|
+
auth_view: AuthView | None = None
|
|
163
171
|
""" The obj instance for authentication view """
|
|
164
|
-
registeruser_view = None
|
|
172
|
+
registeruser_view: BaseRegisterUser | None = None
|
|
165
173
|
""" The obj instance for registering user view """
|
|
166
|
-
user_view = None
|
|
174
|
+
user_view: type[UserModelView] | None = None
|
|
167
175
|
""" The obj instance for user view """
|
|
168
176
|
|
|
169
177
|
""" Models """
|
|
@@ -207,9 +215,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
207
215
|
security_api = SecurityApi
|
|
208
216
|
""" Override if you want your own Security API login endpoint """
|
|
209
217
|
|
|
210
|
-
jwt_manager = None
|
|
218
|
+
jwt_manager: JWTManager | None = None
|
|
211
219
|
""" Flask-JWT-Extended """
|
|
212
|
-
oauth = None
|
|
220
|
+
oauth: OAuth | None = None
|
|
213
221
|
oauth_remotes: dict[str, Any]
|
|
214
222
|
""" Initialized (remote_app) providers dict {'provider_name', OBJ } """
|
|
215
223
|
|
|
@@ -304,6 +312,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
304
312
|
(permissions.ACTION_CAN_READ, permissions.RESOURCE_VARIABLE),
|
|
305
313
|
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_VARIABLE),
|
|
306
314
|
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_VARIABLE),
|
|
315
|
+
(permissions.ACTION_CAN_CREATE, permissions.RESOURCE_XCOM),
|
|
316
|
+
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_XCOM),
|
|
307
317
|
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_XCOM),
|
|
308
318
|
(permissions.ACTION_CAN_CREATE, RESOURCE_ASSET),
|
|
309
319
|
(permissions.ACTION_CAN_DELETE, RESOURCE_ASSET),
|
|
@@ -417,9 +427,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
417
427
|
log.warning("JWT token is not validated!")
|
|
418
428
|
return me
|
|
419
429
|
|
|
420
|
-
raise
|
|
430
|
+
raise FabException("OAuth signature verify failed")
|
|
421
431
|
|
|
422
|
-
def register_views(self):
|
|
432
|
+
def register_views(self) -> None:
|
|
423
433
|
"""Register FAB auth manager related views."""
|
|
424
434
|
if not current_app.config.get("FAB_ADD_SECURITY_VIEWS", True):
|
|
425
435
|
return
|
|
@@ -456,7 +466,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
456
466
|
|
|
457
467
|
# this needs to be done after the view is added, otherwise the blueprint
|
|
458
468
|
# is not initialized
|
|
459
|
-
if self.is_auth_limited:
|
|
469
|
+
if self.is_auth_limited and self.auth_view:
|
|
460
470
|
self.limiter.limit(self.auth_rate_limit, methods=["POST"])(self.auth_view.blueprint)
|
|
461
471
|
|
|
462
472
|
self.user_view = self.appbuilder.add_view(
|
|
@@ -479,14 +489,13 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
479
489
|
)
|
|
480
490
|
role_view.related_views = [self.user_view.__class__]
|
|
481
491
|
|
|
482
|
-
|
|
483
|
-
self.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
)
|
|
492
|
+
self.appbuilder.add_view(
|
|
493
|
+
self.userstatschartview,
|
|
494
|
+
"User's Statistics",
|
|
495
|
+
icon="fa-bar-chart-o",
|
|
496
|
+
label=lazy_gettext("User's Statistics"),
|
|
497
|
+
category="Security",
|
|
498
|
+
)
|
|
490
499
|
if self.auth_user_registration:
|
|
491
500
|
self.appbuilder.add_view(
|
|
492
501
|
self.registerusermodelview,
|
|
@@ -533,7 +542,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
533
542
|
lm.user_loader(self.load_user)
|
|
534
543
|
return lm
|
|
535
544
|
|
|
536
|
-
def create_jwt_manager(self):
|
|
545
|
+
def create_jwt_manager(self) -> None:
|
|
537
546
|
"""Create the JWT manager."""
|
|
538
547
|
jwt_manager = JWTManager()
|
|
539
548
|
jwt_manager.init_app(current_app)
|
|
@@ -549,6 +558,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
549
558
|
:param password: the clear text password to reset and save hashed on the db
|
|
550
559
|
"""
|
|
551
560
|
user = self.get_user_by_id(userid)
|
|
561
|
+
if not user:
|
|
562
|
+
return False
|
|
552
563
|
user.password = generate_password_hash(password)
|
|
553
564
|
self.reset_user_sessions(user)
|
|
554
565
|
return self.update_user(user)
|
|
@@ -590,16 +601,17 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
590
601
|
"warning",
|
|
591
602
|
)
|
|
592
603
|
|
|
593
|
-
def load_user_jwt(self, _jwt_header, jwt_data):
|
|
604
|
+
def load_user_jwt(self, _jwt_header, jwt_data) -> User | None:
|
|
594
605
|
identity = jwt_data["sub"]
|
|
595
606
|
user = self.load_user(identity)
|
|
596
607
|
if user and user.is_active:
|
|
597
608
|
# Set flask g.user to JWT user, we can't do it on before request
|
|
598
609
|
g.user = user
|
|
599
610
|
return user
|
|
611
|
+
return None
|
|
600
612
|
|
|
601
613
|
@property
|
|
602
|
-
def auth_type(self):
|
|
614
|
+
def auth_type(self) -> int:
|
|
603
615
|
"""Get the auth type."""
|
|
604
616
|
return current_app.config["AUTH_TYPE"]
|
|
605
617
|
|
|
@@ -736,11 +748,6 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
736
748
|
def auth_remote_user_env_var(self) -> str:
|
|
737
749
|
return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
|
|
738
750
|
|
|
739
|
-
@property
|
|
740
|
-
def auth_username_ci(self):
|
|
741
|
-
"""Get the auth username for CI."""
|
|
742
|
-
return current_app.config.get("AUTH_USERNAME_CI", True)
|
|
743
|
-
|
|
744
751
|
@property
|
|
745
752
|
def auth_user_registration(self):
|
|
746
753
|
"""Will user self registration be allowed."""
|
|
@@ -762,15 +769,15 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
762
769
|
return current_app.config["AUTH_ROLE_ADMIN"]
|
|
763
770
|
|
|
764
771
|
@property
|
|
765
|
-
def oauth_whitelists(self):
|
|
772
|
+
def oauth_whitelists(self) -> dict[str, list]:
|
|
766
773
|
return self.oauth_allow_list
|
|
767
774
|
|
|
768
775
|
@staticmethod
|
|
769
|
-
def create_builtin_roles():
|
|
776
|
+
def create_builtin_roles() -> dict:
|
|
770
777
|
return current_app.config.get("FAB_ROLES", {})
|
|
771
778
|
|
|
772
779
|
@property
|
|
773
|
-
def builtin_roles(self):
|
|
780
|
+
def builtin_roles(self) -> dict:
|
|
774
781
|
"""Get the builtin roles."""
|
|
775
782
|
return self._builtin_roles
|
|
776
783
|
|
|
@@ -779,11 +786,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
779
786
|
return current_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
|
|
780
787
|
|
|
781
788
|
@property
|
|
782
|
-
def auth_type_provider_name(self):
|
|
789
|
+
def auth_type_provider_name(self) -> str | None:
|
|
783
790
|
provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
|
|
784
791
|
return provider_to_auth_type.get(self.auth_type)
|
|
785
792
|
|
|
786
|
-
def _init_config(self):
|
|
793
|
+
def _init_config(self) -> None:
|
|
787
794
|
"""
|
|
788
795
|
Initialize config.
|
|
789
796
|
|
|
@@ -853,7 +860,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
853
860
|
current_app.config.setdefault("AUTH_RATE_LIMITED", True)
|
|
854
861
|
current_app.config.setdefault("AUTH_RATE_LIMIT", "5 per 40 second")
|
|
855
862
|
|
|
856
|
-
def _init_auth(self):
|
|
863
|
+
def _init_auth(self) -> None:
|
|
857
864
|
"""
|
|
858
865
|
Initialize authentication configuration.
|
|
859
866
|
|
|
@@ -876,7 +883,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
876
883
|
self.oauth_allow_list[provider_name] = provider["whitelist"]
|
|
877
884
|
self.oauth_remotes[provider_name] = obj_provider
|
|
878
885
|
|
|
879
|
-
def _init_data_model(self):
|
|
886
|
+
def _init_data_model(self) -> None:
|
|
880
887
|
user_data_model = SQLAInterface(self.user_model)
|
|
881
888
|
if self.auth_type == const.AUTH_DB:
|
|
882
889
|
self.userdbmodelview.datamodel = user_data_model
|
|
@@ -887,8 +894,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
887
894
|
elif self.auth_type == const.AUTH_REMOTE_USER:
|
|
888
895
|
self.userremoteusermodelview.datamodel = user_data_model
|
|
889
896
|
|
|
890
|
-
|
|
891
|
-
self.userstatschartview.datamodel = user_data_model
|
|
897
|
+
self.userstatschartview.datamodel = user_data_model
|
|
892
898
|
if self.auth_user_registration:
|
|
893
899
|
self.registerusermodelview.datamodel = SQLAInterface(self.registeruser_model)
|
|
894
900
|
|
|
@@ -898,7 +904,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
898
904
|
self.resourcemodelview.datamodel = SQLAInterface(self.resource_model)
|
|
899
905
|
self.permissionmodelview.datamodel = SQLAInterface(self.permission_model)
|
|
900
906
|
|
|
901
|
-
def create_db(self):
|
|
907
|
+
def create_db(self) -> None:
|
|
902
908
|
"""
|
|
903
909
|
Create the database.
|
|
904
910
|
|
|
@@ -1051,7 +1057,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1051
1057
|
for rolename, resource_actions_raw in access_control.items():
|
|
1052
1058
|
role = self.find_role(rolename)
|
|
1053
1059
|
if not role:
|
|
1054
|
-
raise
|
|
1060
|
+
raise FabException(
|
|
1055
1061
|
f"The access_control mapping for DAG '{dag_id}' includes a role named "
|
|
1056
1062
|
f"'{rolename}', but that role does not exist"
|
|
1057
1063
|
)
|
|
@@ -1065,7 +1071,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1065
1071
|
|
|
1066
1072
|
for resource_name, actions in resource_actions.items():
|
|
1067
1073
|
if resource_name not in self.RESOURCE_DETAILS_MAP:
|
|
1068
|
-
raise
|
|
1074
|
+
raise FabException(
|
|
1069
1075
|
f"The access_control map for DAG '{dag_id}' includes the following invalid "
|
|
1070
1076
|
f"resource name: '{resource_name}'; "
|
|
1071
1077
|
f"The set of valid resource names is: {self.RESOURCE_DETAILS_MAP.keys()}"
|
|
@@ -1077,7 +1083,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1077
1083
|
invalid_actions = set(actions) - self.RESOURCE_DETAILS_MAP[resource_name]["actions"]
|
|
1078
1084
|
|
|
1079
1085
|
if invalid_actions:
|
|
1080
|
-
raise
|
|
1086
|
+
raise FabException(
|
|
1081
1087
|
f"The access_control map for DAG '{dag_resource_name}' includes "
|
|
1082
1088
|
f"the following invalid permissions: {invalid_actions}; "
|
|
1083
1089
|
f"The set of valid permissions is: {self.RESOURCE_DETAILS_MAP[resource_name]['actions']}"
|
|
@@ -1088,7 +1094,9 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1088
1094
|
if dag_perm:
|
|
1089
1095
|
self.add_permission_to_role(role, dag_perm)
|
|
1090
1096
|
|
|
1091
|
-
def add_permissions_view(
|
|
1097
|
+
def add_permissions_view(
|
|
1098
|
+
self, base_action_names, resource_name
|
|
1099
|
+
) -> None: # Keep name for compatibility with FAB.
|
|
1092
1100
|
"""
|
|
1093
1101
|
Add an action on a resource to the backend.
|
|
1094
1102
|
|
|
@@ -1113,6 +1121,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1113
1121
|
else:
|
|
1114
1122
|
# Permissions on this view exist but....
|
|
1115
1123
|
admin_role = self.find_role(self.auth_role_admin)
|
|
1124
|
+
if not admin_role:
|
|
1125
|
+
admin_role = self.add_role(self.auth_role_admin)
|
|
1116
1126
|
for action_name in base_action_names:
|
|
1117
1127
|
# Check if base view permissions exist
|
|
1118
1128
|
if not self.perms_include_action(perms, action_name):
|
|
@@ -1136,7 +1146,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1136
1146
|
# Role Admin must have all permissions
|
|
1137
1147
|
self.add_permission_to_role(admin_role, perm)
|
|
1138
1148
|
|
|
1139
|
-
def add_permissions_menu(self, resource_name):
|
|
1149
|
+
def add_permissions_menu(self, resource_name) -> None:
|
|
1140
1150
|
"""
|
|
1141
1151
|
Add menu_access to resource on permission_resource.
|
|
1142
1152
|
|
|
@@ -1149,6 +1159,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1149
1159
|
perm = self.create_permission("menu_access", resource_name)
|
|
1150
1160
|
if self.auth_role_admin not in self.builtin_roles:
|
|
1151
1161
|
role_admin = self.find_role(self.auth_role_admin)
|
|
1162
|
+
if not role_admin:
|
|
1163
|
+
role_admin = self.add_role(self.auth_role_admin)
|
|
1152
1164
|
self.add_permission_to_role(role_admin, perm)
|
|
1153
1165
|
|
|
1154
1166
|
def sync_roles(self) -> None:
|
|
@@ -1203,6 +1215,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1203
1215
|
perms = [p for p in perms if p.action and p.resource]
|
|
1204
1216
|
|
|
1205
1217
|
admin = self.find_role("Admin")
|
|
1218
|
+
if not admin:
|
|
1219
|
+
admin = self.add_role("Admin")
|
|
1206
1220
|
admin.permissions = list(set(admin.permissions) | set(perms))
|
|
1207
1221
|
|
|
1208
1222
|
self.session.commit()
|
|
@@ -1230,7 +1244,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1230
1244
|
if deleted_count:
|
|
1231
1245
|
self.log.info("Deleted %s faulty permissions", deleted_count)
|
|
1232
1246
|
|
|
1233
|
-
def perms_include_action(self, perms, action_name):
|
|
1247
|
+
def perms_include_action(self, perms, action_name) -> bool:
|
|
1234
1248
|
return any(perm.action and perm.action.name == action_name for perm in perms)
|
|
1235
1249
|
|
|
1236
1250
|
def bulk_sync_roles(self, roles: Iterable[dict[str, Any]]) -> None:
|
|
@@ -1287,9 +1301,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1287
1301
|
except Exception as e:
|
|
1288
1302
|
log.error(const.LOGMSG_ERR_SEC_ADD_ROLE, e)
|
|
1289
1303
|
self.session.rollback()
|
|
1304
|
+
raise FabException(const.LOGMSG_ERR_SEC_ADD_ROLE) from e
|
|
1290
1305
|
return role
|
|
1291
1306
|
|
|
1292
|
-
def find_role(self, name):
|
|
1307
|
+
def find_role(self, name) -> Role | None:
|
|
1293
1308
|
"""
|
|
1294
1309
|
Find a role in the database.
|
|
1295
1310
|
|
|
@@ -1297,7 +1312,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1297
1312
|
"""
|
|
1298
1313
|
return self.session.scalars(select(self.role_model).filter_by(name=name)).unique().one_or_none()
|
|
1299
1314
|
|
|
1300
|
-
def get_all_roles(self):
|
|
1315
|
+
def get_all_roles(self) -> list[Role]:
|
|
1301
1316
|
return self.session.scalars(select(self.role_model)).unique().all()
|
|
1302
1317
|
|
|
1303
1318
|
def delete_role(self, role_name: str) -> None:
|
|
@@ -1312,7 +1327,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1312
1327
|
self.session.execute(delete(Role).where(Role.name == role_name))
|
|
1313
1328
|
self.session.commit()
|
|
1314
1329
|
else:
|
|
1315
|
-
raise
|
|
1330
|
+
raise FabException(f"Role named '{role_name}' does not exist")
|
|
1316
1331
|
|
|
1317
1332
|
def get_roles_from_keys(self, role_keys: list[str]) -> set[Role]:
|
|
1318
1333
|
"""
|
|
@@ -1339,7 +1354,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1339
1354
|
)
|
|
1340
1355
|
return _roles
|
|
1341
1356
|
|
|
1342
|
-
def get_public_role(self):
|
|
1357
|
+
def get_public_role(self) -> Role | None:
|
|
1343
1358
|
return (
|
|
1344
1359
|
self.session.scalars(select(self.role_model).filter_by(name=self.auth_role_public))
|
|
1345
1360
|
.unique()
|
|
@@ -1362,7 +1377,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1362
1377
|
password: str = "",
|
|
1363
1378
|
hashed_password: str = "",
|
|
1364
1379
|
groups: list[Group] | None = None,
|
|
1365
|
-
):
|
|
1380
|
+
) -> User:
|
|
1366
1381
|
"""Create a user."""
|
|
1367
1382
|
roles: list[Role] = []
|
|
1368
1383
|
if role:
|
|
@@ -1389,22 +1404,24 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1389
1404
|
except Exception as e:
|
|
1390
1405
|
log.error(const.LOGMSG_ERR_SEC_ADD_USER, e)
|
|
1391
1406
|
self.session.rollback()
|
|
1392
|
-
|
|
1407
|
+
raise FabException(const.LOGMSG_ERR_SEC_ADD_USER) from e
|
|
1393
1408
|
|
|
1394
|
-
def load_user(self, pk: int) ->
|
|
1409
|
+
def load_user(self, pk: int) -> User | None:
|
|
1395
1410
|
user = self.get_user_by_id(int(pk))
|
|
1396
1411
|
if user and user.is_active:
|
|
1397
1412
|
return user
|
|
1398
1413
|
return None
|
|
1399
1414
|
|
|
1400
|
-
def get_user_by_id(self, pk):
|
|
1415
|
+
def get_user_by_id(self, pk) -> User | None:
|
|
1401
1416
|
return self.session.get(self.user_model, pk)
|
|
1402
1417
|
|
|
1403
|
-
def count_users(self):
|
|
1418
|
+
def count_users(self) -> int:
|
|
1404
1419
|
"""Return the number of users in the database."""
|
|
1405
1420
|
return self.session.scalar(select(func.count(self.user_model.id)))
|
|
1406
1421
|
|
|
1407
|
-
def add_register_user(
|
|
1422
|
+
def add_register_user(
|
|
1423
|
+
self, username, first_name, last_name, email, password="", hashed_password=""
|
|
1424
|
+
) -> RegisterUser | None:
|
|
1408
1425
|
"""
|
|
1409
1426
|
Add a registration request for the user.
|
|
1410
1427
|
|
|
@@ -1429,16 +1446,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1429
1446
|
self.session.rollback()
|
|
1430
1447
|
return None
|
|
1431
1448
|
|
|
1432
|
-
def find_user(self, username=None, email=None):
|
|
1449
|
+
def find_user(self, username=None, email=None) -> User | None:
|
|
1433
1450
|
"""Find user by username or email."""
|
|
1434
1451
|
if username:
|
|
1435
1452
|
try:
|
|
1436
|
-
if self.auth_username_ci:
|
|
1437
|
-
return self.session.scalars(
|
|
1438
|
-
select(self.user_model).where(
|
|
1439
|
-
func.lower(self.user_model.username) == func.lower(username)
|
|
1440
|
-
)
|
|
1441
|
-
).one_or_none()
|
|
1442
1453
|
return self.session.scalars(
|
|
1443
1454
|
select(self.user_model).where(
|
|
1444
1455
|
func.lower(self.user_model.username) == func.lower(username)
|
|
@@ -1446,13 +1457,12 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1446
1457
|
).one_or_none()
|
|
1447
1458
|
except MultipleResultsFound:
|
|
1448
1459
|
log.error("Multiple results found for user %s", username)
|
|
1449
|
-
return None
|
|
1450
1460
|
elif email:
|
|
1451
1461
|
try:
|
|
1452
1462
|
return self.session.scalars(select(self.user_model).filter_by(email=email)).one_or_none()
|
|
1453
1463
|
except MultipleResultsFound:
|
|
1454
1464
|
log.error("Multiple results found for user with email %s", email)
|
|
1455
|
-
|
|
1465
|
+
return None
|
|
1456
1466
|
|
|
1457
1467
|
def update_user(self, user: User) -> bool:
|
|
1458
1468
|
try:
|
|
@@ -1465,7 +1475,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1465
1475
|
return False
|
|
1466
1476
|
return True
|
|
1467
1477
|
|
|
1468
|
-
def del_register_user(self, register_user):
|
|
1478
|
+
def del_register_user(self, register_user) -> bool:
|
|
1469
1479
|
"""
|
|
1470
1480
|
Delete registration object from database.
|
|
1471
1481
|
|
|
@@ -1480,10 +1490,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1480
1490
|
self.session.rollback()
|
|
1481
1491
|
return False
|
|
1482
1492
|
|
|
1483
|
-
def get_all_users(self):
|
|
1493
|
+
def get_all_users(self) -> list[User]:
|
|
1484
1494
|
return self.session.scalars(select(self.user_model)).all()
|
|
1485
1495
|
|
|
1486
|
-
def update_user_auth_stat(self, user, success=True):
|
|
1496
|
+
def update_user_auth_stat(self, user, success=True) -> None:
|
|
1487
1497
|
"""
|
|
1488
1498
|
Update user authentication stats.
|
|
1489
1499
|
|
|
@@ -1523,7 +1533,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1523
1533
|
"""
|
|
1524
1534
|
return self.session.scalars(select(self.action_model).filter_by(name=name)).one_or_none()
|
|
1525
1535
|
|
|
1526
|
-
def create_action(self, name):
|
|
1536
|
+
def create_action(self, name) -> Action:
|
|
1527
1537
|
"""
|
|
1528
1538
|
Add an action to the backend, model action.
|
|
1529
1539
|
|
|
@@ -1582,7 +1592,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1582
1592
|
"""
|
|
1583
1593
|
return self.session.scalars(select(self.resource_model).filter_by(name=name)).one_or_none()
|
|
1584
1594
|
|
|
1585
|
-
def create_resource(self, name) -> Resource
|
|
1595
|
+
def create_resource(self, name) -> Resource:
|
|
1586
1596
|
"""
|
|
1587
1597
|
Create a resource with the given name.
|
|
1588
1598
|
|
|
@@ -1599,6 +1609,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1599
1609
|
except Exception as e:
|
|
1600
1610
|
log.error(const.LOGMSG_ERR_SEC_ADD_VIEWMENU, e)
|
|
1601
1611
|
self.session.rollback()
|
|
1612
|
+
raise FabException(const.LOGMSG_ERR_SEC_ADD_VIEWMENU) from e
|
|
1602
1613
|
return resource
|
|
1603
1614
|
|
|
1604
1615
|
"""
|
|
@@ -1737,7 +1748,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1737
1748
|
self.session.rollback()
|
|
1738
1749
|
|
|
1739
1750
|
@staticmethod
|
|
1740
|
-
def get_user_roles(user=None):
|
|
1751
|
+
def get_user_roles(user=None) -> list[Role]:
|
|
1741
1752
|
"""
|
|
1742
1753
|
Get all the roles associated with the user.
|
|
1743
1754
|
|
|
@@ -1754,7 +1765,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1754
1765
|
--------------------
|
|
1755
1766
|
"""
|
|
1756
1767
|
|
|
1757
|
-
def auth_user_ldap(self, username, password, rotate_session_id=True):
|
|
1768
|
+
def auth_user_ldap(self, username, password, rotate_session_id=True) -> User | None:
|
|
1758
1769
|
"""
|
|
1759
1770
|
Authenticate user with LDAP.
|
|
1760
1771
|
|
|
@@ -1947,11 +1958,11 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1947
1958
|
user = self.find_user(username=username)
|
|
1948
1959
|
if user is None:
|
|
1949
1960
|
user = self.find_user(email=username)
|
|
1950
|
-
if user is None:
|
|
1961
|
+
if user is None or user.password is None:
|
|
1951
1962
|
return False
|
|
1952
1963
|
return check_password_hash(user.password, password)
|
|
1953
1964
|
|
|
1954
|
-
def auth_user_db(self, username, password, rotate_session_id=True):
|
|
1965
|
+
def auth_user_db(self, username, password, rotate_session_id=True) -> User | None:
|
|
1955
1966
|
"""
|
|
1956
1967
|
Authenticate user, auth db style.
|
|
1957
1968
|
|
|
@@ -1975,6 +1986,8 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1975
1986
|
)
|
|
1976
1987
|
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
1977
1988
|
return None
|
|
1989
|
+
if user.password is None:
|
|
1990
|
+
return None
|
|
1978
1991
|
if check_password_hash(user.password, password):
|
|
1979
1992
|
if rotate_session_id:
|
|
1980
1993
|
self._rotate_session_id()
|
|
@@ -1984,7 +1997,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
1984
1997
|
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
1985
1998
|
return None
|
|
1986
1999
|
|
|
1987
|
-
def set_oauth_session(self, provider, oauth_response):
|
|
2000
|
+
def set_oauth_session(self, provider, oauth_response) -> None:
|
|
1988
2001
|
"""Set the current session with OAuth user secrets."""
|
|
1989
2002
|
# Get this provider key names for token_key and token_secret
|
|
1990
2003
|
token_key = self.get_oauth_token_key_name(provider)
|
|
@@ -2018,7 +2031,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2018
2031
|
if _provider["name"] == provider:
|
|
2019
2032
|
return _provider.get("token_secret", "oauth_token_secret")
|
|
2020
2033
|
|
|
2021
|
-
def auth_user_oauth(self, userinfo, rotate_session_id=True):
|
|
2034
|
+
def auth_user_oauth(self, userinfo, rotate_session_id=True) -> User | None:
|
|
2022
2035
|
"""
|
|
2023
2036
|
Authenticate user with OAuth.
|
|
2024
2037
|
|
|
@@ -2217,7 +2230,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2217
2230
|
# decode - if empty string, default to fallback, otherwise take first element
|
|
2218
2231
|
return raw_value[0].decode("utf-8") or fallback
|
|
2219
2232
|
|
|
2220
|
-
def auth_user_remote_user(self, username):
|
|
2233
|
+
def auth_user_remote_user(self, username) -> User | None:
|
|
2221
2234
|
"""
|
|
2222
2235
|
REMOTE_USER user Authentication.
|
|
2223
2236
|
|
|
@@ -2253,7 +2266,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2253
2266
|
---------------
|
|
2254
2267
|
"""
|
|
2255
2268
|
|
|
2256
|
-
def _rotate_session_id(self):
|
|
2269
|
+
def _rotate_session_id(self) -> None:
|
|
2257
2270
|
"""
|
|
2258
2271
|
Rotate the session ID.
|
|
2259
2272
|
|
|
@@ -2261,7 +2274,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2261
2274
|
database session backend.
|
|
2262
2275
|
"""
|
|
2263
2276
|
if conf.get("fab", "SESSION_BACKEND") == "database":
|
|
2264
|
-
session.sid = str(uuid.uuid4())
|
|
2277
|
+
session.sid = str(uuid.uuid4()) # type: ignore
|
|
2265
2278
|
|
|
2266
2279
|
def _get_microsoft_jwks(self) -> list[dict[str, Any]]:
|
|
2267
2280
|
import requests
|
|
@@ -2273,7 +2286,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2273
2286
|
if verify_signature:
|
|
2274
2287
|
from authlib.jose import JsonWebKey, jwt as authlib_jwt
|
|
2275
2288
|
|
|
2276
|
-
keyset = JsonWebKey.import_key_set(self._get_microsoft_jwks())
|
|
2289
|
+
keyset = JsonWebKey.import_key_set(self._get_microsoft_jwks()) # type: ignore
|
|
2277
2290
|
claims = authlib_jwt.decode(id_token, keyset)
|
|
2278
2291
|
claims.validate()
|
|
2279
2292
|
return claims
|
|
@@ -2415,7 +2428,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2415
2428
|
except ldap.INVALID_CREDENTIALS:
|
|
2416
2429
|
return False
|
|
2417
2430
|
|
|
2418
|
-
def _ldap_calculate_user_roles(self, user_attributes: dict[str, list[bytes]]) -> list[
|
|
2431
|
+
def _ldap_calculate_user_roles(self, user_attributes: dict[str, list[bytes]]) -> list[Role]:
|
|
2419
2432
|
user_role_objects = set()
|
|
2420
2433
|
|
|
2421
2434
|
# apply AUTH_ROLES_MAPPING
|
|
@@ -2495,7 +2508,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
|
|
|
2495
2508
|
else:
|
|
2496
2509
|
getattr(log, level)(text.replace("<br>", "\n").replace("<b>", "*").replace("</b>", "*"))
|
|
2497
2510
|
|
|
2498
|
-
def _oauth_calculate_user_roles(self, userinfo) -> list[
|
|
2511
|
+
def _oauth_calculate_user_roles(self, userinfo) -> list[Role]:
|
|
2499
2512
|
user_role_objects = set()
|
|
2500
2513
|
|
|
2501
2514
|
# apply AUTH_ROLES_MAPPING
|
|
@@ -26,6 +26,7 @@ from flask_appbuilder.security.views import (
|
|
|
26
26
|
UserOAuthModelView,
|
|
27
27
|
UserRemoteUserModelView,
|
|
28
28
|
)
|
|
29
|
+
from wtforms.validators import DataRequired
|
|
29
30
|
|
|
30
31
|
from airflow.providers.fab.www.security import permissions
|
|
31
32
|
|
|
@@ -186,6 +187,17 @@ class CustomUserDBModelView(MultiResourceUserMixin, UserDBModelView):
|
|
|
186
187
|
"conf_password",
|
|
187
188
|
]
|
|
188
189
|
|
|
190
|
+
edit_columns = [
|
|
191
|
+
"first_name",
|
|
192
|
+
"last_name",
|
|
193
|
+
"username",
|
|
194
|
+
"active",
|
|
195
|
+
"email",
|
|
196
|
+
"roles",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
validators_columns = {"roles": [DataRequired()]}
|
|
200
|
+
|
|
189
201
|
base_permissions = [
|
|
190
202
|
permissions.ACTION_CAN_CREATE,
|
|
191
203
|
permissions.ACTION_CAN_READ,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
from __future__ import annotations
|