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.
Files changed (228) hide show
  1. flask_appbuilder/__init__.py +2 -3
  2. flask_appbuilder/_compat.py +0 -1
  3. flask_appbuilder/actions.py +14 -14
  4. flask_appbuilder/api/__init__.py +741 -527
  5. flask_appbuilder/api/convert.py +104 -98
  6. flask_appbuilder/api/manager.py +14 -8
  7. flask_appbuilder/api/schemas.py +12 -1
  8. flask_appbuilder/babel/manager.py +12 -16
  9. flask_appbuilder/base.py +353 -280
  10. flask_appbuilder/basemanager.py +1 -1
  11. flask_appbuilder/baseviews.py +241 -164
  12. flask_appbuilder/charts/jsontools.py +10 -10
  13. flask_appbuilder/charts/views.py +56 -60
  14. flask_appbuilder/cli.py +115 -70
  15. flask_appbuilder/const.py +52 -52
  16. flask_appbuilder/exceptions.py +67 -5
  17. flask_appbuilder/fields.py +32 -23
  18. flask_appbuilder/fieldwidgets.py +34 -27
  19. flask_appbuilder/filemanager.py +33 -45
  20. flask_appbuilder/filters.py +11 -13
  21. flask_appbuilder/forms.py +31 -35
  22. flask_appbuilder/hooks.py +90 -0
  23. flask_appbuilder/menu.py +35 -10
  24. flask_appbuilder/models/base.py +47 -57
  25. flask_appbuilder/models/decorators.py +13 -13
  26. flask_appbuilder/models/filters.py +42 -38
  27. flask_appbuilder/models/generic/__init__.py +29 -29
  28. flask_appbuilder/models/generic/filters.py +11 -3
  29. flask_appbuilder/models/generic/interface.py +1 -3
  30. flask_appbuilder/models/group.py +37 -39
  31. flask_appbuilder/models/mixins.py +22 -18
  32. flask_appbuilder/models/sqla/__init__.py +19 -72
  33. flask_appbuilder/models/sqla/base.py +24 -0
  34. flask_appbuilder/models/sqla/base_legacy.py +132 -0
  35. flask_appbuilder/models/sqla/filters.py +132 -19
  36. flask_appbuilder/models/sqla/interface.py +390 -276
  37. flask_appbuilder/security/api.py +31 -35
  38. flask_appbuilder/security/decorators.py +181 -83
  39. flask_appbuilder/security/forms.py +20 -31
  40. flask_appbuilder/security/manager.py +715 -489
  41. flask_appbuilder/security/registerviews.py +29 -112
  42. flask_appbuilder/security/schemas.py +43 -0
  43. flask_appbuilder/security/sqla/apis/__init__.py +8 -0
  44. flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
  45. flask_appbuilder/security/sqla/apis/group/api.py +227 -0
  46. flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
  47. flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
  48. flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
  49. flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
  50. flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
  51. flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
  52. flask_appbuilder/security/sqla/apis/role/api.py +306 -0
  53. flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
  54. flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
  55. flask_appbuilder/security/sqla/apis/user/api.py +292 -0
  56. flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
  57. flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
  58. flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
  59. flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
  60. flask_appbuilder/security/sqla/manager.py +421 -203
  61. flask_appbuilder/security/sqla/models.py +192 -57
  62. flask_appbuilder/security/utils.py +9 -0
  63. flask_appbuilder/security/views.py +232 -229
  64. flask_appbuilder/static/.DS_Store +0 -0
  65. flask_appbuilder/static/appbuilder/css/ab.css +20 -12
  66. flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
  67. flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
  68. flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
  69. flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
  70. flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
  71. flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
  72. flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
  73. flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
  74. flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
  75. flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
  76. flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
  77. flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
  78. flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
  79. flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
  80. flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
  81. flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
  82. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
  83. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
  84. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
  85. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
  86. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
  87. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
  88. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
  89. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
  90. flask_appbuilder/static/appbuilder/js/ab.js +33 -23
  91. flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
  92. flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
  93. flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
  94. flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
  95. flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
  96. flask_appbuilder/templates/appbuilder/baselib.html +9 -3
  97. flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
  98. flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
  99. flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
  100. flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
  101. flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
  102. flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
  103. flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
  104. flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
  105. flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
  106. flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
  107. flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
  108. flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
  109. flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
  110. flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
  111. flask_appbuilder/templates/appbuilder/init.html +37 -43
  112. flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
  113. flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
  114. flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
  115. flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
  116. flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
  117. flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
  118. flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
  119. flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
  120. flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
  121. flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
  122. flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
  123. flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
  124. flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
  125. flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
  126. flask_appbuilder/upload.py +20 -22
  127. flask_appbuilder/urltools.py +39 -19
  128. flask_appbuilder/utils/base.py +76 -0
  129. flask_appbuilder/utils/legacy.py +33 -0
  130. flask_appbuilder/utils/limit.py +20 -0
  131. flask_appbuilder/validators.py +73 -14
  132. flask_appbuilder/views.py +75 -424
  133. flask_appbuilder/widgets.py +50 -51
  134. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
  138. Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
  139. Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
  140. flask_appbuilder/console.py +0 -426
  141. flask_appbuilder/models/mongoengine/__init__.py +0 -0
  142. flask_appbuilder/models/mongoengine/fields.py +0 -65
  143. flask_appbuilder/models/mongoengine/filters.py +0 -145
  144. flask_appbuilder/models/mongoengine/interface.py +0 -328
  145. flask_appbuilder/security/mongoengine/__init__.py +0 -0
  146. flask_appbuilder/security/mongoengine/manager.py +0 -402
  147. flask_appbuilder/security/mongoengine/models.py +0 -120
  148. flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
  149. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
  150. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
  151. flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
  152. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
  153. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
  154. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
  155. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
  156. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
  157. flask_appbuilder/static/appbuilder/img/aol.png +0 -0
  158. flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
  159. flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
  160. flask_appbuilder/static/appbuilder/img/google.png +0 -0
  161. flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
  162. flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
  163. flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
  164. flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
  165. flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
  166. flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
  167. flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
  168. flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
  169. flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
  170. flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
  171. flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
  172. flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
  173. flask_appbuilder/tests/__init__.py +0 -0
  174. flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
  175. flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
  176. flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
  177. flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
  178. flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
  179. flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
  180. flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
  181. flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
  182. flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
  183. flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
  184. flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
  185. flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
  186. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
  187. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
  188. flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
  189. flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
  190. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
  191. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
  192. flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
  193. flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
  194. flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
  195. flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
  196. flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
  197. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
  198. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
  199. flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
  200. flask_appbuilder/tests/_test_auth_oauth.py +0 -419
  201. flask_appbuilder/tests/_test_ldapsearch.py +0 -135
  202. flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
  203. flask_appbuilder/tests/app.db +0 -0
  204. flask_appbuilder/tests/base.py +0 -90
  205. flask_appbuilder/tests/config_api.py +0 -21
  206. flask_appbuilder/tests/const.py +0 -9
  207. flask_appbuilder/tests/mongoengine/__init__.py +0 -0
  208. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
  209. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
  210. flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
  211. flask_appbuilder/tests/mongoengine/models.py +0 -41
  212. flask_appbuilder/tests/sqla/__init__.py +0 -0
  213. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
  214. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
  215. flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
  216. flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
  217. flask_appbuilder/tests/sqla/models.py +0 -340
  218. flask_appbuilder/tests/test_0_fixture.py +0 -39
  219. flask_appbuilder/tests/test_api.py +0 -2790
  220. flask_appbuilder/tests/test_fab_cli.py +0 -72
  221. flask_appbuilder/tests/test_menu.py +0 -122
  222. flask_appbuilder/tests/test_mongoengine.py +0 -572
  223. flask_appbuilder/tests/test_mvc.py +0 -1710
  224. flask_appbuilder/tests/test_sqlalchemy.py +0 -24
  225. flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
  226. flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
  227. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.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
