flask-appbuilder 3.2.1__py3-none-any.whl → 5.0.2__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.1.dist-info → flask_appbuilder-5.0.2.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1.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.1.dist-info → flask_appbuilder-5.0.2.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
|
+
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
from flask import abort, current_app, flash, g, redirect, request, session, url_for
|
|
7
|
+
from flask_appbuilder._compat import as_unicode
|
|
8
|
+
from flask_appbuilder.actions import action
|
|
9
|
+
from flask_appbuilder.baseviews import BaseView
|
|
10
|
+
from flask_appbuilder.charts.views import DirectByChartView
|
|
11
|
+
from flask_appbuilder.exceptions import (
|
|
12
|
+
DeleteGroupWithUsersException,
|
|
13
|
+
DeleteRoleWithUsersException,
|
|
14
|
+
)
|
|
15
|
+
from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget
|
|
16
|
+
from flask_appbuilder.security.decorators import has_access, no_cache
|
|
17
|
+
from flask_appbuilder.security.forms import (
|
|
18
|
+
DynamicForm,
|
|
19
|
+
LoginForm_db,
|
|
20
|
+
ResetPasswordForm,
|
|
21
|
+
roles_or_groups_required,
|
|
22
|
+
UserInfoEdit,
|
|
23
|
+
)
|
|
24
|
+
from flask_appbuilder.security.utils import generate_random_string
|
|
25
|
+
from flask_appbuilder.utils.base import get_safe_redirect, lazy_formatter_gettext
|
|
26
|
+
from flask_appbuilder.validators import PasswordComplexityValidator
|
|
27
|
+
from flask_appbuilder.views import expose, ModelView, SimpleFormView
|
|
28
|
+
from flask_appbuilder.widgets import ListWidget, ShowWidget
|
|
6
29
|
from flask_babel import lazy_gettext
|
|
7
30
|
from flask_login import login_user, logout_user
|
|
8
31
|
import jwt
|
|
9
32
|
from werkzeug.security import generate_password_hash
|
|
33
|
+
from werkzeug.wrappers import Response as WerkzeugResponse
|
|
10
34
|
from wtforms import PasswordField, validators
|
|
11
35
|
from wtforms.validators import EqualTo
|
|
12
36
|
|
|
13
|
-
from .decorators import has_access
|
|
14
|
-
from .forms import LoginForm_db, LoginForm_oid, ResetPasswordForm, UserInfoEdit
|
|
15
|
-
from .._compat import as_unicode
|
|
16
|
-
from ..actions import action
|
|
17
|
-
from ..baseviews import BaseView
|
|
18
|
-
from ..charts.views import DirectByChartView
|
|
19
|
-
from ..fieldwidgets import BS3PasswordFieldWidget
|
|
20
|
-
from ..views import expose, ModelView, SimpleFormView
|
|
21
|
-
from ..widgets import ListWidget, ShowWidget
|
|
22
37
|
|
|
23
38
|
log = logging.getLogger(__name__)
|
|
24
39
|
|
|
@@ -65,7 +80,7 @@ class PermissionViewModelView(ModelView):
|
|
|
65
80
|
|
|
66
81
|
class ResetMyPasswordView(SimpleFormView):
|
|
67
82
|
"""
|
|
68
|
-
|
|
83
|
+
View for resetting own user password
|
|
69
84
|
"""
|
|
70
85
|
|
|
71
86
|
route_base = "/resetmypassword"
|
|
@@ -74,14 +89,14 @@ class ResetMyPasswordView(SimpleFormView):
|
|
|
74
89
|
redirect_url = "/"
|
|
75
90
|
message = lazy_gettext("Password Changed")
|
|
76
91
|
|
|
77
|
-
def form_post(self, form):
|
|
92
|
+
def form_post(self, form: DynamicForm) -> None:
|
|
78
93
|
self.appbuilder.sm.reset_password(g.user.id, form.password.data)
|
|
79
94
|
flash(as_unicode(self.message), "info")
|
|
80
95
|
|
|
81
96
|
|
|
82
97
|
class ResetPasswordView(SimpleFormView):
|
|
83
98
|
"""
|
|
84
|
-
|
|
99
|
+
View for reseting all users password
|
|
85
100
|
"""
|
|
86
101
|
|
|
87
102
|
route_base = "/resetpassword"
|
|
@@ -90,7 +105,7 @@ class ResetPasswordView(SimpleFormView):
|
|
|
90
105
|
redirect_url = "/"
|
|
91
106
|
message = lazy_gettext("Password Changed")
|
|
92
107
|
|
|
93
|
-
def form_post(self, form):
|
|
108
|
+
def form_post(self, form: DynamicForm) -> None:
|
|
94
109
|
pk = request.args.get("pk")
|
|
95
110
|
self.appbuilder.sm.reset_password(pk, form.password.data)
|
|
96
111
|
flash(as_unicode(self.message), "info")
|
|
@@ -102,7 +117,7 @@ class UserInfoEditView(SimpleFormView):
|
|
|
102
117
|
redirect_url = "/"
|
|
103
118
|
message = lazy_gettext("User information changed")
|
|
104
119
|
|
|
105
|
-
def form_get(self, form):
|
|
120
|
+
def form_get(self, form: DynamicForm) -> None:
|
|
106
121
|
item = self.appbuilder.sm.get_user_by_id(g.user.id)
|
|
107
122
|
# fills the form generic solution
|
|
108
123
|
for key, value in form.data.items():
|
|
@@ -111,7 +126,7 @@ class UserInfoEditView(SimpleFormView):
|
|
|
111
126
|
form_field = getattr(form, key)
|
|
112
127
|
form_field.data = getattr(item, key)
|
|
113
128
|
|
|
114
|
-
def form_post(self, form):
|
|
129
|
+
def form_post(self, form: DynamicForm) -> None:
|
|
115
130
|
form = self.form.refresh(request.form)
|
|
116
131
|
item = self.appbuilder.sm.get_user_by_id(g.user.id)
|
|
117
132
|
form.populate_obj(item)
|
|
@@ -119,6 +134,17 @@ class UserInfoEditView(SimpleFormView):
|
|
|
119
134
|
flash(as_unicode(self.message), "info")
|
|
120
135
|
|
|
121
136
|
|
|
137
|
+
def _roles_custom_formatter(string: str) -> str:
|
|
138
|
+
if current_app.config.get("AUTH_ROLES_SYNC_AT_LOGIN", False):
|
|
139
|
+
string += (
|
|
140
|
+
". <div class='alert alert-warning' role='alert'>"
|
|
141
|
+
"AUTH_ROLES_SYNC_AT_LOGIN is enabled, changes to this field will "
|
|
142
|
+
"not persist between user logins."
|
|
143
|
+
"</div>"
|
|
144
|
+
)
|
|
145
|
+
return string
|
|
146
|
+
|
|
147
|
+
|
|
122
148
|
class UserModelView(ModelView):
|
|
123
149
|
route_base = "/users"
|
|
124
150
|
|
|
@@ -136,6 +162,7 @@ class UserModelView(ModelView):
|
|
|
136
162
|
"active": lazy_gettext("Is Active?"),
|
|
137
163
|
"email": lazy_gettext("Email"),
|
|
138
164
|
"roles": lazy_gettext("Role"),
|
|
165
|
+
"groups": lazy_gettext("Groups"),
|
|
139
166
|
"last_login": lazy_gettext("Last login"),
|
|
140
167
|
"login_count": lazy_gettext("Login count"),
|
|
141
168
|
"fail_login_count": lazy_gettext("Failed login count"),
|
|
@@ -151,22 +178,33 @@ class UserModelView(ModelView):
|
|
|
151
178
|
"username": lazy_gettext(
|
|
152
179
|
"Username valid for authentication on DB or LDAP, unused for OID auth"
|
|
153
180
|
),
|
|
154
|
-
"password": lazy_gettext(
|
|
155
|
-
"Please use a good password policy,"
|
|
156
|
-
" this application does not check this for you"
|
|
157
|
-
),
|
|
181
|
+
"password": lazy_gettext("The user's password for authentication"),
|
|
158
182
|
"active": lazy_gettext(
|
|
159
183
|
"It's not a good policy to remove a user, just make it inactive"
|
|
160
184
|
),
|
|
161
185
|
"email": lazy_gettext("The user's email, this will also be used for OID auth"),
|
|
162
|
-
"roles":
|
|
186
|
+
"roles": lazy_formatter_gettext(
|
|
163
187
|
"The user role on the application,"
|
|
164
|
-
" this will associate with a list of permissions"
|
|
188
|
+
" this will associate with a list of permissions",
|
|
189
|
+
_roles_custom_formatter,
|
|
190
|
+
),
|
|
191
|
+
"groups": lazy_formatter_gettext(
|
|
192
|
+
"The user group on the application,"
|
|
193
|
+
" this will associate with a list of roles associated with the group",
|
|
194
|
+
_roles_custom_formatter,
|
|
165
195
|
),
|
|
166
196
|
"conf_password": lazy_gettext("Please rewrite the user's password to confirm"),
|
|
167
197
|
}
|
|
168
198
|
|
|
169
|
-
list_columns = [
|
|
199
|
+
list_columns = [
|
|
200
|
+
"first_name",
|
|
201
|
+
"last_name",
|
|
202
|
+
"username",
|
|
203
|
+
"email",
|
|
204
|
+
"active",
|
|
205
|
+
"roles",
|
|
206
|
+
"groups",
|
|
207
|
+
]
|
|
170
208
|
|
|
171
209
|
show_fieldsets = [
|
|
172
210
|
(
|
|
@@ -203,16 +241,48 @@ class UserModelView(ModelView):
|
|
|
203
241
|
{"fields": ["first_name", "last_name", "email"], "expanded": True},
|
|
204
242
|
),
|
|
205
243
|
]
|
|
244
|
+
search_columns = [
|
|
245
|
+
"first_name",
|
|
246
|
+
"last_name",
|
|
247
|
+
"username",
|
|
248
|
+
"email",
|
|
249
|
+
"active",
|
|
250
|
+
"roles",
|
|
251
|
+
"groups",
|
|
252
|
+
"created_on",
|
|
253
|
+
"changed_on",
|
|
254
|
+
"last_login",
|
|
255
|
+
"login_count",
|
|
256
|
+
"fail_login_count",
|
|
257
|
+
]
|
|
206
258
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
259
|
+
add_columns = [
|
|
260
|
+
"first_name",
|
|
261
|
+
"last_name",
|
|
262
|
+
"username",
|
|
263
|
+
"active",
|
|
264
|
+
"email",
|
|
265
|
+
"roles",
|
|
266
|
+
"groups",
|
|
267
|
+
]
|
|
268
|
+
edit_columns = [
|
|
269
|
+
"first_name",
|
|
270
|
+
"last_name",
|
|
271
|
+
"username",
|
|
272
|
+
"active",
|
|
273
|
+
"email",
|
|
274
|
+
"roles",
|
|
275
|
+
"groups",
|
|
276
|
+
]
|
|
211
277
|
user_info_title = lazy_gettext("Your user information")
|
|
278
|
+
validators_columns = {
|
|
279
|
+
"roles": [roles_or_groups_required],
|
|
280
|
+
"groups": [roles_or_groups_required],
|
|
281
|
+
}
|
|
212
282
|
|
|
213
283
|
@expose("/userinfo/")
|
|
214
284
|
@has_access
|
|
215
|
-
def userinfo(self):
|
|
285
|
+
def userinfo(self) -> WerkzeugResponse:
|
|
216
286
|
item = self.datamodel.get(g.user.id, self._base_filters)
|
|
217
287
|
widgets = self._get_show_widget(
|
|
218
288
|
g.user.id, item, show_fieldsets=self.user_show_fieldsets
|
|
@@ -226,27 +296,17 @@ class UserModelView(ModelView):
|
|
|
226
296
|
)
|
|
227
297
|
|
|
228
298
|
@action("userinfoedit", lazy_gettext("Edit User"), "", "fa-edit", multiple=False)
|
|
229
|
-
def userinfoedit(self, item):
|
|
299
|
+
def userinfoedit(self, item: Any) -> WerkzeugResponse:
|
|
230
300
|
return redirect(
|
|
231
301
|
url_for(self.appbuilder.sm.userinfoeditview.__name__ + ".this_form_get")
|
|
232
302
|
)
|
|
233
303
|
|
|
234
304
|
|
|
235
|
-
class UserOIDModelView(UserModelView):
|
|
236
|
-
"""
|
|
237
|
-
View that add OID specifics to User view.
|
|
238
|
-
Override to implement your own custom view.
|
|
239
|
-
Then override useroidmodelview property on SecurityManager
|
|
240
|
-
"""
|
|
241
|
-
|
|
242
|
-
pass
|
|
243
|
-
|
|
244
|
-
|
|
245
305
|
class UserLDAPModelView(UserModelView):
|
|
246
306
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
307
|
+
View that add LDAP specifics to User view.
|
|
308
|
+
Override to implement your own custom view.
|
|
309
|
+
Then override userldapmodelview property on SecurityManager
|
|
250
310
|
"""
|
|
251
311
|
|
|
252
312
|
pass
|
|
@@ -254,9 +314,9 @@ class UserLDAPModelView(UserModelView):
|
|
|
254
314
|
|
|
255
315
|
class UserOAuthModelView(UserModelView):
|
|
256
316
|
"""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
317
|
+
View that add OAUTH specifics to User view.
|
|
318
|
+
Override to implement your own custom view.
|
|
319
|
+
Then override userldapmodelview property on SecurityManager
|
|
260
320
|
"""
|
|
261
321
|
|
|
262
322
|
pass
|
|
@@ -264,9 +324,9 @@ class UserOAuthModelView(UserModelView):
|
|
|
264
324
|
|
|
265
325
|
class UserRemoteUserModelView(UserModelView):
|
|
266
326
|
"""
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
327
|
+
View that add REMOTE_USER specifics to User view.
|
|
328
|
+
Override to implement your own custom view.
|
|
329
|
+
Then override userldapmodelview property on SecurityManager
|
|
270
330
|
"""
|
|
271
331
|
|
|
272
332
|
pass
|
|
@@ -274,26 +334,24 @@ class UserRemoteUserModelView(UserModelView):
|
|
|
274
334
|
|
|
275
335
|
class UserDBModelView(UserModelView):
|
|
276
336
|
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
337
|
+
View that adds DB specifics to User view.
|
|
338
|
+
Override to implement your own custom view.
|
|
339
|
+
Then override userdbmodelview property on SecurityManager
|
|
280
340
|
"""
|
|
281
341
|
|
|
282
342
|
add_form_extra_fields = {
|
|
283
343
|
"password": PasswordField(
|
|
284
344
|
lazy_gettext("Password"),
|
|
285
|
-
description=lazy_gettext(
|
|
286
|
-
|
|
287
|
-
" this application does not check this for you"
|
|
288
|
-
),
|
|
289
|
-
validators=[validators.DataRequired()],
|
|
345
|
+
description=lazy_gettext("The user's password for authentication"),
|
|
346
|
+
validators=[validators.DataRequired(), PasswordComplexityValidator()],
|
|
290
347
|
widget=BS3PasswordFieldWidget(),
|
|
291
348
|
),
|
|
292
349
|
"conf_password": PasswordField(
|
|
293
350
|
lazy_gettext("Confirm Password"),
|
|
294
351
|
description=lazy_gettext("Please rewrite the user's password to confirm"),
|
|
295
352
|
validators=[
|
|
296
|
-
|
|
353
|
+
validators.DataRequired(),
|
|
354
|
+
EqualTo("password", message=lazy_gettext("Passwords must match")),
|
|
297
355
|
],
|
|
298
356
|
widget=BS3PasswordFieldWidget(),
|
|
299
357
|
),
|
|
@@ -306,13 +364,14 @@ class UserDBModelView(UserModelView):
|
|
|
306
364
|
"active",
|
|
307
365
|
"email",
|
|
308
366
|
"roles",
|
|
367
|
+
"groups",
|
|
309
368
|
"password",
|
|
310
369
|
"conf_password",
|
|
311
370
|
]
|
|
312
371
|
|
|
313
372
|
@expose("/show/<pk>", methods=["GET"])
|
|
314
373
|
@has_access
|
|
315
|
-
def show(self, pk):
|
|
374
|
+
def show(self, pk: Any) -> WerkzeugResponse:
|
|
316
375
|
actions = dict()
|
|
317
376
|
actions["resetpasswords"] = self.actions.get("resetpasswords")
|
|
318
377
|
item = self.datamodel.get(pk, self._base_filters)
|
|
@@ -331,7 +390,7 @@ class UserDBModelView(UserModelView):
|
|
|
331
390
|
|
|
332
391
|
@expose("/userinfo/")
|
|
333
392
|
@has_access
|
|
334
|
-
def userinfo(self):
|
|
393
|
+
def userinfo(self) -> WerkzeugResponse:
|
|
335
394
|
actions = dict()
|
|
336
395
|
actions["resetmypassword"] = self.actions.get("resetmypassword")
|
|
337
396
|
actions["userinfoedit"] = self.actions.get("userinfoedit")
|
|
@@ -355,7 +414,7 @@ class UserDBModelView(UserModelView):
|
|
|
355
414
|
"fa-lock",
|
|
356
415
|
multiple=False,
|
|
357
416
|
)
|
|
358
|
-
def resetmypassword(self, item):
|
|
417
|
+
def resetmypassword(self, item: Any):
|
|
359
418
|
return redirect(
|
|
360
419
|
url_for(self.appbuilder.sm.resetmypasswordview.__name__ + ".this_form_get")
|
|
361
420
|
)
|
|
@@ -363,7 +422,7 @@ class UserDBModelView(UserModelView):
|
|
|
363
422
|
@action(
|
|
364
423
|
"resetpasswords", lazy_gettext("Reset Password"), "", "fa-lock", multiple=False
|
|
365
424
|
)
|
|
366
|
-
def resetpasswords(self, item):
|
|
425
|
+
def resetpasswords(self, item: Any) -> WerkzeugResponse:
|
|
367
426
|
return redirect(
|
|
368
427
|
url_for(
|
|
369
428
|
self.appbuilder.sm.resetpasswordview.__name__ + ".this_form_get",
|
|
@@ -371,12 +430,16 @@ class UserDBModelView(UserModelView):
|
|
|
371
430
|
)
|
|
372
431
|
)
|
|
373
432
|
|
|
374
|
-
def pre_update(self, item):
|
|
433
|
+
def pre_update(self, item: Any) -> None:
|
|
375
434
|
item.changed_on = datetime.datetime.now()
|
|
376
435
|
item.changed_by_fk = g.user.id
|
|
377
436
|
|
|
378
|
-
def pre_add(self, item):
|
|
379
|
-
item.password = generate_password_hash(
|
|
437
|
+
def pre_add(self, item: Any) -> None:
|
|
438
|
+
item.password = generate_password_hash(
|
|
439
|
+
password=item.password,
|
|
440
|
+
method=current_app.config.get("FAB_PASSWORD_HASH_METHOD", "scrypt"),
|
|
441
|
+
salt_length=current_app.config.get("FAB_PASSWORD_HASH_SALT_LENGTH", 16),
|
|
442
|
+
)
|
|
380
443
|
|
|
381
444
|
|
|
382
445
|
class UserStatsChartView(DirectByChartView):
|
|
@@ -453,6 +516,35 @@ class RoleModelView(ModelView):
|
|
|
453
516
|
self.datamodel.add(new_role)
|
|
454
517
|
return redirect(self.get_redirect())
|
|
455
518
|
|
|
519
|
+
def pre_delete(self, item):
|
|
520
|
+
if item.user:
|
|
521
|
+
self.update_redirect()
|
|
522
|
+
raise DeleteRoleWithUsersException(
|
|
523
|
+
lazy_gettext("User(s) exists in the role, cannot delete")
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class UserGroupModelView(ModelView):
|
|
528
|
+
route_base = "/groups"
|
|
529
|
+
|
|
530
|
+
list_title = lazy_gettext("List Groups")
|
|
531
|
+
show_title = lazy_gettext("Show Group")
|
|
532
|
+
add_title = lazy_gettext("Add Group")
|
|
533
|
+
edit_title = lazy_gettext("Edit Group")
|
|
534
|
+
|
|
535
|
+
list_columns = ["name", "label", "roles"]
|
|
536
|
+
show_columns = ["name", "label", "description", "users", "roles"]
|
|
537
|
+
edit_columns = ["name", "label", "description", "users", "roles"]
|
|
538
|
+
add_columns = edit_columns
|
|
539
|
+
order_columns = ["name", "label", "description"]
|
|
540
|
+
|
|
541
|
+
def pre_delete(self, item):
|
|
542
|
+
if item.users:
|
|
543
|
+
self.update_redirect()
|
|
544
|
+
raise DeleteGroupWithUsersException(
|
|
545
|
+
lazy_gettext("User(s) exists in the group, cannot delete")
|
|
546
|
+
)
|
|
547
|
+
|
|
456
548
|
|
|
457
549
|
class RegisterUserModelView(ModelView):
|
|
458
550
|
route_base = "/registeruser"
|
|
@@ -477,26 +569,32 @@ class AuthView(BaseView):
|
|
|
477
569
|
@expose("/logout/")
|
|
478
570
|
def logout(self):
|
|
479
571
|
logout_user()
|
|
480
|
-
return redirect(
|
|
572
|
+
return redirect(
|
|
573
|
+
current_app.config.get(
|
|
574
|
+
"LOGOUT_REDIRECT_URL", self.appbuilder.get_url_for_index
|
|
575
|
+
)
|
|
576
|
+
)
|
|
481
577
|
|
|
482
578
|
|
|
483
579
|
class AuthDBView(AuthView):
|
|
484
580
|
login_template = "appbuilder/general/security/login_db.html"
|
|
485
581
|
|
|
486
582
|
@expose("/login/", methods=["GET", "POST"])
|
|
583
|
+
@no_cache
|
|
487
584
|
def login(self):
|
|
488
585
|
if g.user is not None and g.user.is_authenticated:
|
|
489
586
|
return redirect(self.appbuilder.get_url_for_index)
|
|
490
587
|
form = LoginForm_db()
|
|
491
588
|
if form.validate_on_submit():
|
|
589
|
+
next_url = get_safe_redirect(request.args.get("next", ""))
|
|
492
590
|
user = self.appbuilder.sm.auth_user_db(
|
|
493
591
|
form.username.data, form.password.data
|
|
494
592
|
)
|
|
495
593
|
if not user:
|
|
496
594
|
flash(as_unicode(self.invalid_login_message), "warning")
|
|
497
|
-
return redirect(self.appbuilder.
|
|
595
|
+
return redirect(self.appbuilder.get_url_for_login_with(next_url))
|
|
498
596
|
login_user(user, remember=False)
|
|
499
|
-
return redirect(
|
|
597
|
+
return redirect(next_url)
|
|
500
598
|
return self.render_template(
|
|
501
599
|
self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
|
|
502
600
|
)
|
|
@@ -506,132 +604,37 @@ class AuthLDAPView(AuthView):
|
|
|
506
604
|
login_template = "appbuilder/general/security/login_ldap.html"
|
|
507
605
|
|
|
508
606
|
@expose("/login/", methods=["GET", "POST"])
|
|
607
|
+
@no_cache
|
|
509
608
|
def login(self):
|
|
510
609
|
if g.user is not None and g.user.is_authenticated:
|
|
511
610
|
return redirect(self.appbuilder.get_url_for_index)
|
|
512
611
|
form = LoginForm_db()
|
|
513
612
|
if form.validate_on_submit():
|
|
613
|
+
next_url = get_safe_redirect(request.args.get("next", ""))
|
|
514
614
|
user = self.appbuilder.sm.auth_user_ldap(
|
|
515
615
|
form.username.data, form.password.data
|
|
516
616
|
)
|
|
517
617
|
if not user:
|
|
518
618
|
flash(as_unicode(self.invalid_login_message), "warning")
|
|
519
|
-
return redirect(self.appbuilder.
|
|
619
|
+
return redirect(self.appbuilder.get_url_for_login_with(next_url))
|
|
520
620
|
login_user(user, remember=False)
|
|
521
|
-
return redirect(
|
|
621
|
+
return redirect(next_url)
|
|
522
622
|
return self.render_template(
|
|
523
623
|
self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
|
|
524
624
|
)
|
|
525
625
|
|
|
526
|
-
"""
|
|
527
|
-
For Future Use, API Auth, must check howto keep REST stateless
|
|
528
|
-
"""
|
|
529
|
-
|
|
530
|
-
"""
|
|
531
|
-
@expose_api(name='auth',url='/api/auth')
|
|
532
|
-
def auth(self):
|
|
533
|
-
if g.user is not None and g.user.is_authenticated:
|
|
534
|
-
http_return_code = 401
|
|
535
|
-
response = make_response(
|
|
536
|
-
jsonify(
|
|
537
|
-
{
|
|
538
|
-
'message': 'Login Failed already authenticated',
|
|
539
|
-
'severity': 'critical'
|
|
540
|
-
}
|
|
541
|
-
),
|
|
542
|
-
http_return_code
|
|
543
|
-
)
|
|
544
|
-
username = str(request.args.get('username'))
|
|
545
|
-
password = str(request.args.get('password'))
|
|
546
|
-
user = self.appbuilder.sm.auth_user_ldap(username, password)
|
|
547
|
-
if not user:
|
|
548
|
-
http_return_code = 401
|
|
549
|
-
response = make_response(
|
|
550
|
-
jsonify(
|
|
551
|
-
{
|
|
552
|
-
'message': 'Login Failed',
|
|
553
|
-
'severity': 'critical'
|
|
554
|
-
}
|
|
555
|
-
),
|
|
556
|
-
http_return_code
|
|
557
|
-
)
|
|
558
|
-
else:
|
|
559
|
-
login_user(user, remember=False)
|
|
560
|
-
http_return_code = 201
|
|
561
|
-
response = make_response(
|
|
562
|
-
jsonify(
|
|
563
|
-
{
|
|
564
|
-
'message': 'Login Success',
|
|
565
|
-
'severity': 'info'
|
|
566
|
-
}
|
|
567
|
-
),
|
|
568
|
-
http_return_code
|
|
569
|
-
)
|
|
570
|
-
return response
|
|
571
|
-
"""
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
class AuthOIDView(AuthView):
|
|
575
|
-
login_template = "appbuilder/general/security/login_oid.html"
|
|
576
|
-
oid_ask_for = ["email"]
|
|
577
|
-
oid_ask_for_optional = []
|
|
578
|
-
|
|
579
|
-
def __init__(self):
|
|
580
|
-
super(AuthOIDView, self).__init__()
|
|
581
|
-
|
|
582
|
-
@expose("/login/", methods=["GET", "POST"])
|
|
583
|
-
def login(self, flag=True):
|
|
584
|
-
@self.appbuilder.sm.oid.loginhandler
|
|
585
|
-
def login_handler(self):
|
|
586
|
-
if g.user is not None and g.user.is_authenticated:
|
|
587
|
-
return redirect(self.appbuilder.get_url_for_index)
|
|
588
|
-
form = LoginForm_oid()
|
|
589
|
-
if form.validate_on_submit():
|
|
590
|
-
session["remember_me"] = form.remember_me.data
|
|
591
|
-
return self.appbuilder.sm.oid.try_login(
|
|
592
|
-
form.openid.data,
|
|
593
|
-
ask_for=self.oid_ask_for,
|
|
594
|
-
ask_for_optional=self.oid_ask_for_optional,
|
|
595
|
-
)
|
|
596
|
-
return self.render_template(
|
|
597
|
-
self.login_template,
|
|
598
|
-
title=self.title,
|
|
599
|
-
form=form,
|
|
600
|
-
providers=self.appbuilder.sm.openid_providers,
|
|
601
|
-
appbuilder=self.appbuilder,
|
|
602
|
-
)
|
|
603
|
-
|
|
604
|
-
@self.appbuilder.sm.oid.after_login
|
|
605
|
-
def after_login(resp):
|
|
606
|
-
if resp.email is None or resp.email == "":
|
|
607
|
-
flash(as_unicode(self.invalid_login_message), "warning")
|
|
608
|
-
return redirect(self.appbuilder.get_url_for_login)
|
|
609
|
-
user = self.appbuilder.sm.auth_user_oid(resp.email)
|
|
610
|
-
if user is None:
|
|
611
|
-
flash(as_unicode(self.invalid_login_message), "warning")
|
|
612
|
-
return redirect(self.appbuilder.get_url_for_login)
|
|
613
|
-
remember_me = False
|
|
614
|
-
if "remember_me" in session:
|
|
615
|
-
remember_me = session["remember_me"]
|
|
616
|
-
session.pop("remember_me", None)
|
|
617
|
-
|
|
618
|
-
login_user(user, remember=remember_me)
|
|
619
|
-
return redirect(self.appbuilder.get_url_for_index)
|
|
620
|
-
|
|
621
|
-
return login_handler(self)
|
|
622
|
-
|
|
623
626
|
|
|
624
627
|
class AuthOAuthView(AuthView):
|
|
625
628
|
login_template = "appbuilder/general/security/login_oauth.html"
|
|
626
629
|
|
|
627
630
|
@expose("/login/")
|
|
628
631
|
@expose("/login/<provider>")
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
log.debug("Provider: {0}".format(provider))
|
|
632
|
+
def login(self, provider: Optional[str] = None) -> WerkzeugResponse:
|
|
633
|
+
log.debug("Provider: %s", provider)
|
|
632
634
|
if g.user is not None and g.user.is_authenticated:
|
|
633
|
-
log.debug("Already authenticated
|
|
635
|
+
log.debug("Already authenticated %s", g.user)
|
|
634
636
|
return redirect(self.appbuilder.get_url_for_index)
|
|
637
|
+
|
|
635
638
|
if provider is None:
|
|
636
639
|
return self.render_template(
|
|
637
640
|
self.login_template,
|
|
@@ -639,71 +642,71 @@ class AuthOAuthView(AuthView):
|
|
|
639
642
|
title=self.title,
|
|
640
643
|
appbuilder=self.appbuilder,
|
|
641
644
|
)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
redirect_uri=url_for(
|
|
658
|
-
".oauth_authorized",
|
|
659
|
-
provider=provider,
|
|
660
|
-
_external=True,
|
|
661
|
-
state=state,
|
|
662
|
-
)
|
|
663
|
-
)
|
|
664
|
-
else:
|
|
665
|
-
return self.appbuilder.sm.oauth_remotes[
|
|
666
|
-
provider
|
|
667
|
-
].authorize_redirect(
|
|
668
|
-
redirect_uri=url_for(
|
|
669
|
-
".oauth_authorized", provider=provider, _external=True
|
|
670
|
-
),
|
|
671
|
-
state=state.decode("ascii")
|
|
672
|
-
if isinstance(state, bytes)
|
|
673
|
-
else state,
|
|
645
|
+
|
|
646
|
+
log.debug("Going to call authorize for: %s", provider)
|
|
647
|
+
random_state = generate_random_string()
|
|
648
|
+
state = jwt.encode(
|
|
649
|
+
request.args.to_dict(flat=False), random_state, algorithm="HS256"
|
|
650
|
+
)
|
|
651
|
+
session["oauth_state"] = random_state
|
|
652
|
+
try:
|
|
653
|
+
if provider == "twitter":
|
|
654
|
+
return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
|
|
655
|
+
redirect_uri=url_for(
|
|
656
|
+
".oauth_authorized",
|
|
657
|
+
provider=provider,
|
|
658
|
+
_external=True,
|
|
659
|
+
state=state,
|
|
674
660
|
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
|
|
664
|
+
redirect_uri=url_for(
|
|
665
|
+
".oauth_authorized", provider=provider, _external=True
|
|
666
|
+
),
|
|
667
|
+
state=state.decode("ascii") if isinstance(state, bytes) else state,
|
|
668
|
+
)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
log.error("Error on OAuth authorize: %s", e)
|
|
671
|
+
flash(as_unicode(self.invalid_login_message), "warning")
|
|
672
|
+
return redirect(self.appbuilder.get_url_for_index)
|
|
679
673
|
|
|
680
674
|
@expose("/oauth-authorized/<provider>")
|
|
681
|
-
def oauth_authorized(self, provider):
|
|
675
|
+
def oauth_authorized(self, provider: str) -> WerkzeugResponse:
|
|
682
676
|
log.debug("Authorized init")
|
|
683
|
-
|
|
677
|
+
if provider not in self.appbuilder.sm.oauth_remotes:
|
|
678
|
+
flash("Provider not supported.", "warning")
|
|
679
|
+
log.warning("OAuth authorized got an unknown provider %s", provider)
|
|
680
|
+
return redirect(self.appbuilder.get_url_for_login)
|
|
681
|
+
try:
|
|
682
|
+
resp = self.appbuilder.sm.oauth_remotes[provider].authorize_access_token()
|
|
683
|
+
except Exception as e:
|
|
684
|
+
log.error("Error authorizing OAuth access token: %s", e)
|
|
685
|
+
flash("The request to sign in was denied.", "error")
|
|
686
|
+
return redirect(self.appbuilder.get_url_for_login)
|
|
684
687
|
if resp is None:
|
|
685
|
-
flash(
|
|
688
|
+
flash("You denied the request to sign in.", "warning")
|
|
686
689
|
return redirect(self.appbuilder.get_url_for_login)
|
|
687
|
-
log.debug("OAUTH Authorized resp:
|
|
690
|
+
log.debug("OAUTH Authorized resp: %s", resp)
|
|
688
691
|
# Retrieves specific user info from the provider
|
|
689
692
|
try:
|
|
690
693
|
self.appbuilder.sm.set_oauth_session(provider, resp)
|
|
691
694
|
userinfo = self.appbuilder.sm.oauth_user_info(provider, resp)
|
|
692
695
|
except Exception as e:
|
|
693
|
-
log.error("Error returning OAuth user info:
|
|
696
|
+
log.error("Error returning OAuth user info: %s", e)
|
|
694
697
|
user = None
|
|
695
698
|
else:
|
|
696
|
-
log.debug("User info retrieved from
|
|
699
|
+
log.debug("User info retrieved from %s: %s", provider, userinfo)
|
|
697
700
|
# User email is not whitelisted
|
|
698
701
|
if provider in self.appbuilder.sm.oauth_whitelists:
|
|
699
702
|
whitelist = self.appbuilder.sm.oauth_whitelists[provider]
|
|
700
703
|
allow = False
|
|
701
|
-
for
|
|
702
|
-
if re.search(
|
|
704
|
+
for email in whitelist:
|
|
705
|
+
if "email" in userinfo and re.search(email, userinfo["email"]):
|
|
703
706
|
allow = True
|
|
704
707
|
break
|
|
705
708
|
if not allow:
|
|
706
|
-
flash(
|
|
709
|
+
flash("You are not authorized.", "warning")
|
|
707
710
|
return redirect(self.appbuilder.get_url_for_login)
|
|
708
711
|
else:
|
|
709
712
|
log.debug("No whitelist for OAuth provider")
|
|
@@ -713,21 +716,19 @@ class AuthOAuthView(AuthView):
|
|
|
713
716
|
flash(as_unicode(self.invalid_login_message), "warning")
|
|
714
717
|
return redirect(self.appbuilder.get_url_for_login)
|
|
715
718
|
else:
|
|
716
|
-
login_user(user)
|
|
717
719
|
try:
|
|
718
720
|
state = jwt.decode(
|
|
719
|
-
request.args["state"],
|
|
720
|
-
self.appbuilder.app.config["SECRET_KEY"],
|
|
721
|
-
algorithms=["HS256"],
|
|
721
|
+
request.args["state"], session["oauth_state"], algorithms=["HS256"]
|
|
722
722
|
)
|
|
723
|
-
except jwt.InvalidTokenError:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
try:
|
|
727
|
-
next_url = state["next"][0] or self.appbuilder.get_url_for_index
|
|
728
|
-
except (KeyError, IndexError):
|
|
729
|
-
next_url = self.appbuilder.get_url_for_index
|
|
723
|
+
except (jwt.InvalidTokenError, KeyError):
|
|
724
|
+
flash(as_unicode("Invalid state signature"), "warning")
|
|
725
|
+
return redirect(self.appbuilder.get_url_for_login)
|
|
730
726
|
|
|
727
|
+
login_user(user)
|
|
728
|
+
next_url = self.appbuilder.get_url_for_index
|
|
729
|
+
# Check if there is a next url on state
|
|
730
|
+
if "next" in state and len(state["next"]) > 0:
|
|
731
|
+
next_url = get_safe_redirect(state["next"][0])
|
|
731
732
|
return redirect(next_url)
|
|
732
733
|
|
|
733
734
|
|
|
@@ -735,10 +736,11 @@ class AuthRemoteUserView(AuthView):
|
|
|
735
736
|
login_template = ""
|
|
736
737
|
|
|
737
738
|
@expose("/login/")
|
|
738
|
-
def login(self):
|
|
739
|
-
username = request.environ.get(
|
|
739
|
+
def login(self) -> WerkzeugResponse:
|
|
740
|
+
username = request.environ.get(self.appbuilder.sm.auth_remote_user_env_var)
|
|
740
741
|
if g.user is not None and g.user.is_authenticated:
|
|
741
|
-
|
|
742
|
+
next_url = request.args.get("next", "")
|
|
743
|
+
return redirect(get_safe_redirect(next_url))
|
|
742
744
|
if username:
|
|
743
745
|
user = self.appbuilder.sm.auth_user_remote_user(username)
|
|
744
746
|
if user is None:
|
|
@@ -747,4 +749,5 @@ class AuthRemoteUserView(AuthView):
|
|
|
747
749
|
login_user(user)
|
|
748
750
|
else:
|
|
749
751
|
flash(as_unicode(self.invalid_login_message), "warning")
|
|
750
|
-
|
|
752
|
+
next_url = request.args.get("next", "")
|
|
753
|
+
return redirect(get_safe_redirect(next_url))
|