flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2rc1__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.
- flask_appbuilder/__init__.py +2 -3
- flask_appbuilder/_compat.py +0 -1
- flask_appbuilder/actions.py +14 -14
- flask_appbuilder/api/__init__.py +741 -527
- flask_appbuilder/api/convert.py +104 -98
- flask_appbuilder/api/manager.py +14 -8
- flask_appbuilder/api/schemas.py +12 -1
- flask_appbuilder/babel/manager.py +12 -16
- flask_appbuilder/base.py +353 -280
- flask_appbuilder/basemanager.py +1 -1
- flask_appbuilder/baseviews.py +241 -164
- flask_appbuilder/charts/jsontools.py +10 -10
- flask_appbuilder/charts/views.py +56 -60
- flask_appbuilder/cli.py +115 -70
- flask_appbuilder/const.py +52 -52
- flask_appbuilder/exceptions.py +67 -5
- flask_appbuilder/fields.py +32 -23
- flask_appbuilder/fieldwidgets.py +34 -27
- flask_appbuilder/filemanager.py +33 -45
- flask_appbuilder/filters.py +11 -13
- flask_appbuilder/forms.py +31 -35
- flask_appbuilder/hooks.py +90 -0
- flask_appbuilder/menu.py +35 -10
- flask_appbuilder/models/base.py +47 -57
- flask_appbuilder/models/decorators.py +13 -13
- flask_appbuilder/models/filters.py +42 -38
- flask_appbuilder/models/generic/__init__.py +29 -29
- flask_appbuilder/models/generic/filters.py +11 -3
- flask_appbuilder/models/generic/interface.py +1 -3
- flask_appbuilder/models/group.py +37 -39
- flask_appbuilder/models/mixins.py +22 -18
- flask_appbuilder/models/sqla/__init__.py +19 -72
- flask_appbuilder/models/sqla/base.py +24 -0
- flask_appbuilder/models/sqla/base_legacy.py +132 -0
- flask_appbuilder/models/sqla/filters.py +132 -19
- flask_appbuilder/models/sqla/interface.py +390 -276
- flask_appbuilder/security/api.py +31 -35
- flask_appbuilder/security/decorators.py +181 -83
- flask_appbuilder/security/forms.py +20 -31
- flask_appbuilder/security/manager.py +715 -489
- flask_appbuilder/security/registerviews.py +29 -112
- flask_appbuilder/security/schemas.py +43 -0
- flask_appbuilder/security/sqla/apis/__init__.py +8 -0
- flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/group/api.py +227 -0
- flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
- flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
- flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/role/api.py +306 -0
- flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
- flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/user/api.py +292 -0
- flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
- flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
- flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
- flask_appbuilder/security/sqla/manager.py +421 -203
- flask_appbuilder/security/sqla/models.py +192 -57
- flask_appbuilder/security/utils.py +9 -0
- flask_appbuilder/security/views.py +232 -229
- flask_appbuilder/static/.DS_Store +0 -0
- flask_appbuilder/static/appbuilder/css/ab.css +20 -12
- flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
- flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
- flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
- flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
- flask_appbuilder/static/appbuilder/js/ab.js +33 -23
- flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
- flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
- flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
- flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
- flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
- flask_appbuilder/templates/appbuilder/baselib.html +9 -3
- flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
- flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
- flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
- flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
- flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
- flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
- flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
- flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
- flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
- flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
- flask_appbuilder/templates/appbuilder/init.html +37 -43
- flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
- flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
- flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
- flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
- flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
- flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
- flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
- flask_appbuilder/upload.py +20 -22
- flask_appbuilder/urltools.py +39 -19
- flask_appbuilder/utils/base.py +76 -0
- flask_appbuilder/utils/legacy.py +33 -0
- flask_appbuilder/utils/limit.py +20 -0
- flask_appbuilder/validators.py +73 -14
- flask_appbuilder/views.py +75 -424
- flask_appbuilder/widgets.py +50 -51
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
- flask_appbuilder/console.py +0 -426
- flask_appbuilder/models/mongoengine/__init__.py +0 -0
- flask_appbuilder/models/mongoengine/fields.py +0 -65
- flask_appbuilder/models/mongoengine/filters.py +0 -145
- flask_appbuilder/models/mongoengine/interface.py +0 -328
- flask_appbuilder/security/mongoengine/__init__.py +0 -0
- flask_appbuilder/security/mongoengine/manager.py +0 -402
- flask_appbuilder/security/mongoengine/models.py +0 -120
- flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
- flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
- flask_appbuilder/static/appbuilder/img/aol.png +0 -0
- flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
- flask_appbuilder/static/appbuilder/img/google.png +0 -0
- flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
- flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
- flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
- flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
- flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
- flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
- flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
- flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
- flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
- flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
- flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
- flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
- flask_appbuilder/tests/__init__.py +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
- flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
- flask_appbuilder/tests/_test_auth_oauth.py +0 -419
- flask_appbuilder/tests/_test_ldapsearch.py +0 -135
- flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
- flask_appbuilder/tests/app.db +0 -0
- flask_appbuilder/tests/base.py +0 -90
- flask_appbuilder/tests/config_api.py +0 -21
- flask_appbuilder/tests/const.py +0 -9
- flask_appbuilder/tests/mongoengine/__init__.py +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/models.py +0 -41
- flask_appbuilder/tests/sqla/__init__.py +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/models.py +0 -340
- flask_appbuilder/tests/test_0_fixture.py +0 -39
- flask_appbuilder/tests/test_api.py +0 -2790
- flask_appbuilder/tests/test_fab_cli.py +0 -72
- flask_appbuilder/tests/test_menu.py +0 -122
- flask_appbuilder/tests/test_mongoengine.py +0 -572
- flask_appbuilder/tests/test_mvc.py +0 -1710
- flask_appbuilder/tests/test_sqlalchemy.py +0 -24
- flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import datetime
|
|
3
|
-
import
|
|
4
|
+
import importlib
|
|
4
5
|
import logging
|
|
5
6
|
import re
|
|
6
|
-
from typing import Dict, List, Set
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
7
8
|
|
|
8
|
-
from flask import g, session, url_for
|
|
9
|
+
from flask import current_app, Flask, g, session, url_for
|
|
10
|
+
from flask_appbuilder.exceptions import InvalidLoginAttempt, OAuthProviderUnknown
|
|
9
11
|
from flask_babel import lazy_gettext as _
|
|
10
12
|
from flask_jwt_extended import current_user as current_user_jwt
|
|
11
13
|
from flask_jwt_extended import JWTManager
|
|
14
|
+
from flask_limiter import Limiter
|
|
15
|
+
from flask_limiter.util import get_remote_address
|
|
12
16
|
from flask_login import current_user, LoginManager
|
|
17
|
+
import jwt
|
|
18
|
+
from packaging.version import Version
|
|
13
19
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
14
20
|
|
|
15
21
|
from .api import SecurityApi
|
|
16
22
|
from .registerviews import (
|
|
17
23
|
RegisterUserDBView,
|
|
18
24
|
RegisterUserOAuthView,
|
|
19
|
-
RegisterUserOIDView,
|
|
20
25
|
)
|
|
21
26
|
from .views import (
|
|
22
27
|
AuthDBView,
|
|
23
28
|
AuthLDAPView,
|
|
24
29
|
AuthOAuthView,
|
|
25
|
-
AuthOIDView,
|
|
26
30
|
AuthRemoteUserView,
|
|
27
31
|
PermissionModelView,
|
|
28
32
|
PermissionViewModelView,
|
|
@@ -31,10 +35,10 @@ from .views import (
|
|
|
31
35
|
ResetPasswordView,
|
|
32
36
|
RoleModelView,
|
|
33
37
|
UserDBModelView,
|
|
38
|
+
UserGroupModelView,
|
|
34
39
|
UserInfoEditView,
|
|
35
40
|
UserLDAPModelView,
|
|
36
41
|
UserOAuthModelView,
|
|
37
|
-
UserOIDModelView,
|
|
38
42
|
UserRemoteUserModelView,
|
|
39
43
|
UserStatsChartView,
|
|
40
44
|
ViewMenuModelView,
|
|
@@ -44,7 +48,6 @@ from ..const import (
|
|
|
44
48
|
AUTH_DB,
|
|
45
49
|
AUTH_LDAP,
|
|
46
50
|
AUTH_OAUTH,
|
|
47
|
-
AUTH_OID,
|
|
48
51
|
AUTH_REMOTE_USER,
|
|
49
52
|
LOGMSG_ERR_SEC_ADD_REGISTER_USER,
|
|
50
53
|
LOGMSG_ERR_SEC_AUTH_LDAP,
|
|
@@ -52,6 +55,7 @@ from ..const import (
|
|
|
52
55
|
LOGMSG_WAR_SEC_LOGIN_FAILED,
|
|
53
56
|
LOGMSG_WAR_SEC_NO_USER,
|
|
54
57
|
LOGMSG_WAR_SEC_NOLDAP_OBJ,
|
|
58
|
+
MICROSOFT_KEY_SET_URL,
|
|
55
59
|
PERMISSION_PREFIX,
|
|
56
60
|
)
|
|
57
61
|
|
|
@@ -60,65 +64,71 @@ log = logging.getLogger(__name__)
|
|
|
60
64
|
|
|
61
65
|
class AbstractSecurityManager(BaseManager):
|
|
62
66
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
Abstract SecurityManager class, declares all methods used by the
|
|
68
|
+
framework. There is no assumptions about security models or auth types.
|
|
65
69
|
"""
|
|
66
70
|
|
|
67
71
|
def add_permissions_view(self, base_permissions, view_menu):
|
|
68
72
|
"""
|
|
69
|
-
|
|
73
|
+
Adds a permission on a view menu to the backend
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
:param base_permissions:
|
|
76
|
+
list of permissions from view (all exposed methods):
|
|
77
|
+
'can_add','can_edit' etc...
|
|
78
|
+
:param view_menu:
|
|
79
|
+
name of the view or menu to add
|
|
76
80
|
"""
|
|
77
81
|
raise NotImplementedError
|
|
78
82
|
|
|
79
83
|
def add_permissions_menu(self, view_menu_name):
|
|
80
84
|
"""
|
|
81
|
-
|
|
85
|
+
Adds menu_access to menu on permission_view_menu
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
:param view_menu_name:
|
|
88
|
+
The menu name
|
|
85
89
|
"""
|
|
86
90
|
raise NotImplementedError
|
|
87
91
|
|
|
88
92
|
def register_views(self):
|
|
89
93
|
"""
|
|
90
|
-
|
|
94
|
+
Generic function to create the security views
|
|
91
95
|
"""
|
|
92
96
|
raise NotImplementedError
|
|
93
97
|
|
|
94
98
|
def is_item_public(self, permission_name, view_name):
|
|
95
99
|
"""
|
|
96
|
-
|
|
100
|
+
Check if view has public permissions
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
:param permission_name:
|
|
103
|
+
the permission: can_show, can_edit...
|
|
104
|
+
:param view_name:
|
|
105
|
+
the name of the class view (child of BaseView)
|
|
102
106
|
"""
|
|
103
107
|
raise NotImplementedError
|
|
104
108
|
|
|
105
109
|
def has_access(self, permission_name, view_name):
|
|
106
110
|
"""
|
|
107
|
-
|
|
111
|
+
Check if current user or public has access to view or menu
|
|
108
112
|
"""
|
|
109
113
|
raise NotImplementedError
|
|
110
114
|
|
|
111
115
|
def security_cleanup(self, baseviews, menus):
|
|
112
116
|
raise NotImplementedError
|
|
113
117
|
|
|
118
|
+
def get_first_user(self):
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
def noop_user_update(self, user) -> None:
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
|
|
114
124
|
|
|
115
125
|
def _oauth_tokengetter(token=None):
|
|
116
126
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
Default function to return the current user oauth token
|
|
128
|
+
from session cookie.
|
|
119
129
|
"""
|
|
120
130
|
token = session.get("oauth")
|
|
121
|
-
log.debug("Token Get:
|
|
131
|
+
log.debug("Token Get: %s", token)
|
|
122
132
|
return token
|
|
123
133
|
|
|
124
134
|
|
|
@@ -133,8 +143,6 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
133
143
|
""" Flask-Login LoginManager """
|
|
134
144
|
jwt_manager = None
|
|
135
145
|
""" Flask-JWT-Extended """
|
|
136
|
-
oid = None
|
|
137
|
-
""" Flask-OpenID OpenID """
|
|
138
146
|
oauth = None
|
|
139
147
|
""" Flask-OAuth """
|
|
140
148
|
oauth_remotes = None
|
|
@@ -149,6 +157,8 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
149
157
|
""" Override to set your own User Model """
|
|
150
158
|
role_model = None
|
|
151
159
|
""" Override to set your own Role Model """
|
|
160
|
+
group_model = None
|
|
161
|
+
""" Override to set your own Group Model """
|
|
152
162
|
permission_model = None
|
|
153
163
|
""" Override to set your own Permission Model """
|
|
154
164
|
viewmenu_model = None
|
|
@@ -162,8 +172,6 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
162
172
|
""" Override if you want your own user db view """
|
|
163
173
|
userldapmodelview = UserLDAPModelView
|
|
164
174
|
""" Override if you want your own user ldap view """
|
|
165
|
-
useroidmodelview = UserOIDModelView
|
|
166
|
-
""" Override if you want your own user OID view """
|
|
167
175
|
useroauthmodelview = UserOAuthModelView
|
|
168
176
|
""" Override if you want your own user OAuth view """
|
|
169
177
|
userremoteusermodelview = UserRemoteUserModelView
|
|
@@ -174,8 +182,6 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
174
182
|
""" Override if you want your own Authentication DB view """
|
|
175
183
|
authldapview = AuthLDAPView
|
|
176
184
|
""" Override if you want your own Authentication LDAP view """
|
|
177
|
-
authoidview = AuthOIDView
|
|
178
|
-
""" Override if you want your own Authentication OID view """
|
|
179
185
|
authoauthview = AuthOAuthView
|
|
180
186
|
""" Override if you want your own Authentication OAuth view """
|
|
181
187
|
authremoteuserview = AuthRemoteUserView
|
|
@@ -183,8 +189,6 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
183
189
|
|
|
184
190
|
registeruserdbview = RegisterUserDBView
|
|
185
191
|
""" Override if you want your own register user db view """
|
|
186
|
-
registeruseroidview = RegisterUserOIDView
|
|
187
|
-
""" Override if you want your own register user OpenID view """
|
|
188
192
|
registeruseroauthview = RegisterUserOAuthView
|
|
189
193
|
""" Override if you want your own register user OAuth view """
|
|
190
194
|
|
|
@@ -200,66 +204,89 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
200
204
|
""" Override if you want your own Security API login endpoint """
|
|
201
205
|
|
|
202
206
|
rolemodelview = RoleModelView
|
|
207
|
+
groupmodelview = UserGroupModelView
|
|
203
208
|
permissionmodelview = PermissionModelView
|
|
204
209
|
userstatschartview = UserStatsChartView
|
|
205
210
|
viewmenumodelview = ViewMenuModelView
|
|
206
211
|
permissionviewmodelview = PermissionViewModelView
|
|
207
212
|
|
|
208
213
|
def __init__(self, appbuilder):
|
|
209
|
-
super(
|
|
210
|
-
app = self.appbuilder.get_app
|
|
214
|
+
super().__init__(appbuilder)
|
|
211
215
|
# Base Security Config
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
current_app.config.setdefault("AUTH_ROLE_ADMIN", "Admin")
|
|
217
|
+
current_app.config.setdefault("AUTH_ROLE_PUBLIC", "Public")
|
|
218
|
+
current_app.config.setdefault("AUTH_TYPE", AUTH_DB)
|
|
215
219
|
# Self Registration
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
220
|
+
current_app.config.setdefault("AUTH_USER_REGISTRATION", False)
|
|
221
|
+
current_app.config.setdefault(
|
|
222
|
+
"AUTH_USER_REGISTRATION_ROLE", self.auth_role_public
|
|
223
|
+
)
|
|
224
|
+
current_app.config.setdefault("AUTH_USER_REGISTRATION_ROLE_JMESPATH", None)
|
|
219
225
|
# Role Mapping
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
current_app.config.setdefault("AUTH_ROLES_MAPPING", {})
|
|
227
|
+
current_app.config.setdefault("AUTH_ROLES_SYNC_AT_LOGIN", False)
|
|
228
|
+
current_app.config.setdefault("AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS", False)
|
|
229
|
+
|
|
230
|
+
# Werkzeug prior to 3.0.0 does not support scrypt
|
|
231
|
+
parsed_werkzeug_version = Version(importlib.metadata.version("werkzeug"))
|
|
232
|
+
if parsed_werkzeug_version < Version("3.0.0"):
|
|
233
|
+
current_app.config.setdefault(
|
|
234
|
+
"AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
|
|
235
|
+
"pbkdf2:sha256:150000$Z3t6fmj2$22da622d94a1f8118"
|
|
236
|
+
"c0976a03d2f18f680bfff877c9a965db9eedc51bc0be87c",
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
current_app.config.setdefault(
|
|
240
|
+
"AUTH_DB_FAKE_PASSWORD_HASH_CHECK",
|
|
241
|
+
"scrypt:32768:8:1$wiDa0ruWlIPhp9LM$6e40"
|
|
242
|
+
"9d093e62ad54df2af895d0e125b05ff6cf6414"
|
|
243
|
+
"8350189ffc4bcc71286edf1b8ad94a442c00f8"
|
|
244
|
+
"90224bf2b32153d0750c89ee9401e62f9dcee5399065e4e5",
|
|
245
|
+
)
|
|
222
246
|
|
|
223
247
|
# LDAP Config
|
|
224
248
|
if self.auth_type == AUTH_LDAP:
|
|
225
|
-
if "AUTH_LDAP_SERVER" not in
|
|
249
|
+
if "AUTH_LDAP_SERVER" not in current_app.config:
|
|
226
250
|
raise Exception(
|
|
227
251
|
"No AUTH_LDAP_SERVER defined on config"
|
|
228
252
|
" with AUTH_LDAP authentication type."
|
|
229
253
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
current_app.config.setdefault("AUTH_LDAP_SEARCH", "")
|
|
255
|
+
current_app.config.setdefault("AUTH_LDAP_SEARCH_FILTER", "")
|
|
256
|
+
current_app.config.setdefault("AUTH_LDAP_APPEND_DOMAIN", "")
|
|
257
|
+
current_app.config.setdefault("AUTH_LDAP_USERNAME_FORMAT", "")
|
|
258
|
+
current_app.config.setdefault("AUTH_LDAP_BIND_USER", "")
|
|
259
|
+
current_app.config.setdefault("AUTH_LDAP_BIND_PASSWORD", "")
|
|
236
260
|
# TLS options
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
current_app.config.setdefault("AUTH_LDAP_USE_TLS", False)
|
|
262
|
+
current_app.config.setdefault("AUTH_LDAP_ALLOW_SELF_SIGNED", False)
|
|
263
|
+
current_app.config.setdefault("AUTH_LDAP_TLS_DEMAND", False)
|
|
264
|
+
current_app.config.setdefault("AUTH_LDAP_TLS_CACERTDIR", "")
|
|
265
|
+
current_app.config.setdefault("AUTH_LDAP_TLS_CACERTFILE", "")
|
|
266
|
+
current_app.config.setdefault("AUTH_LDAP_TLS_CERTFILE", "")
|
|
267
|
+
current_app.config.setdefault("AUTH_LDAP_TLS_KEYFILE", "")
|
|
244
268
|
# Mapping options
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
current_app.config.setdefault("AUTH_LDAP_UID_FIELD", "uid")
|
|
270
|
+
current_app.config.setdefault("AUTH_LDAP_GROUP_FIELD", "memberOf")
|
|
271
|
+
current_app.config.setdefault("AUTH_LDAP_FIRSTNAME_FIELD", "givenName")
|
|
272
|
+
current_app.config.setdefault("AUTH_LDAP_LASTNAME_FIELD", "sn")
|
|
273
|
+
current_app.config.setdefault("AUTH_LDAP_EMAIL_FIELD", "mail")
|
|
250
274
|
|
|
251
|
-
if self.auth_type ==
|
|
252
|
-
|
|
275
|
+
if self.auth_type == AUTH_REMOTE_USER:
|
|
276
|
+
current_app.config.setdefault("AUTH_REMOTE_USER_ENV_VAR", "REMOTE_USER")
|
|
277
|
+
|
|
278
|
+
# Rate limiting
|
|
279
|
+
current_app.config.setdefault("AUTH_RATE_LIMITED", False)
|
|
280
|
+
current_app.config.setdefault("AUTH_RATE_LIMIT", "10 per 20 second")
|
|
253
281
|
|
|
254
|
-
self.oid = OpenID(app)
|
|
255
282
|
if self.auth_type == AUTH_OAUTH:
|
|
256
283
|
from authlib.integrations.flask_client import OAuth
|
|
257
284
|
|
|
258
|
-
self.oauth = OAuth(
|
|
259
|
-
self.oauth_remotes =
|
|
285
|
+
self.oauth = OAuth(current_app)
|
|
286
|
+
self.oauth_remotes = {}
|
|
260
287
|
for _provider in self.oauth_providers:
|
|
261
288
|
provider_name = _provider["name"]
|
|
262
|
-
log.debug("OAuth providers init
|
|
289
|
+
log.debug("OAuth providers init %s", provider_name)
|
|
263
290
|
obj_provider = self.oauth.register(
|
|
264
291
|
provider_name, **_provider["remote_app"]
|
|
265
292
|
)
|
|
@@ -273,16 +300,26 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
273
300
|
|
|
274
301
|
self._builtin_roles = self.create_builtin_roles()
|
|
275
302
|
# Setup Flask-Login
|
|
276
|
-
self.lm = self.create_login_manager(
|
|
303
|
+
self.lm = self.create_login_manager(current_app)
|
|
277
304
|
|
|
278
305
|
# Setup Flask-Jwt-Extended
|
|
279
|
-
self.jwt_manager = self.create_jwt_manager(
|
|
306
|
+
self.jwt_manager = self.create_jwt_manager(current_app)
|
|
307
|
+
|
|
308
|
+
# Setup Flask-Limiter
|
|
309
|
+
self.limiter = self.create_limiter(current_app)
|
|
310
|
+
|
|
311
|
+
def create_limiter(self, app: Flask) -> Limiter:
|
|
312
|
+
limiter = Limiter(
|
|
313
|
+
key_func=app.config.get("RATELIMIT_KEY_FUNC", get_remote_address)
|
|
314
|
+
)
|
|
315
|
+
limiter.init_app(app)
|
|
316
|
+
return limiter
|
|
280
317
|
|
|
281
318
|
def create_login_manager(self, app) -> LoginManager:
|
|
282
319
|
"""
|
|
283
|
-
|
|
320
|
+
Override to implement your custom login manager instance
|
|
284
321
|
|
|
285
|
-
|
|
322
|
+
:param app: Flask app
|
|
286
323
|
"""
|
|
287
324
|
lm = LoginManager(app)
|
|
288
325
|
lm.login_view = "login"
|
|
@@ -291,19 +328,20 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
291
328
|
|
|
292
329
|
def create_jwt_manager(self, app) -> JWTManager:
|
|
293
330
|
"""
|
|
294
|
-
|
|
331
|
+
Override to implement your custom JWT manager instance
|
|
295
332
|
|
|
296
|
-
|
|
333
|
+
:param app: Flask app
|
|
297
334
|
"""
|
|
298
335
|
jwt_manager = JWTManager()
|
|
299
336
|
jwt_manager.init_app(app)
|
|
300
|
-
jwt_manager.
|
|
337
|
+
jwt_manager.user_lookup_loader(self.load_user_jwt)
|
|
301
338
|
return jwt_manager
|
|
302
339
|
|
|
303
|
-
|
|
304
|
-
|
|
340
|
+
@staticmethod
|
|
341
|
+
def create_builtin_roles():
|
|
342
|
+
return current_app.config.get("FAB_ROLES", {})
|
|
305
343
|
|
|
306
|
-
def get_roles_from_keys(self, role_keys: List[str]) ->
|
|
344
|
+
def get_roles_from_keys(self, role_keys: List[str]) -> Set[role_model]:
|
|
307
345
|
"""
|
|
308
346
|
Construct a list of FAB role objects, from a list of keys.
|
|
309
347
|
|
|
@@ -314,22 +352,26 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
314
352
|
:param role_keys: the list of FAB role keys
|
|
315
353
|
:return: a list of RoleModelView
|
|
316
354
|
"""
|
|
317
|
-
_roles =
|
|
355
|
+
_roles = set()
|
|
318
356
|
_role_keys = set(role_keys)
|
|
319
357
|
for role_key, fab_role_names in self.auth_roles_mapping.items():
|
|
320
358
|
if role_key in _role_keys:
|
|
321
359
|
for fab_role_name in fab_role_names:
|
|
322
360
|
fab_role = self.find_role(fab_role_name)
|
|
323
361
|
if fab_role:
|
|
324
|
-
_roles.
|
|
362
|
+
_roles.add(fab_role)
|
|
325
363
|
else:
|
|
326
364
|
log.warning(
|
|
327
|
-
"Can't find role specified in AUTH_ROLES_MAPPING:
|
|
328
|
-
|
|
329
|
-
)
|
|
365
|
+
"Can't find role specified in AUTH_ROLES_MAPPING: %s",
|
|
366
|
+
fab_role_name,
|
|
330
367
|
)
|
|
331
368
|
return _roles
|
|
332
369
|
|
|
370
|
+
@property
|
|
371
|
+
def auth_type_provider_name(self) -> Optional[str]:
|
|
372
|
+
provider_to_auth_type = {AUTH_DB: "db", AUTH_LDAP: "ldap"}
|
|
373
|
+
return provider_to_auth_type.get(self.auth_type)
|
|
374
|
+
|
|
333
375
|
@property
|
|
334
376
|
def get_url_for_registeruser(self):
|
|
335
377
|
return url_for(
|
|
@@ -346,132 +388,144 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
346
388
|
return self.registerusermodelview.datamodel
|
|
347
389
|
|
|
348
390
|
@property
|
|
349
|
-
def builtin_roles(self):
|
|
391
|
+
def builtin_roles(self) -> Dict[str, Any]:
|
|
350
392
|
return self._builtin_roles
|
|
351
393
|
|
|
352
394
|
@property
|
|
353
|
-
def
|
|
354
|
-
return
|
|
395
|
+
def api_login_allow_multiple_providers(self):
|
|
396
|
+
return current_app.config["AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS"]
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def auth_type(self) -> int:
|
|
400
|
+
return current_app.config["AUTH_TYPE"]
|
|
355
401
|
|
|
356
402
|
@property
|
|
357
|
-
def auth_username_ci(self):
|
|
358
|
-
return
|
|
403
|
+
def auth_username_ci(self) -> str:
|
|
404
|
+
return current_app.config.get("AUTH_USERNAME_CI", True)
|
|
359
405
|
|
|
360
406
|
@property
|
|
361
|
-
def auth_role_admin(self):
|
|
362
|
-
return
|
|
407
|
+
def auth_role_admin(self) -> str:
|
|
408
|
+
return current_app.config["AUTH_ROLE_ADMIN"]
|
|
363
409
|
|
|
364
410
|
@property
|
|
365
|
-
def auth_role_public(self):
|
|
366
|
-
return
|
|
411
|
+
def auth_role_public(self) -> str:
|
|
412
|
+
return current_app.config["AUTH_ROLE_PUBLIC"]
|
|
367
413
|
|
|
368
414
|
@property
|
|
369
|
-
def auth_ldap_server(self):
|
|
370
|
-
return
|
|
415
|
+
def auth_ldap_server(self) -> str:
|
|
416
|
+
return current_app.config["AUTH_LDAP_SERVER"]
|
|
371
417
|
|
|
372
418
|
@property
|
|
373
|
-
def auth_ldap_use_tls(self):
|
|
374
|
-
return
|
|
419
|
+
def auth_ldap_use_tls(self) -> bool:
|
|
420
|
+
return current_app.config["AUTH_LDAP_USE_TLS"]
|
|
375
421
|
|
|
376
422
|
@property
|
|
377
|
-
def auth_user_registration(self):
|
|
378
|
-
return
|
|
423
|
+
def auth_user_registration(self) -> bool:
|
|
424
|
+
return current_app.config["AUTH_USER_REGISTRATION"]
|
|
379
425
|
|
|
380
426
|
@property
|
|
381
|
-
def auth_user_registration_role(self):
|
|
382
|
-
return
|
|
427
|
+
def auth_user_registration_role(self) -> str:
|
|
428
|
+
return current_app.config["AUTH_USER_REGISTRATION_ROLE"]
|
|
383
429
|
|
|
384
430
|
@property
|
|
385
431
|
def auth_user_registration_role_jmespath(self) -> str:
|
|
386
|
-
return
|
|
432
|
+
return current_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def auth_remote_user_env_var(self) -> str:
|
|
436
|
+
return current_app.config["AUTH_REMOTE_USER_ENV_VAR"]
|
|
387
437
|
|
|
388
438
|
@property
|
|
389
439
|
def auth_roles_mapping(self) -> Dict[str, List[str]]:
|
|
390
|
-
return
|
|
440
|
+
return current_app.config["AUTH_ROLES_MAPPING"]
|
|
391
441
|
|
|
392
442
|
@property
|
|
393
443
|
def auth_roles_sync_at_login(self) -> bool:
|
|
394
|
-
return
|
|
444
|
+
return current_app.config["AUTH_ROLES_SYNC_AT_LOGIN"]
|
|
395
445
|
|
|
396
446
|
@property
|
|
397
447
|
def auth_ldap_search(self):
|
|
398
|
-
return
|
|
448
|
+
return current_app.config["AUTH_LDAP_SEARCH"]
|
|
399
449
|
|
|
400
450
|
@property
|
|
401
451
|
def auth_ldap_search_filter(self):
|
|
402
|
-
return
|
|
452
|
+
return current_app.config["AUTH_LDAP_SEARCH_FILTER"]
|
|
403
453
|
|
|
404
454
|
@property
|
|
405
455
|
def auth_ldap_bind_user(self):
|
|
406
|
-
return
|
|
456
|
+
return current_app.config["AUTH_LDAP_BIND_USER"]
|
|
407
457
|
|
|
408
458
|
@property
|
|
409
459
|
def auth_ldap_bind_password(self):
|
|
410
|
-
return
|
|
460
|
+
return current_app.config["AUTH_LDAP_BIND_PASSWORD"]
|
|
411
461
|
|
|
412
462
|
@property
|
|
413
463
|
def auth_ldap_append_domain(self):
|
|
414
|
-
return
|
|
464
|
+
return current_app.config["AUTH_LDAP_APPEND_DOMAIN"]
|
|
415
465
|
|
|
416
466
|
@property
|
|
417
467
|
def auth_ldap_username_format(self):
|
|
418
|
-
return
|
|
468
|
+
return current_app.config["AUTH_LDAP_USERNAME_FORMAT"]
|
|
419
469
|
|
|
420
470
|
@property
|
|
421
471
|
def auth_ldap_uid_field(self):
|
|
422
|
-
return
|
|
472
|
+
return current_app.config["AUTH_LDAP_UID_FIELD"]
|
|
423
473
|
|
|
424
474
|
@property
|
|
425
475
|
def auth_ldap_group_field(self) -> str:
|
|
426
|
-
return
|
|
476
|
+
return current_app.config["AUTH_LDAP_GROUP_FIELD"]
|
|
427
477
|
|
|
428
478
|
@property
|
|
429
479
|
def auth_ldap_firstname_field(self):
|
|
430
|
-
return
|
|
480
|
+
return current_app.config["AUTH_LDAP_FIRSTNAME_FIELD"]
|
|
431
481
|
|
|
432
482
|
@property
|
|
433
483
|
def auth_ldap_lastname_field(self):
|
|
434
|
-
return
|
|
484
|
+
return current_app.config["AUTH_LDAP_LASTNAME_FIELD"]
|
|
435
485
|
|
|
436
486
|
@property
|
|
437
487
|
def auth_ldap_email_field(self):
|
|
438
|
-
return
|
|
488
|
+
return current_app.config["AUTH_LDAP_EMAIL_FIELD"]
|
|
439
489
|
|
|
440
490
|
@property
|
|
441
491
|
def auth_ldap_bind_first(self):
|
|
442
|
-
return
|
|
492
|
+
return current_app.config["AUTH_LDAP_BIND_FIRST"]
|
|
443
493
|
|
|
444
494
|
@property
|
|
445
495
|
def auth_ldap_allow_self_signed(self):
|
|
446
|
-
return
|
|
496
|
+
return current_app.config["AUTH_LDAP_ALLOW_SELF_SIGNED"]
|
|
447
497
|
|
|
448
498
|
@property
|
|
449
499
|
def auth_ldap_tls_demand(self):
|
|
450
|
-
return
|
|
500
|
+
return current_app.config["AUTH_LDAP_TLS_DEMAND"]
|
|
451
501
|
|
|
452
502
|
@property
|
|
453
503
|
def auth_ldap_tls_cacertdir(self):
|
|
454
|
-
return
|
|
504
|
+
return current_app.config["AUTH_LDAP_TLS_CACERTDIR"]
|
|
455
505
|
|
|
456
506
|
@property
|
|
457
507
|
def auth_ldap_tls_cacertfile(self):
|
|
458
|
-
return
|
|
508
|
+
return current_app.config["AUTH_LDAP_TLS_CACERTFILE"]
|
|
459
509
|
|
|
460
510
|
@property
|
|
461
511
|
def auth_ldap_tls_certfile(self):
|
|
462
|
-
return
|
|
512
|
+
return current_app.config["AUTH_LDAP_TLS_CERTFILE"]
|
|
463
513
|
|
|
464
514
|
@property
|
|
465
515
|
def auth_ldap_tls_keyfile(self):
|
|
466
|
-
return
|
|
516
|
+
return current_app.config["AUTH_LDAP_TLS_KEYFILE"]
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def oauth_providers(self):
|
|
520
|
+
return current_app.config["OAUTH_PROVIDERS"]
|
|
467
521
|
|
|
468
522
|
@property
|
|
469
|
-
def
|
|
470
|
-
return
|
|
523
|
+
def is_auth_limited(self) -> bool:
|
|
524
|
+
return current_app.config["AUTH_RATE_LIMITED"]
|
|
471
525
|
|
|
472
526
|
@property
|
|
473
|
-
def
|
|
474
|
-
return
|
|
527
|
+
def auth_rate_limit(self) -> str:
|
|
528
|
+
return current_app.config["AUTH_RATE_LIMIT"]
|
|
475
529
|
|
|
476
530
|
@property
|
|
477
531
|
def current_user(self):
|
|
@@ -480,44 +534,38 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
480
534
|
elif current_user_jwt:
|
|
481
535
|
return current_user_jwt
|
|
482
536
|
|
|
483
|
-
def oauth_user_info_getter(
|
|
537
|
+
def oauth_user_info_getter(
|
|
538
|
+
self,
|
|
539
|
+
func: Callable[["BaseSecurityManager", str, Dict[str, Any]], Dict[str, Any]],
|
|
540
|
+
):
|
|
484
541
|
"""
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
542
|
+
Decorator function to be the OAuth user info getter
|
|
543
|
+
for all the providers, receives provider and response
|
|
544
|
+
return a dict with the information returned from the provider.
|
|
545
|
+
The returned user info dict should have it's keys with the same
|
|
546
|
+
name as the User Model.
|
|
490
547
|
|
|
491
|
-
|
|
548
|
+
Use it like this an example for GitHub ::
|
|
492
549
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return {}
|
|
550
|
+
@appbuilder.sm.oauth_user_info_getter
|
|
551
|
+
def my_oauth_user_info(sm, provider, response=None):
|
|
552
|
+
if provider == 'github':
|
|
553
|
+
me = sm.oauth_remotes[provider].get('user')
|
|
554
|
+
return {'username': me.data.get('login')}
|
|
555
|
+
return {}
|
|
500
556
|
"""
|
|
501
557
|
|
|
502
|
-
def wraps(provider, response=None):
|
|
503
|
-
|
|
504
|
-
# Checks if decorator is well behaved and returns a dict as supposed.
|
|
505
|
-
if not type(ret) == dict:
|
|
506
|
-
log.error(
|
|
507
|
-
"OAuth user info decorated function "
|
|
508
|
-
"did not returned a dict, but: {0}".format(type(ret))
|
|
509
|
-
)
|
|
510
|
-
return {}
|
|
511
|
-
return ret
|
|
558
|
+
def wraps(provider: str, response: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
559
|
+
return func(self, provider, response)
|
|
512
560
|
|
|
513
561
|
self.oauth_user_info = wraps
|
|
514
562
|
return wraps
|
|
515
563
|
|
|
516
564
|
def get_oauth_token_key_name(self, provider):
|
|
517
565
|
"""
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
566
|
+
Returns the token_key name for the oauth provider
|
|
567
|
+
if none is configured defaults to oauth_token
|
|
568
|
+
this is configured using OAUTH_PROVIDERS and token_key key.
|
|
521
569
|
"""
|
|
522
570
|
for _provider in self.oauth_providers:
|
|
523
571
|
if _provider["name"] == provider:
|
|
@@ -525,9 +573,9 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
525
573
|
|
|
526
574
|
def get_oauth_token_secret_name(self, provider):
|
|
527
575
|
"""
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
576
|
+
Returns the token_secret name for the oauth provider
|
|
577
|
+
if none is configured defaults to oauth_secret
|
|
578
|
+
this is configured using OAUTH_PROVIDERS and token_secret
|
|
531
579
|
"""
|
|
532
580
|
for _provider in self.oauth_providers:
|
|
533
581
|
if _provider["name"] == provider:
|
|
@@ -535,7 +583,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
535
583
|
|
|
536
584
|
def set_oauth_session(self, provider, oauth_response):
|
|
537
585
|
"""
|
|
538
|
-
|
|
586
|
+
Set the current session with OAuth user secrets
|
|
539
587
|
"""
|
|
540
588
|
# Get this provider key names for token_key and token_secret
|
|
541
589
|
token_key = self.appbuilder.sm.get_oauth_token_key_name(provider)
|
|
@@ -547,22 +595,24 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
547
595
|
)
|
|
548
596
|
session["oauth_provider"] = provider
|
|
549
597
|
|
|
550
|
-
def get_oauth_user_info(
|
|
598
|
+
def get_oauth_user_info(
|
|
599
|
+
self, provider: str, resp: Dict[str, Any]
|
|
600
|
+
) -> Dict[str, Any]:
|
|
551
601
|
"""
|
|
552
|
-
|
|
553
|
-
|
|
602
|
+
Since there are different OAuth APIs with different ways to
|
|
603
|
+
retrieve user info
|
|
554
604
|
"""
|
|
555
605
|
# for GITHUB
|
|
556
606
|
if provider == "github" or provider == "githublocal":
|
|
557
607
|
me = self.appbuilder.sm.oauth_remotes[provider].get("user")
|
|
558
608
|
data = me.json()
|
|
559
|
-
log.debug("User info from Github:
|
|
609
|
+
log.debug("User info from Github: %s", data)
|
|
560
610
|
return {"username": "github_" + data.get("login")}
|
|
561
611
|
# for twitter
|
|
562
612
|
if provider == "twitter":
|
|
563
613
|
me = self.appbuilder.sm.oauth_remotes[provider].get("account/settings.json")
|
|
564
614
|
data = me.json()
|
|
565
|
-
log.debug("User info from Twitter:
|
|
615
|
+
log.debug("User info from Twitter: %s", data)
|
|
566
616
|
return {"username": "twitter_" + data.get("screen_name", "")}
|
|
567
617
|
# for linkedin
|
|
568
618
|
if provider == "linkedin":
|
|
@@ -570,7 +620,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
570
620
|
"people/~:(id,email-address,first-name,last-name)?format=json"
|
|
571
621
|
)
|
|
572
622
|
data = me.json()
|
|
573
|
-
log.debug("User info from Linkedin:
|
|
623
|
+
log.debug("User info from Linkedin: %s", data)
|
|
574
624
|
return {
|
|
575
625
|
"username": "linkedin_" + data.get("id", ""),
|
|
576
626
|
"email": data.get("email-address", ""),
|
|
@@ -581,31 +631,25 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
581
631
|
if provider == "google":
|
|
582
632
|
me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
|
|
583
633
|
data = me.json()
|
|
584
|
-
log.debug("User info from Google:
|
|
634
|
+
log.debug("User info from Google: %s", data)
|
|
585
635
|
return {
|
|
586
636
|
"username": "google_" + data.get("id", ""),
|
|
587
637
|
"first_name": data.get("given_name", ""),
|
|
588
638
|
"last_name": data.get("family_name", ""),
|
|
589
639
|
"email": data.get("email", ""),
|
|
590
640
|
}
|
|
591
|
-
# for Azure AD Tenant. Azure OAuth response contains
|
|
592
|
-
# JWT token which has user info.
|
|
593
|
-
# JWT token needs to be base64 decoded.
|
|
594
|
-
# https://docs.microsoft.com/en-us/azure/active-directory/develop/
|
|
595
|
-
# active-directory-protocols-oauth-code
|
|
596
641
|
if provider == "azure":
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
me = self._azure_jwt_token_parse(id_token)
|
|
601
|
-
log.debug("Parse JWT token : {0}".format(me))
|
|
642
|
+
me = self._decode_and_validate_azure_jwt(resp["id_token"])
|
|
643
|
+
log.debug("User info from Azure: %s", me)
|
|
644
|
+
# https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference#payload-claims
|
|
602
645
|
return {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
"
|
|
606
|
-
"
|
|
607
|
-
"
|
|
646
|
+
# To keep backward compatibility with previous versions
|
|
647
|
+
# of FAB, we use upn if available, otherwise we use email
|
|
648
|
+
"email": me["upn"] if "upn" in me else me["email"],
|
|
649
|
+
"first_name": me.get("given_name", ""),
|
|
650
|
+
"last_name": me.get("family_name", ""),
|
|
608
651
|
"username": me["oid"],
|
|
652
|
+
"role_keys": me.get("roles", []),
|
|
609
653
|
}
|
|
610
654
|
# for OpenShift
|
|
611
655
|
if provider == "openshift":
|
|
@@ -613,55 +657,127 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
613
657
|
"apis/user.openshift.io/v1/users/~"
|
|
614
658
|
)
|
|
615
659
|
data = me.json()
|
|
616
|
-
log.debug("User info from OpenShift:
|
|
660
|
+
log.debug("User info from OpenShift: %s", data)
|
|
617
661
|
return {"username": "openshift_" + data.get("metadata").get("name")}
|
|
618
662
|
# for Okta
|
|
619
663
|
if provider == "okta":
|
|
620
664
|
me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
|
|
621
|
-
|
|
665
|
+
data = me.json()
|
|
666
|
+
log.debug("User info from Okta: %s", data)
|
|
667
|
+
if "error" not in data:
|
|
668
|
+
return {
|
|
669
|
+
"username": f"{provider}_{data['sub']}",
|
|
670
|
+
"first_name": data.get("given_name", ""),
|
|
671
|
+
"last_name": data.get("family_name", ""),
|
|
672
|
+
"email": data["email"],
|
|
673
|
+
"role_keys": data.get("groups", []),
|
|
674
|
+
}
|
|
675
|
+
else:
|
|
676
|
+
log.error(data.get("error_description"))
|
|
677
|
+
return {}
|
|
678
|
+
# for Auth0
|
|
679
|
+
if provider == "auth0":
|
|
680
|
+
data = self.appbuilder.sm.oauth_remotes[provider].userinfo()
|
|
681
|
+
log.debug("User info from Auth0: %s", data)
|
|
622
682
|
return {
|
|
623
|
-
"username": "
|
|
624
|
-
"first_name":
|
|
625
|
-
"last_name":
|
|
626
|
-
"email":
|
|
627
|
-
"role_keys":
|
|
683
|
+
"username": f"{provider}_{data['sub']}",
|
|
684
|
+
"first_name": data.get("given_name", ""),
|
|
685
|
+
"last_name": data.get("family_name", ""),
|
|
686
|
+
"email": data["email"],
|
|
687
|
+
"role_keys": data.get("groups", []),
|
|
688
|
+
}
|
|
689
|
+
# for Keycloak
|
|
690
|
+
if provider in ["keycloak", "keycloak_before_17"]:
|
|
691
|
+
me = self.appbuilder.sm.oauth_remotes[provider].get(
|
|
692
|
+
"openid-connect/userinfo"
|
|
693
|
+
)
|
|
694
|
+
me.raise_for_status()
|
|
695
|
+
data = me.json()
|
|
696
|
+
log.debug("User info from Keycloak: %s", data)
|
|
697
|
+
return {
|
|
698
|
+
"username": data.get("preferred_username", ""),
|
|
699
|
+
"first_name": data.get("given_name", ""),
|
|
700
|
+
"last_name": data.get("family_name", ""),
|
|
701
|
+
"email": data.get("email", ""),
|
|
702
|
+
"role_keys": data.get("groups", []),
|
|
703
|
+
}
|
|
704
|
+
# for Authentik
|
|
705
|
+
if provider == "authentik":
|
|
706
|
+
id_token = resp["id_token"]
|
|
707
|
+
me = self._get_authentik_token_info(id_token)
|
|
708
|
+
log.debug("User info from authentik: %s", me)
|
|
709
|
+
return {
|
|
710
|
+
"email": me["preferred_username"],
|
|
711
|
+
"first_name": me.get("given_name", ""),
|
|
712
|
+
"username": me["nickname"],
|
|
713
|
+
"role_keys": me.get("groups", []),
|
|
628
714
|
}
|
|
629
|
-
else:
|
|
630
|
-
return {}
|
|
631
|
-
|
|
632
|
-
def _azure_parse_jwt(self, id_token):
|
|
633
|
-
jwt_token_parts = r"^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$"
|
|
634
|
-
matches = re.search(jwt_token_parts, id_token)
|
|
635
|
-
if not matches or len(matches.groups()) < 3:
|
|
636
|
-
log.error("Unable to parse token.")
|
|
637
|
-
return {}
|
|
638
|
-
return {
|
|
639
|
-
"header": matches.group(1),
|
|
640
|
-
"Payload": matches.group(2),
|
|
641
|
-
"Sig": matches.group(3),
|
|
642
|
-
}
|
|
643
715
|
|
|
644
|
-
|
|
645
|
-
jwt_split_token = self._azure_parse_jwt(id_token)
|
|
646
|
-
if not jwt_split_token:
|
|
647
|
-
return
|
|
716
|
+
raise OAuthProviderUnknown()
|
|
648
717
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
payload_b64_string = jwt_payload
|
|
652
|
-
payload_b64_string += "=" * (4 - ((len(jwt_payload) % 4)))
|
|
653
|
-
decoded_payload = base64.urlsafe_b64decode(payload_b64_string.encode("ascii"))
|
|
718
|
+
def _get_microsoft_jwks(self) -> List[Dict[str, Any]]:
|
|
719
|
+
import requests
|
|
654
720
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
721
|
+
return requests.get(MICROSOFT_KEY_SET_URL).json()
|
|
722
|
+
|
|
723
|
+
def _decode_and_validate_azure_jwt(self, id_token: str) -> Dict[str, str]:
|
|
724
|
+
verify_signature = self.oauth_remotes["azure"].client_kwargs.get(
|
|
725
|
+
"verify_signature", False
|
|
726
|
+
)
|
|
727
|
+
if verify_signature:
|
|
728
|
+
from authlib.jose import JsonWebKey, jwt as authlib_jwt
|
|
729
|
+
|
|
730
|
+
keyset = JsonWebKey.import_key_set(self._get_microsoft_jwks())
|
|
731
|
+
claims = authlib_jwt.decode(id_token, keyset)
|
|
732
|
+
claims.validate()
|
|
733
|
+
return claims
|
|
734
|
+
|
|
735
|
+
return jwt.decode(id_token, options={"verify_signature": False})
|
|
736
|
+
|
|
737
|
+
def _get_authentik_jwks(self, jwks_url) -> dict:
|
|
738
|
+
import requests
|
|
739
|
+
|
|
740
|
+
resp = requests.get(jwks_url)
|
|
741
|
+
if resp.status_code == 200:
|
|
742
|
+
return resp.json()
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
def _validate_jwt(self, id_token, jwks):
|
|
746
|
+
from authlib.jose import JsonWebKey, jwt as authlib_jwt
|
|
658
747
|
|
|
659
|
-
|
|
748
|
+
keyset = JsonWebKey.import_key_set(jwks)
|
|
749
|
+
claims = authlib_jwt.decode(id_token, keyset)
|
|
750
|
+
claims.validate()
|
|
751
|
+
log.info("JWT token is validated")
|
|
752
|
+
return claims
|
|
660
753
|
|
|
661
|
-
|
|
754
|
+
def _get_authentik_token_info(self, id_token):
|
|
755
|
+
me = jwt.decode(id_token, options={"verify_signature": False})
|
|
756
|
+
|
|
757
|
+
verify_signature = self.oauth_remotes["authentik"].client_kwargs.get(
|
|
758
|
+
"verify_signature", True
|
|
759
|
+
)
|
|
760
|
+
if verify_signature:
|
|
761
|
+
# Validate the token using authentik certificate
|
|
762
|
+
jwks_uri = self.oauth_remotes["authentik"].server_metadata.get("jwks_uri")
|
|
763
|
+
if jwks_uri:
|
|
764
|
+
jwks = self._get_authentik_jwks(jwks_uri)
|
|
765
|
+
if jwks:
|
|
766
|
+
return self._validate_jwt(id_token, jwks)
|
|
767
|
+
else:
|
|
768
|
+
log.error(
|
|
769
|
+
"jwks_uri not specified in OAuth Providers, "
|
|
770
|
+
"could not verify token signature"
|
|
771
|
+
)
|
|
772
|
+
else:
|
|
773
|
+
# Return the token info without validating
|
|
774
|
+
log.warning("JWT token is not validated!")
|
|
775
|
+
return me
|
|
776
|
+
|
|
777
|
+
raise InvalidLoginAttempt("OAuth signature verify failed")
|
|
662
778
|
|
|
663
779
|
def register_views(self):
|
|
664
|
-
if not
|
|
780
|
+
if not current_app.config.get("FAB_ADD_SECURITY_VIEWS", True):
|
|
665
781
|
return
|
|
666
782
|
# Security APIs
|
|
667
783
|
self.appbuilder.add_api(self.security_api)
|
|
@@ -669,20 +785,18 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
669
785
|
if self.auth_user_registration:
|
|
670
786
|
if self.auth_type == AUTH_DB:
|
|
671
787
|
self.registeruser_view = self.registeruserdbview()
|
|
672
|
-
elif self.auth_type == AUTH_OID:
|
|
673
|
-
self.registeruser_view = self.registeruseroidview()
|
|
674
788
|
elif self.auth_type == AUTH_OAUTH:
|
|
675
789
|
self.registeruser_view = self.registeruseroauthview()
|
|
676
790
|
if self.registeruser_view:
|
|
677
791
|
self.appbuilder.add_view_no_menu(self.registeruser_view)
|
|
678
792
|
|
|
679
|
-
self.appbuilder.add_view_no_menu(self.resetpasswordview())
|
|
680
|
-
self.appbuilder.add_view_no_menu(self.resetmypasswordview())
|
|
681
793
|
self.appbuilder.add_view_no_menu(self.userinfoeditview())
|
|
682
794
|
|
|
683
795
|
if self.auth_type == AUTH_DB:
|
|
684
796
|
self.user_view = self.userdbmodelview
|
|
685
797
|
self.auth_view = self.authdbview()
|
|
798
|
+
self.appbuilder.add_view_no_menu(self.resetpasswordview())
|
|
799
|
+
self.appbuilder.add_view_no_menu(self.resetmypasswordview())
|
|
686
800
|
|
|
687
801
|
elif self.auth_type == AUTH_LDAP:
|
|
688
802
|
self.user_view = self.userldapmodelview
|
|
@@ -693,16 +807,15 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
693
807
|
elif self.auth_type == AUTH_REMOTE_USER:
|
|
694
808
|
self.user_view = self.userremoteusermodelview
|
|
695
809
|
self.auth_view = self.authremoteuserview()
|
|
696
|
-
else:
|
|
697
|
-
self.user_view = self.useroidmodelview
|
|
698
|
-
self.auth_view = self.authoidview()
|
|
699
|
-
if self.auth_user_registration:
|
|
700
|
-
pass
|
|
701
|
-
# self.registeruser_view = self.registeruseroidview()
|
|
702
|
-
# self.appbuilder.add_view_no_menu(self.registeruser_view)
|
|
703
|
-
|
|
704
810
|
self.appbuilder.add_view_no_menu(self.auth_view)
|
|
705
811
|
|
|
812
|
+
# this needs to be done after the view is added, otherwise the blueprint
|
|
813
|
+
# is not initialized
|
|
814
|
+
if self.is_auth_limited:
|
|
815
|
+
self.limiter.limit(self.auth_rate_limit, methods=["POST"])(
|
|
816
|
+
self.auth_view.blueprint
|
|
817
|
+
)
|
|
818
|
+
|
|
706
819
|
self.user_view = self.appbuilder.add_view(
|
|
707
820
|
self.user_view,
|
|
708
821
|
"List Users",
|
|
@@ -716,13 +829,22 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
716
829
|
role_view = self.appbuilder.add_view(
|
|
717
830
|
self.rolemodelview,
|
|
718
831
|
"List Roles",
|
|
719
|
-
icon="fa-
|
|
832
|
+
icon="fa-user-gear",
|
|
720
833
|
label=_("List Roles"),
|
|
721
834
|
category="Security",
|
|
722
835
|
category_icon="fa-cogs",
|
|
723
836
|
)
|
|
724
837
|
role_view.related_views = [self.user_view.__class__]
|
|
725
838
|
|
|
839
|
+
self.appbuilder.add_view(
|
|
840
|
+
self.groupmodelview,
|
|
841
|
+
"List Groups",
|
|
842
|
+
icon="fa-group",
|
|
843
|
+
label=_("List Groups"),
|
|
844
|
+
category="Security",
|
|
845
|
+
category_icon="fa-cogs",
|
|
846
|
+
)
|
|
847
|
+
|
|
726
848
|
if self.userstatschartview:
|
|
727
849
|
self.appbuilder.add_view(
|
|
728
850
|
self.userstatschartview,
|
|
@@ -734,13 +856,13 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
734
856
|
if self.auth_user_registration:
|
|
735
857
|
self.appbuilder.add_view(
|
|
736
858
|
self.registerusermodelview,
|
|
737
|
-
"User
|
|
859
|
+
"User Registrations",
|
|
738
860
|
icon="fa-user-plus",
|
|
739
861
|
label=_("User Registrations"),
|
|
740
862
|
category="Security",
|
|
741
863
|
)
|
|
742
864
|
self.appbuilder.menu.add_separator("Security")
|
|
743
|
-
if
|
|
865
|
+
if current_app.config.get("FAB_ADD_SECURITY_PERMISSION_VIEW", True):
|
|
744
866
|
self.appbuilder.add_view(
|
|
745
867
|
self.permissionmodelview,
|
|
746
868
|
"Base Permissions",
|
|
@@ -748,7 +870,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
748
870
|
label=_("Base Permissions"),
|
|
749
871
|
category="Security",
|
|
750
872
|
)
|
|
751
|
-
if
|
|
873
|
+
if current_app.config.get("FAB_ADD_SECURITY_VIEW_MENU_VIEW", True):
|
|
752
874
|
self.appbuilder.add_view(
|
|
753
875
|
self.viewmenumodelview,
|
|
754
876
|
"Views/Menus",
|
|
@@ -756,9 +878,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
756
878
|
label=_("Views/Menus"),
|
|
757
879
|
category="Security",
|
|
758
880
|
)
|
|
759
|
-
if
|
|
760
|
-
"FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW", True
|
|
761
|
-
):
|
|
881
|
+
if current_app.config.get("FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW", True):
|
|
762
882
|
self.appbuilder.add_view(
|
|
763
883
|
self.permissionviewmodelview,
|
|
764
884
|
"Permission on Views/Menus",
|
|
@@ -769,13 +889,17 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
769
889
|
|
|
770
890
|
def create_db(self):
|
|
771
891
|
"""
|
|
772
|
-
|
|
892
|
+
Setups the DB, creates admin and public roles if they don't exist.
|
|
773
893
|
"""
|
|
774
|
-
roles_mapping =
|
|
894
|
+
roles_mapping = current_app.config.get("FAB_ROLES_MAPPING", {})
|
|
775
895
|
for pk, name in roles_mapping.items():
|
|
776
896
|
self.update_role(pk, name)
|
|
777
|
-
for role_name in self.builtin_roles:
|
|
778
|
-
|
|
897
|
+
for role_name, permission_view_menus in self.builtin_roles.items():
|
|
898
|
+
permission_view_menus = [
|
|
899
|
+
self.add_permission_view_menu(permission_name, view_menu_name)
|
|
900
|
+
for view_menu_name, permission_name in permission_view_menus
|
|
901
|
+
]
|
|
902
|
+
self.add_role(name=role_name, permissions=permission_view_menus)
|
|
779
903
|
if self.auth_role_admin not in self.builtin_roles:
|
|
780
904
|
self.add_role(self.auth_role_admin)
|
|
781
905
|
self.add_role(self.auth_role_public)
|
|
@@ -784,26 +908,35 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
784
908
|
|
|
785
909
|
def reset_password(self, userid, password):
|
|
786
910
|
"""
|
|
787
|
-
|
|
788
|
-
|
|
911
|
+
Change/Reset a user's password for authdb.
|
|
912
|
+
Password will be hashed and saved.
|
|
789
913
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
914
|
+
:param userid:
|
|
915
|
+
the user.id to reset the password
|
|
916
|
+
:param password:
|
|
917
|
+
The clear text password to reset and save hashed on the db
|
|
794
918
|
"""
|
|
795
919
|
user = self.get_user_by_id(userid)
|
|
796
|
-
user.password = generate_password_hash(
|
|
920
|
+
user.password = generate_password_hash(
|
|
921
|
+
password=password,
|
|
922
|
+
method=current_app.config.get("FAB_PASSWORD_HASH_METHOD", "scrypt"),
|
|
923
|
+
salt_length=current_app.config.get("FAB_PASSWORD_HASH_SALT_LENGTH", 16),
|
|
924
|
+
)
|
|
797
925
|
self.update_user(user)
|
|
798
926
|
|
|
799
927
|
def update_user_auth_stat(self, user, success=True):
|
|
800
928
|
"""
|
|
801
|
-
|
|
929
|
+
Update user authentication stats upon successful/unsuccessful
|
|
930
|
+
authentication attempts.
|
|
802
931
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
932
|
+
:param user:
|
|
933
|
+
The identified (but possibly not successfully authenticated) user
|
|
934
|
+
model
|
|
935
|
+
:param success:
|
|
936
|
+
:type success: bool or None
|
|
937
|
+
Defaults to true, if true increments login_count, updates
|
|
938
|
+
last_login, and resets fail_login_count to 0, if false increments
|
|
939
|
+
fail_login_count on user model.
|
|
807
940
|
"""
|
|
808
941
|
if not user.login_count:
|
|
809
942
|
user.login_count = 0
|
|
@@ -811,45 +944,57 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
811
944
|
user.fail_login_count = 0
|
|
812
945
|
if success:
|
|
813
946
|
user.login_count += 1
|
|
947
|
+
user.last_login = datetime.datetime.now()
|
|
814
948
|
user.fail_login_count = 0
|
|
815
949
|
else:
|
|
816
950
|
user.fail_login_count += 1
|
|
817
|
-
user.last_login = datetime.datetime.now()
|
|
818
951
|
self.update_user(user)
|
|
819
952
|
|
|
820
953
|
def auth_user_db(self, username, password):
|
|
821
954
|
"""
|
|
822
|
-
|
|
955
|
+
Method for authenticating user, auth db style
|
|
823
956
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
957
|
+
:param username:
|
|
958
|
+
The username or registered email address
|
|
959
|
+
:param password:
|
|
960
|
+
The password, will be tested against hashed password on db
|
|
828
961
|
"""
|
|
829
962
|
if username is None or username == "":
|
|
830
963
|
return None
|
|
964
|
+
first_user = self.get_first_user()
|
|
831
965
|
user = self.find_user(username=username)
|
|
832
966
|
if user is None:
|
|
833
967
|
user = self.find_user(email=username)
|
|
968
|
+
else:
|
|
969
|
+
# Balance failure and success
|
|
970
|
+
_ = self.find_user(email=username)
|
|
834
971
|
if user is None or (not user.is_active):
|
|
835
|
-
|
|
972
|
+
# Balance failure and success
|
|
973
|
+
check_password_hash(
|
|
974
|
+
current_app.config["AUTH_DB_FAKE_PASSWORD_HASH_CHECK"],
|
|
975
|
+
"password",
|
|
976
|
+
)
|
|
977
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
978
|
+
# Balance failure and success
|
|
979
|
+
if first_user:
|
|
980
|
+
self.noop_user_update(first_user)
|
|
836
981
|
return None
|
|
837
982
|
elif check_password_hash(user.password, password):
|
|
838
983
|
self.update_user_auth_stat(user, True)
|
|
839
984
|
return user
|
|
840
985
|
else:
|
|
841
986
|
self.update_user_auth_stat(user, False)
|
|
842
|
-
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED
|
|
987
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
843
988
|
return None
|
|
844
989
|
|
|
845
990
|
def _search_ldap(self, ldap, con, username):
|
|
846
991
|
"""
|
|
847
|
-
|
|
992
|
+
Searches LDAP for user.
|
|
848
993
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
994
|
+
:param ldap: The ldap module reference
|
|
995
|
+
:param con: The ldap connection
|
|
996
|
+
:param username: username to match with AUTH_LDAP_UID_FIELD
|
|
997
|
+
:return: ldap object array
|
|
853
998
|
"""
|
|
854
999
|
# always check AUTH_LDAP_SEARCH is set before calling this method
|
|
855
1000
|
assert self.auth_ldap_search, "AUTH_LDAP_SEARCH must be set"
|
|
@@ -871,23 +1016,31 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
871
1016
|
if len(self.auth_roles_mapping) > 0:
|
|
872
1017
|
request_fields.append(self.auth_ldap_group_field)
|
|
873
1018
|
|
|
874
|
-
#
|
|
1019
|
+
# perform the LDAP search
|
|
875
1020
|
log.debug(
|
|
876
|
-
"LDAP search for '
|
|
877
|
-
|
|
878
|
-
|
|
1021
|
+
"LDAP search for '%s' with fields %s in scope '%s'",
|
|
1022
|
+
filter_str,
|
|
1023
|
+
request_fields,
|
|
1024
|
+
self.auth_ldap_search,
|
|
879
1025
|
)
|
|
880
|
-
|
|
1026
|
+
raw_search_result = con.search_s(
|
|
881
1027
|
self.auth_ldap_search, ldap.SCOPE_SUBTREE, filter_str, request_fields
|
|
882
1028
|
)
|
|
883
|
-
log.debug("LDAP search returned:
|
|
1029
|
+
log.debug("LDAP search returned: %s", raw_search_result)
|
|
1030
|
+
|
|
1031
|
+
# Remove any search referrals from results
|
|
1032
|
+
search_result = [
|
|
1033
|
+
(dn, attrs)
|
|
1034
|
+
for dn, attrs in raw_search_result
|
|
1035
|
+
if dn is not None and isinstance(attrs, dict)
|
|
1036
|
+
]
|
|
884
1037
|
|
|
885
1038
|
# only continue if 0 or 1 results were returned
|
|
886
1039
|
if len(search_result) > 1:
|
|
887
1040
|
log.error(
|
|
888
|
-
"LDAP search for '
|
|
889
|
-
|
|
890
|
-
|
|
1041
|
+
"LDAP search for '%s' in scope '%s' returned multiple results",
|
|
1042
|
+
filter_str,
|
|
1043
|
+
self.auth_ldap_search,
|
|
891
1044
|
)
|
|
892
1045
|
return None, None
|
|
893
1046
|
|
|
@@ -904,14 +1057,14 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
904
1057
|
def _ldap_calculate_user_roles(
|
|
905
1058
|
self, user_attributes: Dict[str, bytes]
|
|
906
1059
|
) -> List[str]:
|
|
907
|
-
user_role_objects =
|
|
1060
|
+
user_role_objects = set()
|
|
908
1061
|
|
|
909
1062
|
# apply AUTH_ROLES_MAPPING
|
|
910
1063
|
if len(self.auth_roles_mapping) > 0:
|
|
911
1064
|
user_role_keys = self.ldap_extract_list(
|
|
912
1065
|
user_attributes, self.auth_ldap_group_field
|
|
913
1066
|
)
|
|
914
|
-
user_role_objects
|
|
1067
|
+
user_role_objects.update(self.get_roles_from_keys(user_role_keys))
|
|
915
1068
|
|
|
916
1069
|
# apply AUTH_USER_REGISTRATION
|
|
917
1070
|
if self.auth_user_registration:
|
|
@@ -920,37 +1073,32 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
920
1073
|
# lookup registration role in flask db
|
|
921
1074
|
fab_role = self.find_role(registration_role_name)
|
|
922
1075
|
if fab_role:
|
|
923
|
-
user_role_objects.
|
|
1076
|
+
user_role_objects.add(fab_role)
|
|
924
1077
|
else:
|
|
925
1078
|
log.warning(
|
|
926
|
-
"Can't find AUTH_USER_REGISTRATION role:
|
|
927
|
-
registration_role_name
|
|
928
|
-
)
|
|
1079
|
+
"Can't find AUTH_USER_REGISTRATION role: %s", registration_role_name
|
|
929
1080
|
)
|
|
930
1081
|
|
|
931
|
-
return user_role_objects
|
|
1082
|
+
return list(user_role_objects)
|
|
932
1083
|
|
|
933
1084
|
def _ldap_bind_indirect(self, ldap, con) -> None:
|
|
934
1085
|
"""
|
|
935
|
-
|
|
1086
|
+
Attempt to bind to LDAP using the AUTH_LDAP_BIND_USER.
|
|
936
1087
|
|
|
937
|
-
|
|
938
|
-
|
|
1088
|
+
:param ldap: The ldap module reference
|
|
1089
|
+
:param con: The ldap connection
|
|
939
1090
|
"""
|
|
940
1091
|
# always check AUTH_LDAP_BIND_USER is set before calling this method
|
|
941
1092
|
assert self.auth_ldap_bind_user, "AUTH_LDAP_BIND_USER must be set"
|
|
942
1093
|
|
|
943
1094
|
try:
|
|
944
1095
|
log.debug(
|
|
945
|
-
"LDAP bind indirect TRY with username: '
|
|
946
|
-
self.auth_ldap_bind_user
|
|
947
|
-
)
|
|
1096
|
+
"LDAP bind indirect TRY with username: '%s'", self.auth_ldap_bind_user
|
|
948
1097
|
)
|
|
949
1098
|
con.simple_bind_s(self.auth_ldap_bind_user, self.auth_ldap_bind_password)
|
|
950
1099
|
log.debug(
|
|
951
|
-
"LDAP bind indirect SUCCESS with username: '
|
|
952
|
-
|
|
953
|
-
)
|
|
1100
|
+
"LDAP bind indirect SUCCESS with username: '%s'",
|
|
1101
|
+
self.auth_ldap_bind_user,
|
|
954
1102
|
)
|
|
955
1103
|
except ldap.INVALID_CREDENTIALS as ex:
|
|
956
1104
|
log.error(
|
|
@@ -962,12 +1110,12 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
962
1110
|
@staticmethod
|
|
963
1111
|
def _ldap_bind(ldap, con, dn: str, password: str) -> bool:
|
|
964
1112
|
"""
|
|
965
|
-
|
|
1113
|
+
Validates/binds the provided dn/password with the LDAP sever.
|
|
966
1114
|
"""
|
|
967
1115
|
try:
|
|
968
|
-
log.debug("LDAP bind TRY with username: '
|
|
1116
|
+
log.debug("LDAP bind TRY with username: '%s'", dn)
|
|
969
1117
|
con.simple_bind_s(dn, password)
|
|
970
|
-
log.debug("LDAP bind SUCCESS with username: '
|
|
1118
|
+
log.debug("LDAP bind SUCCESS with username: '%s'", dn)
|
|
971
1119
|
return True
|
|
972
1120
|
except ldap.INVALID_CREDENTIALS:
|
|
973
1121
|
return False
|
|
@@ -988,12 +1136,12 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
988
1136
|
|
|
989
1137
|
def auth_user_ldap(self, username, password):
|
|
990
1138
|
"""
|
|
991
|
-
|
|
1139
|
+
Method for authenticating user with LDAP.
|
|
992
1140
|
|
|
993
|
-
|
|
1141
|
+
NOTE: this depends on python-ldap module
|
|
994
1142
|
|
|
995
|
-
|
|
996
|
-
|
|
1143
|
+
:param username: the username
|
|
1144
|
+
:param password: the password
|
|
997
1145
|
"""
|
|
998
1146
|
# If no username is provided, go away
|
|
999
1147
|
if (username is None) or username == "":
|
|
@@ -1019,12 +1167,6 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1019
1167
|
|
|
1020
1168
|
try:
|
|
1021
1169
|
# LDAP certificate settings
|
|
1022
|
-
if self.auth_ldap_allow_self_signed:
|
|
1023
|
-
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
|
|
1024
|
-
ldap.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
|
1025
|
-
elif self.auth_ldap_tls_demand:
|
|
1026
|
-
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
|
|
1027
|
-
ldap.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
|
1028
1170
|
if self.auth_ldap_tls_cacertdir:
|
|
1029
1171
|
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.auth_ldap_tls_cacertdir)
|
|
1030
1172
|
if self.auth_ldap_tls_cacertfile:
|
|
@@ -1035,6 +1177,12 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1035
1177
|
ldap.set_option(ldap.OPT_X_TLS_CERTFILE, self.auth_ldap_tls_certfile)
|
|
1036
1178
|
if self.auth_ldap_tls_keyfile:
|
|
1037
1179
|
ldap.set_option(ldap.OPT_X_TLS_KEYFILE, self.auth_ldap_tls_keyfile)
|
|
1180
|
+
if self.auth_ldap_allow_self_signed:
|
|
1181
|
+
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
|
|
1182
|
+
ldap.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
|
1183
|
+
elif self.auth_ldap_tls_demand:
|
|
1184
|
+
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
|
|
1185
|
+
ldap.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
|
1038
1186
|
|
|
1039
1187
|
# Initialise LDAP connection
|
|
1040
1188
|
con = ldap.initialize(self.auth_ldap_server)
|
|
@@ -1043,9 +1191,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1043
1191
|
try:
|
|
1044
1192
|
con.start_tls_s()
|
|
1045
1193
|
except Exception:
|
|
1046
|
-
log.error(
|
|
1047
|
-
LOGMSG_ERR_SEC_AUTH_LDAP_TLS.format(self.auth_ldap_server)
|
|
1048
|
-
)
|
|
1194
|
+
log.error(LOGMSG_ERR_SEC_AUTH_LDAP_TLS, self.auth_ldap_server)
|
|
1049
1195
|
return None
|
|
1050
1196
|
|
|
1051
1197
|
# Define variables, so we can check if they are set in later steps
|
|
@@ -1053,7 +1199,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1053
1199
|
user_attributes = {}
|
|
1054
1200
|
|
|
1055
1201
|
# Flow 1 - (Indirect Search Bind):
|
|
1056
|
-
# - in this flow, special bind credentials are used to
|
|
1202
|
+
# - in this flow, special bind credentials are used to perform the
|
|
1057
1203
|
# LDAP search
|
|
1058
1204
|
# - in this flow, AUTH_LDAP_SEARCH must be set
|
|
1059
1205
|
if self.auth_ldap_bind_user:
|
|
@@ -1075,7 +1221,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1075
1221
|
|
|
1076
1222
|
# If search failed, go away
|
|
1077
1223
|
if user_dn is None:
|
|
1078
|
-
log.info(LOGMSG_WAR_SEC_NOLDAP_OBJ
|
|
1224
|
+
log.info(LOGMSG_WAR_SEC_NOLDAP_OBJ, username)
|
|
1079
1225
|
return None
|
|
1080
1226
|
|
|
1081
1227
|
# Bind with user_dn/password (validates credentials)
|
|
@@ -1084,12 +1230,12 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1084
1230
|
self.update_user_auth_stat(user, False)
|
|
1085
1231
|
|
|
1086
1232
|
# Invalid credentials, go away
|
|
1087
|
-
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED
|
|
1233
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
1088
1234
|
return None
|
|
1089
1235
|
|
|
1090
1236
|
# Flow 2 - (Direct Search Bind):
|
|
1091
1237
|
# - in this flow, the credentials provided by the end-user are used
|
|
1092
|
-
# to
|
|
1238
|
+
# to perform the LDAP search
|
|
1093
1239
|
# - in this flow, we only search LDAP if AUTH_LDAP_SEARCH is set
|
|
1094
1240
|
# - features like AUTH_USER_REGISTRATION & AUTH_ROLES_SYNC_AT_LOGIN
|
|
1095
1241
|
# will only work if AUTH_LDAP_SEARCH is set
|
|
@@ -1115,7 +1261,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1115
1261
|
self.update_user_auth_stat(user, False)
|
|
1116
1262
|
|
|
1117
1263
|
# Invalid credentials, go away
|
|
1118
|
-
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED
|
|
1264
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, bind_username)
|
|
1119
1265
|
return None
|
|
1120
1266
|
|
|
1121
1267
|
# Search for `username` (if AUTH_LDAP_SEARCH is set)
|
|
@@ -1129,16 +1275,14 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1129
1275
|
|
|
1130
1276
|
# If search failed, go away
|
|
1131
1277
|
if user_dn is None:
|
|
1132
|
-
log.info(LOGMSG_WAR_SEC_NOLDAP_OBJ
|
|
1278
|
+
log.info(LOGMSG_WAR_SEC_NOLDAP_OBJ, username)
|
|
1133
1279
|
return None
|
|
1134
1280
|
|
|
1135
1281
|
# Sync the user's roles
|
|
1136
1282
|
if user and user_attributes and self.auth_roles_sync_at_login:
|
|
1137
1283
|
user.roles = self._ldap_calculate_user_roles(user_attributes)
|
|
1138
1284
|
log.debug(
|
|
1139
|
-
"Calculated new roles for user='
|
|
1140
|
-
user_dn, user.roles
|
|
1141
|
-
)
|
|
1285
|
+
"Calculated new roles for user='%s' as: %s", user_dn, user.roles
|
|
1142
1286
|
)
|
|
1143
1287
|
|
|
1144
1288
|
# If the user is new, register them
|
|
@@ -1158,11 +1302,11 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1158
1302
|
),
|
|
1159
1303
|
role=self._ldap_calculate_user_roles(user_attributes),
|
|
1160
1304
|
)
|
|
1161
|
-
log.debug("New user registered:
|
|
1305
|
+
log.debug("New user registered: %s", user)
|
|
1162
1306
|
|
|
1163
1307
|
# If user registration failed, go away
|
|
1164
1308
|
if not user:
|
|
1165
|
-
log.info(LOGMSG_ERR_SEC_ADD_REGISTER_USER
|
|
1309
|
+
log.info(LOGMSG_ERR_SEC_ADD_REGISTER_USER, username)
|
|
1166
1310
|
return None
|
|
1167
1311
|
|
|
1168
1312
|
# LOGIN SUCCESS (only if user is now registered)
|
|
@@ -1177,33 +1321,18 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1177
1321
|
if isinstance(e, dict):
|
|
1178
1322
|
msg = getattr(e, "message", None)
|
|
1179
1323
|
if (msg is not None) and ("desc" in msg):
|
|
1180
|
-
log.error(LOGMSG_ERR_SEC_AUTH_LDAP
|
|
1324
|
+
log.error(LOGMSG_ERR_SEC_AUTH_LDAP, e.message["desc"])
|
|
1181
1325
|
return None
|
|
1182
1326
|
else:
|
|
1183
1327
|
log.error(e)
|
|
1184
1328
|
return None
|
|
1185
1329
|
|
|
1186
|
-
def auth_user_oid(self, email):
|
|
1187
|
-
"""
|
|
1188
|
-
OpenID user Authentication
|
|
1189
|
-
|
|
1190
|
-
:param email: user's email to authenticate
|
|
1191
|
-
:type self: User model
|
|
1192
|
-
"""
|
|
1193
|
-
user = self.find_user(email=email)
|
|
1194
|
-
if user is None or (not user.is_active):
|
|
1195
|
-
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED.format(email))
|
|
1196
|
-
return None
|
|
1197
|
-
else:
|
|
1198
|
-
self.update_user_auth_stat(user)
|
|
1199
|
-
return user
|
|
1200
|
-
|
|
1201
1330
|
def auth_user_remote_user(self, username):
|
|
1202
1331
|
"""
|
|
1203
|
-
|
|
1332
|
+
REMOTE_USER user Authentication
|
|
1204
1333
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1334
|
+
:param username: user's username for remote auth
|
|
1335
|
+
:type self: User model
|
|
1207
1336
|
"""
|
|
1208
1337
|
user = self.find_user(username=username)
|
|
1209
1338
|
|
|
@@ -1222,19 +1351,19 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1222
1351
|
# If user does not exist on the DB and not auto user registration,
|
|
1223
1352
|
# or user is inactive, go away.
|
|
1224
1353
|
elif user is None or (not user.is_active):
|
|
1225
|
-
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED
|
|
1354
|
+
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
|
|
1226
1355
|
return None
|
|
1227
1356
|
|
|
1228
1357
|
self.update_user_auth_stat(user)
|
|
1229
1358
|
return user
|
|
1230
1359
|
|
|
1231
1360
|
def _oauth_calculate_user_roles(self, userinfo) -> List[str]:
|
|
1232
|
-
user_role_objects =
|
|
1361
|
+
user_role_objects = set()
|
|
1233
1362
|
|
|
1234
1363
|
# apply AUTH_ROLES_MAPPING
|
|
1235
1364
|
if len(self.auth_roles_mapping) > 0:
|
|
1236
1365
|
user_role_keys = userinfo.get("role_keys", [])
|
|
1237
|
-
user_role_objects
|
|
1366
|
+
user_role_objects.update(self.get_roles_from_keys(user_role_keys))
|
|
1238
1367
|
|
|
1239
1368
|
# apply AUTH_USER_REGISTRATION_ROLE
|
|
1240
1369
|
if self.auth_user_registration:
|
|
@@ -1252,22 +1381,20 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1252
1381
|
# lookup registration role in flask db
|
|
1253
1382
|
fab_role = self.find_role(registration_role_name)
|
|
1254
1383
|
if fab_role:
|
|
1255
|
-
user_role_objects.
|
|
1384
|
+
user_role_objects.add(fab_role)
|
|
1256
1385
|
else:
|
|
1257
1386
|
log.warning(
|
|
1258
|
-
"Can't find AUTH_USER_REGISTRATION role:
|
|
1259
|
-
registration_role_name
|
|
1260
|
-
)
|
|
1387
|
+
"Can't find AUTH_USER_REGISTRATION role: %s", registration_role_name
|
|
1261
1388
|
)
|
|
1262
1389
|
|
|
1263
|
-
return user_role_objects
|
|
1390
|
+
return list(user_role_objects)
|
|
1264
1391
|
|
|
1265
1392
|
def auth_user_oauth(self, userinfo):
|
|
1266
1393
|
"""
|
|
1267
|
-
|
|
1394
|
+
Method for authenticating user with OAuth.
|
|
1268
1395
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1396
|
+
:userinfo: dict with user information
|
|
1397
|
+
(keys are the same as User model columns)
|
|
1271
1398
|
"""
|
|
1272
1399
|
# extract the username from `userinfo`
|
|
1273
1400
|
if "username" in userinfo:
|
|
@@ -1275,9 +1402,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1275
1402
|
elif "email" in userinfo:
|
|
1276
1403
|
username = userinfo["email"]
|
|
1277
1404
|
else:
|
|
1278
|
-
log.error(
|
|
1279
|
-
"OAUTH userinfo does not have username or email {0}".format(userinfo)
|
|
1280
|
-
)
|
|
1405
|
+
log.error("OAUTH userinfo does not have username or email %s", userinfo)
|
|
1281
1406
|
return None
|
|
1282
1407
|
|
|
1283
1408
|
# If username is empty, go away
|
|
@@ -1298,11 +1423,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1298
1423
|
# Sync the user's roles
|
|
1299
1424
|
if user and self.auth_roles_sync_at_login:
|
|
1300
1425
|
user.roles = self._oauth_calculate_user_roles(userinfo)
|
|
1301
|
-
log.debug(
|
|
1302
|
-
"Calculated new roles for user='{0}' as: {1}".format(
|
|
1303
|
-
username, user.roles
|
|
1304
|
-
)
|
|
1305
|
-
)
|
|
1426
|
+
log.debug("Calculated new roles for user='%s' as: %s", username, user.roles)
|
|
1306
1427
|
|
|
1307
1428
|
# If the user is new, register them
|
|
1308
1429
|
if (not user) and self.auth_user_registration:
|
|
@@ -1313,11 +1434,11 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1313
1434
|
email=userinfo.get("email", "") or f"{username}@email.notfound",
|
|
1314
1435
|
role=self._oauth_calculate_user_roles(userinfo),
|
|
1315
1436
|
)
|
|
1316
|
-
log.debug("New user registered:
|
|
1437
|
+
log.debug("New user registered: %s", user)
|
|
1317
1438
|
|
|
1318
1439
|
# If user registration failed, go away
|
|
1319
1440
|
if not user:
|
|
1320
|
-
log.error("Error creating a new OAuth user
|
|
1441
|
+
log.error("Error creating a new OAuth user %s", username)
|
|
1321
1442
|
return None
|
|
1322
1443
|
|
|
1323
1444
|
# LOGIN SUCCESS (only if user is now registered)
|
|
@@ -1335,12 +1456,12 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1335
1456
|
|
|
1336
1457
|
def is_item_public(self, permission_name, view_name):
|
|
1337
1458
|
"""
|
|
1338
|
-
|
|
1459
|
+
Check if view has public permissions
|
|
1339
1460
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1461
|
+
:param permission_name:
|
|
1462
|
+
the permission: can_show, can_edit...
|
|
1463
|
+
:param view_name:
|
|
1464
|
+
the name of the class view (child of BaseView)
|
|
1344
1465
|
"""
|
|
1345
1466
|
permissions = self.get_public_permissions()
|
|
1346
1467
|
if permissions:
|
|
@@ -1357,7 +1478,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1357
1478
|
self, role, permission_name: str, view_name: str
|
|
1358
1479
|
) -> bool:
|
|
1359
1480
|
"""
|
|
1360
|
-
|
|
1481
|
+
Checks permission on builtin role
|
|
1361
1482
|
"""
|
|
1362
1483
|
builtin_pvms = self.builtin_roles.get(role.name, [])
|
|
1363
1484
|
for pvm in builtin_pvms:
|
|
@@ -1372,19 +1493,62 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1372
1493
|
def _has_view_access(
|
|
1373
1494
|
self, user: object, permission_name: str, view_name: str
|
|
1374
1495
|
) -> bool:
|
|
1375
|
-
roles = user
|
|
1376
|
-
|
|
1377
|
-
# First check against
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1496
|
+
roles = self.get_user_roles(user)
|
|
1497
|
+
|
|
1498
|
+
# First check against built-in roles (avoiding unnecessary DB queries)
|
|
1499
|
+
if any(
|
|
1500
|
+
role.name in self.builtin_roles
|
|
1501
|
+
and self._has_access_builtin_roles(role, permission_name, view_name)
|
|
1502
|
+
for role in roles
|
|
1503
|
+
):
|
|
1504
|
+
return True
|
|
1505
|
+
|
|
1506
|
+
db_role_ids = [role.id for role in roles if role.name not in self.builtin_roles]
|
|
1507
|
+
|
|
1508
|
+
# Check database-stored roles if no match was found in built-in roles
|
|
1509
|
+
return bool(db_role_ids) and self.exist_permission_on_roles(
|
|
1510
|
+
view_name, permission_name, db_role_ids
|
|
1511
|
+
)
|
|
1385
1512
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1513
|
+
def get_user_roles(self, user) -> List[object]:
|
|
1514
|
+
"""
|
|
1515
|
+
Get current user roles, if user is not authenticated returns the public role
|
|
1516
|
+
"""
|
|
1517
|
+
if not user.is_authenticated:
|
|
1518
|
+
return [self.get_public_role()]
|
|
1519
|
+
return user.roles + [role for group in user.groups for role in group.roles]
|
|
1520
|
+
|
|
1521
|
+
def get_user_roles_permissions(self, user) -> Dict[str, List[Tuple[str, str]]]:
|
|
1522
|
+
"""
|
|
1523
|
+
Utility method just implemented for SQLAlchemy.
|
|
1524
|
+
Take a look to: flask_appbuilder.security.sqla.manager
|
|
1525
|
+
:param user:
|
|
1526
|
+
:return:
|
|
1527
|
+
"""
|
|
1528
|
+
raise NotImplementedError()
|
|
1529
|
+
|
|
1530
|
+
def get_role_permissions(self, role) -> Set[Tuple[str, str]]:
|
|
1531
|
+
"""
|
|
1532
|
+
Get all permissions for a certain role
|
|
1533
|
+
"""
|
|
1534
|
+
result = set()
|
|
1535
|
+
if role.name in self.builtin_roles:
|
|
1536
|
+
for permission in self.builtin_roles[role.name]:
|
|
1537
|
+
result.add((permission[1], permission[0]))
|
|
1538
|
+
else:
|
|
1539
|
+
for permission in self.get_db_role_permissions(role.id):
|
|
1540
|
+
result.add((permission.permission.name, permission.view_menu.name))
|
|
1541
|
+
return result
|
|
1542
|
+
|
|
1543
|
+
def get_user_permissions(self, user) -> Set[Tuple[str, str]]:
|
|
1544
|
+
"""
|
|
1545
|
+
Get all permissions from the current user
|
|
1546
|
+
"""
|
|
1547
|
+
roles = self.get_user_roles(user)
|
|
1548
|
+
result = set()
|
|
1549
|
+
for role in roles:
|
|
1550
|
+
result.update(self.get_role_permissions(role))
|
|
1551
|
+
return result
|
|
1388
1552
|
|
|
1389
1553
|
def _get_user_permission_view_menus(
|
|
1390
1554
|
self, user: object, permission_name: str, view_menus_name: List[str]
|
|
@@ -1394,41 +1558,39 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1394
1558
|
that a user has access to. Mainly used to fetch all menu permissions
|
|
1395
1559
|
on a single db call, will also check public permissions and builtin roles
|
|
1396
1560
|
"""
|
|
1397
|
-
|
|
1398
|
-
if user is None
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
permission_name, db_role_ids
|
|
1561
|
+
# Determine user roles (use public role if user is None)
|
|
1562
|
+
roles = [self.get_public_role()] if user is None else self.get_user_roles(user)
|
|
1563
|
+
|
|
1564
|
+
# First, check built-in roles (avoiding unnecessary DB queries)
|
|
1565
|
+
result = {
|
|
1566
|
+
view_menu_name
|
|
1567
|
+
for role in roles
|
|
1568
|
+
if role.name in self.builtin_roles
|
|
1569
|
+
for view_menu_name in view_menus_name
|
|
1570
|
+
if self._has_access_builtin_roles(role, permission_name, view_menu_name)
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
# Collect database role IDs for further checking
|
|
1574
|
+
db_role_ids = [role.id for role in roles if role.name not in self.builtin_roles]
|
|
1575
|
+
|
|
1576
|
+
# Check database-stored roles if needed
|
|
1577
|
+
if db_role_ids:
|
|
1578
|
+
result.update(
|
|
1579
|
+
pvm.view_menu.name
|
|
1580
|
+
for pvm in self.find_roles_permission_view_menus(
|
|
1581
|
+
permission_name, db_role_ids
|
|
1582
|
+
)
|
|
1420
1583
|
)
|
|
1421
|
-
|
|
1422
|
-
result.update(pvms_names)
|
|
1584
|
+
|
|
1423
1585
|
return result
|
|
1424
1586
|
|
|
1425
|
-
def has_access(self, permission_name, view_name):
|
|
1587
|
+
def has_access(self, permission_name: str, view_name: str) -> bool:
|
|
1426
1588
|
"""
|
|
1427
|
-
|
|
1589
|
+
Check if current user or public has access to view or menu
|
|
1428
1590
|
"""
|
|
1429
|
-
if current_user.is_authenticated:
|
|
1591
|
+
if current_user.is_authenticated and current_user.is_active:
|
|
1430
1592
|
return self._has_view_access(g.user, permission_name, view_name)
|
|
1431
|
-
elif current_user_jwt:
|
|
1593
|
+
elif current_user_jwt and current_user_jwt.is_active:
|
|
1432
1594
|
return self._has_view_access(current_user_jwt, permission_name, view_name)
|
|
1433
1595
|
else:
|
|
1434
1596
|
return self.is_item_public(permission_name, view_name)
|
|
@@ -1447,15 +1609,33 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1447
1609
|
None, "menu_access", view_menus_name=menu_names
|
|
1448
1610
|
)
|
|
1449
1611
|
|
|
1612
|
+
def add_limit_view(self, baseview):
|
|
1613
|
+
if not baseview.limits:
|
|
1614
|
+
return
|
|
1615
|
+
|
|
1616
|
+
for limit in baseview.limits:
|
|
1617
|
+
self.limiter.limit(
|
|
1618
|
+
limit_value=limit.limit_value,
|
|
1619
|
+
key_func=limit.key_func,
|
|
1620
|
+
per_method=limit.per_method,
|
|
1621
|
+
methods=limit.methods,
|
|
1622
|
+
error_message=limit.error_message,
|
|
1623
|
+
exempt_when=limit.exempt_when,
|
|
1624
|
+
override_defaults=limit.override_defaults,
|
|
1625
|
+
deduct_when=limit.deduct_when,
|
|
1626
|
+
on_breach=limit.on_breach,
|
|
1627
|
+
cost=limit.cost,
|
|
1628
|
+
)(baseview.blueprint)
|
|
1629
|
+
|
|
1450
1630
|
def add_permissions_view(self, base_permissions, view_menu):
|
|
1451
1631
|
"""
|
|
1452
|
-
|
|
1632
|
+
Adds a permission on a view menu to the backend
|
|
1453
1633
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1634
|
+
:param base_permissions:
|
|
1635
|
+
list of permissions from view (all exposed methods):
|
|
1636
|
+
'can_add','can_edit' etc...
|
|
1637
|
+
:param view_menu:
|
|
1638
|
+
name of the view or menu to add
|
|
1459
1639
|
"""
|
|
1460
1640
|
view_menu_db = self.add_view_menu(view_menu)
|
|
1461
1641
|
perm_views = self.find_permissions_view_menu(view_menu_db)
|
|
@@ -1497,10 +1677,10 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1497
1677
|
|
|
1498
1678
|
def add_permissions_menu(self, view_menu_name):
|
|
1499
1679
|
"""
|
|
1500
|
-
|
|
1680
|
+
Adds menu_access to menu on permission_view_menu
|
|
1501
1681
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1682
|
+
:param view_menu_name:
|
|
1683
|
+
The menu name
|
|
1504
1684
|
"""
|
|
1505
1685
|
self.add_view_menu(view_menu_name)
|
|
1506
1686
|
pv = self.find_permission_view_menu("menu_access", view_menu_name)
|
|
@@ -1512,10 +1692,10 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1512
1692
|
|
|
1513
1693
|
def security_cleanup(self, baseviews, menus):
|
|
1514
1694
|
"""
|
|
1515
|
-
|
|
1695
|
+
Will cleanup all unused permissions from the database
|
|
1516
1696
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1697
|
+
:param baseviews: A list of BaseViews class
|
|
1698
|
+
:param menus: Menu class
|
|
1519
1699
|
"""
|
|
1520
1700
|
viewsmenus = self.get_all_view_menu()
|
|
1521
1701
|
roles = self.get_all_roles()
|
|
@@ -1587,9 +1767,9 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1587
1767
|
@staticmethod
|
|
1588
1768
|
def _update_del_transitions(state_transitions: Dict, baseviews: List) -> None:
|
|
1589
1769
|
"""
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1770
|
+
Mutates state_transitions, loop baseviews and prunes all
|
|
1771
|
+
views and permissions that are not to delete because references
|
|
1772
|
+
exist.
|
|
1593
1773
|
|
|
1594
1774
|
:param baseview:
|
|
1595
1775
|
:param state_transitions:
|
|
@@ -1603,16 +1783,18 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1603
1783
|
)
|
|
1604
1784
|
state_transitions["del_perms"].discard(permission)
|
|
1605
1785
|
|
|
1606
|
-
def create_state_transitions(
|
|
1786
|
+
def create_state_transitions(
|
|
1787
|
+
self, baseviews: List, menus: Optional[List[Any]]
|
|
1788
|
+
) -> Dict:
|
|
1607
1789
|
"""
|
|
1608
|
-
|
|
1790
|
+
Creates a Dict with all the necessary vm/permission transitions
|
|
1609
1791
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1792
|
+
Dict: {
|
|
1793
|
+
"add": {(<VM>, <PERM>): ((<VM>, PERM), ... )}
|
|
1794
|
+
"del_role_pvm": ((<VM>, <PERM>), ...)
|
|
1795
|
+
"del_views": (<VM>, ... )
|
|
1796
|
+
"del_perms": (<PERM>, ... )
|
|
1797
|
+
}
|
|
1616
1798
|
|
|
1617
1799
|
:param baseviews: List with all the registered BaseView, BaseApi
|
|
1618
1800
|
:param menus: List with all the menu entries
|
|
@@ -1659,12 +1841,14 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1659
1841
|
self._update_del_transitions(state_transitions, baseviews)
|
|
1660
1842
|
return state_transitions
|
|
1661
1843
|
|
|
1662
|
-
def security_converge(
|
|
1844
|
+
def security_converge(
|
|
1845
|
+
self, baseviews: List, menus: Optional[List[Any]], dry=False
|
|
1846
|
+
) -> Dict:
|
|
1663
1847
|
"""
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1848
|
+
Converges overridden permissions on all registered views/api
|
|
1849
|
+
will compute all necessary operations from `class_permissions_name`,
|
|
1850
|
+
`previous_class_permission_name`, method_permission_name`,
|
|
1851
|
+
`previous_method_permission_name` class attributes.
|
|
1668
1852
|
|
|
1669
1853
|
:param baseviews: List of registered views/apis
|
|
1670
1854
|
:param menus: List of menu items
|
|
@@ -1677,7 +1861,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1677
1861
|
if not state_transitions:
|
|
1678
1862
|
log.info("No state transitions found")
|
|
1679
1863
|
return dict()
|
|
1680
|
-
log.debug(
|
|
1864
|
+
log.debug("State transitions: %s", state_transitions)
|
|
1681
1865
|
roles = self.get_all_roles()
|
|
1682
1866
|
for role in roles:
|
|
1683
1867
|
permissions = list(role.permissions)
|
|
@@ -1716,7 +1900,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1716
1900
|
|
|
1717
1901
|
def find_register_user(self, registration_hash):
|
|
1718
1902
|
"""
|
|
1719
|
-
|
|
1903
|
+
Generic function to return user registration
|
|
1720
1904
|
"""
|
|
1721
1905
|
raise NotImplementedError
|
|
1722
1906
|
|
|
@@ -1724,51 +1908,65 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1724
1908
|
self, username, first_name, last_name, email, password="", hashed_password=""
|
|
1725
1909
|
):
|
|
1726
1910
|
"""
|
|
1727
|
-
|
|
1911
|
+
Generic function to add user registration
|
|
1728
1912
|
"""
|
|
1729
1913
|
raise NotImplementedError
|
|
1730
1914
|
|
|
1731
1915
|
def del_register_user(self, register_user):
|
|
1732
1916
|
"""
|
|
1733
|
-
|
|
1917
|
+
Generic function to delete user registration
|
|
1734
1918
|
"""
|
|
1735
1919
|
raise NotImplementedError
|
|
1736
1920
|
|
|
1737
1921
|
def get_user_by_id(self, pk):
|
|
1738
1922
|
"""
|
|
1739
|
-
|
|
1923
|
+
Generic function to return user by it's id (pk)
|
|
1740
1924
|
"""
|
|
1741
1925
|
raise NotImplementedError
|
|
1742
1926
|
|
|
1743
1927
|
def find_user(self, username=None, email=None):
|
|
1744
1928
|
"""
|
|
1745
|
-
|
|
1929
|
+
Generic function find a user by it's username or email
|
|
1746
1930
|
"""
|
|
1747
1931
|
raise NotImplementedError
|
|
1748
1932
|
|
|
1749
1933
|
def get_all_users(self):
|
|
1750
1934
|
"""
|
|
1751
|
-
|
|
1935
|
+
Generic function that returns all existing users
|
|
1752
1936
|
"""
|
|
1753
1937
|
raise NotImplementedError
|
|
1754
1938
|
|
|
1755
|
-
def
|
|
1939
|
+
def get_db_role_permissions(self, role_id: int) -> List[object]:
|
|
1756
1940
|
"""
|
|
1757
|
-
|
|
1941
|
+
Get all DB permissions from a role id
|
|
1942
|
+
"""
|
|
1943
|
+
raise NotImplementedError
|
|
1944
|
+
|
|
1945
|
+
def add_user(
|
|
1946
|
+
self,
|
|
1947
|
+
username: str,
|
|
1948
|
+
first_name: str,
|
|
1949
|
+
last_name: str,
|
|
1950
|
+
email: str,
|
|
1951
|
+
role,
|
|
1952
|
+
**kwargs: Any,
|
|
1953
|
+
):
|
|
1954
|
+
"""
|
|
1955
|
+
Generic function to create user
|
|
1758
1956
|
"""
|
|
1759
1957
|
raise NotImplementedError
|
|
1760
1958
|
|
|
1761
1959
|
def update_user(self, user):
|
|
1762
1960
|
"""
|
|
1763
|
-
|
|
1961
|
+
Generic function to update user
|
|
1764
1962
|
|
|
1765
|
-
|
|
1963
|
+
:param user: User model to update to database
|
|
1766
1964
|
"""
|
|
1767
1965
|
raise NotImplementedError
|
|
1768
1966
|
|
|
1769
1967
|
def count_users(self):
|
|
1770
1968
|
"""
|
|
1771
|
-
|
|
1969
|
+
Generic function to count the existing users
|
|
1772
1970
|
"""
|
|
1773
1971
|
raise NotImplementedError
|
|
1774
1972
|
|
|
@@ -1781,7 +1979,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1781
1979
|
def find_role(self, name):
|
|
1782
1980
|
raise NotImplementedError
|
|
1783
1981
|
|
|
1784
|
-
def add_role(self, name):
|
|
1982
|
+
def add_role(self, name, permissions=None):
|
|
1785
1983
|
raise NotImplementedError
|
|
1786
1984
|
|
|
1787
1985
|
def update_role(self, pk, name):
|
|
@@ -1790,6 +1988,20 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1790
1988
|
def get_all_roles(self):
|
|
1791
1989
|
raise NotImplementedError
|
|
1792
1990
|
|
|
1991
|
+
"""
|
|
1992
|
+
----------------------
|
|
1993
|
+
PRIMITIVES FOR Groups
|
|
1994
|
+
----------------------
|
|
1995
|
+
"""
|
|
1996
|
+
|
|
1997
|
+
def find_group(self, name: str):
|
|
1998
|
+
raise NotImplementedError
|
|
1999
|
+
|
|
2000
|
+
def add_group(
|
|
2001
|
+
self, name: str, label: str, description: str, roles=None, users=None
|
|
2002
|
+
):
|
|
2003
|
+
raise NotImplementedError
|
|
2004
|
+
|
|
1793
2005
|
"""
|
|
1794
2006
|
----------------------------
|
|
1795
2007
|
PRIMITIVES FOR PERMISSIONS
|
|
@@ -1798,19 +2010,19 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1798
2010
|
|
|
1799
2011
|
def get_public_role(self):
|
|
1800
2012
|
"""
|
|
1801
|
-
|
|
2013
|
+
returns all permissions from public role
|
|
1802
2014
|
"""
|
|
1803
2015
|
raise NotImplementedError
|
|
1804
2016
|
|
|
1805
2017
|
def get_public_permissions(self):
|
|
1806
2018
|
"""
|
|
1807
|
-
|
|
2019
|
+
returns all permissions from public role
|
|
1808
2020
|
"""
|
|
1809
2021
|
raise NotImplementedError
|
|
1810
2022
|
|
|
1811
2023
|
def find_permission(self, name):
|
|
1812
2024
|
"""
|
|
1813
|
-
|
|
2025
|
+
Finds and returns a Permission by name
|
|
1814
2026
|
"""
|
|
1815
2027
|
raise NotImplementedError
|
|
1816
2028
|
|
|
@@ -1823,25 +2035,25 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1823
2035
|
self, view_name: str, permission_name: str, role_ids: List[int]
|
|
1824
2036
|
) -> bool:
|
|
1825
2037
|
"""
|
|
1826
|
-
|
|
2038
|
+
Finds and returns permission views for a group of roles
|
|
1827
2039
|
"""
|
|
1828
2040
|
raise NotImplementedError
|
|
1829
2041
|
|
|
1830
2042
|
def add_permission(self, name):
|
|
1831
2043
|
"""
|
|
1832
|
-
|
|
2044
|
+
Adds a permission to the backend, model permission
|
|
1833
2045
|
|
|
1834
|
-
|
|
1835
|
-
|
|
2046
|
+
:param name:
|
|
2047
|
+
name of the permission: 'can_add','can_edit' etc...
|
|
1836
2048
|
"""
|
|
1837
2049
|
raise NotImplementedError
|
|
1838
2050
|
|
|
1839
2051
|
def del_permission(self, name):
|
|
1840
2052
|
"""
|
|
1841
|
-
|
|
2053
|
+
Deletes a permission from the backend, model permission
|
|
1842
2054
|
|
|
1843
|
-
|
|
1844
|
-
|
|
2055
|
+
:param name:
|
|
2056
|
+
name of the permission: 'can_add','can_edit' etc...
|
|
1845
2057
|
"""
|
|
1846
2058
|
raise NotImplementedError
|
|
1847
2059
|
|
|
@@ -1853,7 +2065,7 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1853
2065
|
|
|
1854
2066
|
def find_view_menu(self, name):
|
|
1855
2067
|
"""
|
|
1856
|
-
|
|
2068
|
+
Finds and returns a ViewMenu by name
|
|
1857
2069
|
"""
|
|
1858
2070
|
raise NotImplementedError
|
|
1859
2071
|
|
|
@@ -1862,18 +2074,18 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1862
2074
|
|
|
1863
2075
|
def add_view_menu(self, name):
|
|
1864
2076
|
"""
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2077
|
+
Adds a view or menu to the backend, model view_menu
|
|
2078
|
+
param name:
|
|
2079
|
+
name of the view menu to add
|
|
1868
2080
|
"""
|
|
1869
2081
|
raise NotImplementedError
|
|
1870
2082
|
|
|
1871
2083
|
def del_view_menu(self, name):
|
|
1872
2084
|
"""
|
|
1873
|
-
|
|
2085
|
+
Deletes a ViewMenu from the backend
|
|
1874
2086
|
|
|
1875
|
-
|
|
1876
|
-
|
|
2087
|
+
:param name:
|
|
2088
|
+
name of the ViewMenu
|
|
1877
2089
|
"""
|
|
1878
2090
|
raise NotImplementedError
|
|
1879
2091
|
|
|
@@ -1885,27 +2097,27 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1885
2097
|
|
|
1886
2098
|
def find_permission_view_menu(self, permission_name, view_menu_name):
|
|
1887
2099
|
"""
|
|
1888
|
-
|
|
2100
|
+
Finds and returns a PermissionView by names
|
|
1889
2101
|
"""
|
|
1890
2102
|
raise NotImplementedError
|
|
1891
2103
|
|
|
1892
2104
|
def find_permissions_view_menu(self, view_menu):
|
|
1893
2105
|
"""
|
|
1894
|
-
|
|
2106
|
+
Finds all permissions from ViewMenu, returns list of PermissionView
|
|
1895
2107
|
|
|
1896
|
-
|
|
1897
|
-
|
|
2108
|
+
:param view_menu: ViewMenu object
|
|
2109
|
+
:return: list of PermissionView objects
|
|
1898
2110
|
"""
|
|
1899
2111
|
raise NotImplementedError
|
|
1900
2112
|
|
|
1901
2113
|
def add_permission_view_menu(self, permission_name, view_menu_name):
|
|
1902
2114
|
"""
|
|
1903
|
-
|
|
2115
|
+
Adds a permission on a view or menu to the backend
|
|
1904
2116
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2117
|
+
:param permission_name:
|
|
2118
|
+
name of the permission to add: 'can_add','can_edit' etc...
|
|
2119
|
+
:param view_menu_name:
|
|
2120
|
+
name of the view menu to add
|
|
1909
2121
|
"""
|
|
1910
2122
|
raise NotImplementedError
|
|
1911
2123
|
|
|
@@ -1920,34 +2132,48 @@ class BaseSecurityManager(AbstractSecurityManager):
|
|
|
1920
2132
|
|
|
1921
2133
|
def add_permission_role(self, role, perm_view):
|
|
1922
2134
|
"""
|
|
1923
|
-
|
|
2135
|
+
Add permission-ViewMenu object to Role
|
|
1924
2136
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2137
|
+
:param role:
|
|
2138
|
+
The role object
|
|
2139
|
+
:param perm_view:
|
|
2140
|
+
The PermissionViewMenu object
|
|
1929
2141
|
"""
|
|
1930
2142
|
raise NotImplementedError
|
|
1931
2143
|
|
|
1932
2144
|
def del_permission_role(self, role, perm_view):
|
|
1933
2145
|
"""
|
|
1934
|
-
|
|
2146
|
+
Remove permission-ViewMenu object to Role
|
|
1935
2147
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2148
|
+
:param role:
|
|
2149
|
+
The role object
|
|
2150
|
+
:param perm_view:
|
|
2151
|
+
The PermissionViewMenu object
|
|
1940
2152
|
"""
|
|
1941
2153
|
raise NotImplementedError
|
|
1942
2154
|
|
|
1943
|
-
def
|
|
1944
|
-
|
|
2155
|
+
def export_roles(
|
|
2156
|
+
self, path: Optional[str] = None, indent: Optional[Union[int, str]] = None
|
|
2157
|
+
) -> None:
|
|
2158
|
+
"""Exports roles to JSON file."""
|
|
2159
|
+
raise NotImplementedError
|
|
1945
2160
|
|
|
1946
|
-
def
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2161
|
+
def import_roles(self, path: str) -> None:
|
|
2162
|
+
"""Imports roles from JSON file."""
|
|
2163
|
+
raise NotImplementedError
|
|
2164
|
+
|
|
2165
|
+
def load_user(self, pk: int) -> Any | None:
|
|
2166
|
+
user = self.get_user_by_id(int(pk))
|
|
2167
|
+
if user and user.is_active:
|
|
2168
|
+
return user
|
|
2169
|
+
|
|
2170
|
+
def load_user_jwt(self, _jwt_header, jwt_data):
|
|
2171
|
+
identity = jwt_data["sub"]
|
|
2172
|
+
user = self.load_user(identity)
|
|
2173
|
+
if user and user.is_active:
|
|
2174
|
+
# Set flask g.user to JWT user, we can't do it on before request
|
|
2175
|
+
g.user = user
|
|
2176
|
+
return user
|
|
1951
2177
|
|
|
1952
2178
|
@staticmethod
|
|
1953
2179
|
def before_request():
|