- View for resetting own user password
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
- View for reseting all users password
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": lazy_gettext(
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 = ["first_name", "last_name", "username", "email", "active", "roles"]
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
- search_exclude_columns = ["password"]
208
-
209
- add_columns = ["first_name", "last_name", "username", "active", "email", "roles"]
210
- edit_columns = ["first_name", "last_name", "username", "active", "email", "roles"]
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
- View that add LDAP specifics to User view.
248
- Override to implement your own custom view.
249
- Then override userldapmodelview property on SecurityManager
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
- View that add OAUTH specifics to User view.
258
- Override to implement your own custom view.
259
- Then override userldapmodelview property on SecurityManager
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
- View that add REMOTE_USER specifics to User view.
268
- Override to implement your own custom view.
269
- Then override userldapmodelview property on SecurityManager
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
- View that add DB specifics to User view.
278
- Override to implement your own custom view.
279
- Then override userdbmodelview property on SecurityManager
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
- "Please use a good password policy,"
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
- EqualTo("password", message=lazy_gettext("Passwords must match"))
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(item.password)
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(self.appbuilder.get_url_for_index)
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.get_url_for_login)
595
+ return redirect(self.appbuilder.get_url_for_login_with(next_url))
498
596
  login_user(user, remember=False)
499
- return redirect(self.appbuilder.get_url_for_index)
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.get_url_for_login)
619
+ return redirect(self.appbuilder.get_url_for_login_with(next_url))
520
620
  login_user(user, remember=False)
521
- return redirect(self.appbuilder.get_url_for_index)
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
- @expose("/login/<provider>/<register>")
630
- def login(self, provider=None, register=None):
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 {0}".format(g.user))
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
- else:
643
- log.debug("Going to call authorize for: {0}".format(provider))
644
- state = jwt.encode(
645
- request.args.to_dict(flat=False),
646
- self.appbuilder.app.config["SECRET_KEY"],
647
- algorithm="HS256",
648
- )
649
- try:
650
- if register:
651
- log.debug("Login to Register")
652
- session["register"] = True
653
- if provider == "twitter":
654
- return self.appbuilder.sm.oauth_remotes[
655
- provider
656
- ].authorize_redirect(
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
- except Exception as e:
676
- log.error("Error on OAuth authorize: {0}".format(e))
677
- flash(as_unicode(self.invalid_login_message), "warning")
678
- return redirect(self.appbuilder.get_url_for_index)
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
- resp = self.appbuilder.sm.oauth_remotes[provider].authorize_access_token()
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(u"You denied the request to sign in.", "warning")
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: {0}".format(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: {0}".format(e))
696
+ log.error("Error returning OAuth user info: %s", e)
694
697
  user = None
695
698
  else:
696
- log.debug("User info retrieved from {0}: {1}".format(provider, userinfo))
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 e in whitelist:
702
- if re.search(e, userinfo["email"]):
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(u"You are not authorized.", "warning")
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
- raise Exception("State signature is not valid!")
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("REMOTE_USER")
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
- return redirect(self.appbuilder.get_url_for_index)
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
- return redirect(self.appbuilder.get_url_for_index)
752
+ next_url = request.args.get("next", "")
753
+ return redirect(get_safe_redirect(next_url))