flask-appbuilder 3.2.1rc1__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.
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.2.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2.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.2.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2.dist-info}/top_level.txt +0 -0
@@ -1,28 +1,24 @@
1
- from flask import request
2
- from flask_jwt_extended import (
3
- create_access_token,
4
- create_refresh_token,
5
- get_jwt_identity,
6
- jwt_refresh_token_required,
7
- )
8
-
9
- from ..api import BaseApi, safe
10
- from ..const import (
1
+ from flask import request, Response
2
+ from flask_appbuilder.api import BaseApi, safe
3
+ from flask_appbuilder.const import (
11
4
  API_SECURITY_ACCESS_TOKEN_KEY,
12
- API_SECURITY_PASSWORD_KEY,
13
5
  API_SECURITY_PROVIDER_DB,
14
- API_SECURITY_PROVIDER_KEY,
15
6
  API_SECURITY_PROVIDER_LDAP,
16
- API_SECURITY_REFRESH_KEY,
17
7
  API_SECURITY_REFRESH_TOKEN_KEY,
18
- API_SECURITY_USERNAME_KEY,
19
8
  API_SECURITY_VERSION,
20
9
  )
21
- from ..views import expose
10
+ from flask_appbuilder.security.schemas import login_post
11
+ from flask_appbuilder.views import expose
12
+ from flask_jwt_extended import (
13
+ create_access_token,
14
+ create_refresh_token,
15
+ get_jwt_identity,
16
+ jwt_required,
17
+ )
18
+ from marshmallow import ValidationError
22
19
 
23
20
 
24
21
  class SecurityApi(BaseApi):
25
-
26
22
  resource_name = "security"
27
23
  version = API_SECURITY_VERSION
28
24
  openapi_spec_tag = "Security"
@@ -35,7 +31,7 @@ class SecurityApi(BaseApi):
35
31
 
36
32
  @expose("/login", methods=["POST"])
37
33
  @safe
38
- def login(self):
34
+ def login(self) -> Response:
39
35
  """Login endpoint for the API returns a JWT and optionally a refresh token
40
36
  ---
41
37
  post:
@@ -88,20 +84,20 @@ class SecurityApi(BaseApi):
88
84
  """
89
85
  if not request.is_json:
90
86
  return self.response_400(message="Request payload is not JSON")
91
- username = request.json.get(API_SECURITY_USERNAME_KEY, None)
92
- password = request.json.get(API_SECURITY_PASSWORD_KEY, None)
93
- provider = request.json.get(API_SECURITY_PROVIDER_KEY, None)
94
- refresh = request.json.get(API_SECURITY_REFRESH_KEY, False)
95
- if not username or not password or not provider:
96
- return self.response_400(message="Missing required parameter")
87
+ try:
88
+ login_payload = login_post.load(request.json)
89
+ except ValidationError as error:
90
+ return self.response_400(message=error.messages)
91
+
97
92
  # AUTH
98
- if provider == API_SECURITY_PROVIDER_DB:
99
- user = self.appbuilder.sm.auth_user_db(username, password)
100
- elif provider == API_SECURITY_PROVIDER_LDAP:
101
- user = self.appbuilder.sm.auth_user_ldap(username, password)
102
- else:
103
- return self.response_400(
104
- message="Provider {} not supported".format(provider)
93
+ user = None
94
+ if login_payload["provider"] == API_SECURITY_PROVIDER_DB:
95
+ user = self.appbuilder.sm.auth_user_db(
96
+ login_payload["username"], login_payload["password"]
97
+ )
98
+ elif login_payload["provider"] == API_SECURITY_PROVIDER_LDAP:
99
+ user = self.appbuilder.sm.auth_user_ldap(
100
+ login_payload["username"], login_payload["password"]
105
101
  )
106
102
  if not user:
107
103
  return self.response_401()
@@ -109,18 +105,18 @@ class SecurityApi(BaseApi):
109
105
  # Identity can be any data that is json serializable
110
106
  resp = dict()
111
107
  resp[API_SECURITY_ACCESS_TOKEN_KEY] = create_access_token(
112
- identity=user.id, fresh=True
108
+ identity=str(user.id), fresh=True
113
109
  )
114
- if refresh:
110
+ if "refresh" in login_payload and login_payload["refresh"]:
115
111
  resp[API_SECURITY_REFRESH_TOKEN_KEY] = create_refresh_token(
116
- identity=user.id
112
+ identity=str(user.id)
117
113
  )
118
114
  return self.response(200, **resp)
119
115
 
120
116
  @expose("/refresh", methods=["POST"])
121
- @jwt_refresh_token_required
117
+ @jwt_required(refresh=True)
122
118
  @safe
123
- def refresh(self):
119
+ def refresh(self) -> Response:
124
120
  """
125
121
  Security endpoint for the refresh token, so we can obtain a new
126
122
  token without forcing the user to login again
@@ -1,42 +1,80 @@
1
1
  import functools
2
2
  import logging
3
-
4
- from flask import current_app, flash, jsonify, make_response, redirect, request, url_for
5
- from flask_jwt_extended import verify_jwt_in_request
6
- from flask_login import current_user
7
-
8
- from .._compat import as_unicode
9
- from ..const import (
3
+ from typing import Callable, List, Optional, TypeVar, Union
4
+
5
+ from flask import (
6
+ current_app,
7
+ flash,
8
+ jsonify,
9
+ make_response,
10
+ redirect,
11
+ request,
12
+ Response,
13
+ url_for,
14
+ )
15
+ from flask_appbuilder._compat import as_unicode
16
+ from flask_appbuilder.const import (
10
17
  FLAMSG_ERR_SEC_ACCESS_DENIED,
11
18
  LOGMSG_ERR_SEC_ACCESS_DENIED,
12
19
  PERMISSION_PREFIX,
13
20
  )
21
+ from flask_appbuilder.utils.limit import Limit
22
+ from flask_jwt_extended import verify_jwt_in_request
23
+ from flask_limiter import RequestLimit
24
+ from flask_login import current_user
25
+ from typing_extensions import ParamSpec
14
26
 
15
27
  log = logging.getLogger(__name__)
16
28
 
29
+ R = TypeVar("R")
30
+ P = ParamSpec("P")
31
+
32
+
33
+ def no_cache(view: Callable[..., Response]) -> Callable[..., Response]:
34
+ @functools.wraps(view)
35
+ def wrapped_view(*args, **kwargs) -> Response:
36
+ response = make_response(view(*args, **kwargs))
37
+ response.headers[
38
+ "Cache-Control"
39
+ ] = "no-store, no-cache, must-revalidate, max-age=0"
40
+ response.headers["Pragma"] = "no-cache"
41
+ response.headers["Expires"] = "0"
42
+ return response
43
+
44
+ return wrapped_view
45
+
46
+
47
+ def response_unauthorized_mvc(status_code: int) -> Response:
48
+ response = make_response(
49
+ jsonify({"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}),
50
+ status_code,
51
+ )
52
+ response.headers["Content-Type"] = "application/json"
53
+ return response
54
+
17
55
 
18
56
  def protect(allow_browser_login=False):
19
57
  """
20
- Use this decorator to enable granular security permissions
21
- to your API methods (BaseApi and child classes).
22
- Permissions will be associated to a role, and roles are associated to users.
23
-
24
- allow_browser_login will accept signed cookies obtained from the normal MVC app::
25
-
26
- class MyApi(BaseApi):
27
- @expose('/dosonmething', methods=['GET'])
28
- @protect(allow_browser_login=True)
29
- @safe
30
- def do_something(self):
31
- ....
32
-
33
- @expose('/dosonmethingelse', methods=['GET'])
34
- @protect()
35
- @safe
36
- def do_something_else(self):
37
- ....
38
-
39
- By default the permission's name is the methods name.
58
+ Use this decorator to enable granular security permissions
59
+ to your API methods (BaseApi and child classes).
60
+ Permissions will be associated to a role, and roles are associated to users.
61
+
62
+ allow_browser_login will accept signed cookies obtained from the normal MVC app::
63
+
64
+ class MyApi(BaseApi):
65
+ @expose('/dosonmething', methods=['GET'])
66
+ @protect(allow_browser_login=True)
67
+ @safe
68
+ def do_something(self):
69
+ ....
70
+
71
+ @expose('/dosonmethingelse', methods=['GET'])
72
+ @protect()
73
+ @safe
74
+ def do_something_else(self):
75
+ ....
76
+
77
+ By default the permission's name is the methods name.
40
78
  """
41
79
 
42
80
  def _protect(f):
@@ -47,25 +85,31 @@ def protect(allow_browser_login=False):
47
85
 
48
86
  def wraps(self, *args, **kwargs):
49
87
  # Apply method permission name override if exists
50
- permission_str = "{}{}".format(PERMISSION_PREFIX, f._permission_name)
88
+ permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
51
89
  if self.method_permission_name:
52
90
  _permission_name = self.method_permission_name.get(f.__name__)
53
91
  if _permission_name:
54
- permission_str = "{}{}".format(PERMISSION_PREFIX, _permission_name)
92
+ permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
55
93
  class_permission_name = self.class_permission_name
94
+ # Check if permission is allowed on the class
56
95
  if permission_str not in self.base_permissions:
57
- return self.response_401()
96
+ return self.response_403()
97
+ # Check if the resource is public
58
98
  if current_app.appbuilder.sm.is_item_public(
59
99
  permission_str, class_permission_name
60
100
  ):
61
101
  return f(self, *args, **kwargs)
102
+ # if no browser login then verify JWT
62
103
  if not (self.allow_browser_login or allow_browser_login):
63
104
  verify_jwt_in_request()
105
+ # Verify resource access
64
106
  if current_app.appbuilder.sm.has_access(
65
107
  permission_str, class_permission_name
66
108
  ):
67
109
  return f(self, *args, **kwargs)
110
+ # If browser login?
68
111
  elif self.allow_browser_login or allow_browser_login:
112
+ # no session cookie (but we allow it), then try JWT
69
113
  if not current_user.is_authenticated:
70
114
  verify_jwt_in_request()
71
115
  if current_app.appbuilder.sm.has_access(
@@ -73,11 +117,9 @@ def protect(allow_browser_login=False):
73
117
  ):
74
118
  return f(self, *args, **kwargs)
75
119
  log.warning(
76
- LOGMSG_ERR_SEC_ACCESS_DENIED.format(
77
- permission_str, class_permission_name
78
- )
120
+ LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, class_permission_name
79
121
  )
80
- return self.response_401()
122
+ return self.response_403()
81
123
 
82
124
  f._permission_name = permission_str
83
125
  return functools.update_wrapper(wraps, f)
@@ -87,10 +129,10 @@ def protect(allow_browser_login=False):
87
129
 
88
130
  def has_access(f):
89
131
  """
90
- Use this decorator to enable granular security permissions to your methods.
91
- Permissions will be associated to a role, and roles are associated to users.
132
+ Use this decorator to enable granular security permissions to your methods.
133
+ Permissions will be associated to a role, and roles are associated to users.
92
134
 
93
- By default the permission's name is the methods name.
135
+ By default the permission's name is the methods name.
94
136
  """
95
137
  if hasattr(f, "_permission_name"):
96
138
  permission_str = f._permission_name
@@ -98,20 +140,18 @@ def has_access(f):
98
140
  permission_str = f.__name__
99
141
 
100
142
  def wraps(self, *args, **kwargs):
101
- permission_str = "{}{}".format(PERMISSION_PREFIX, f._permission_name)
143
+ permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
102
144
  if self.method_permission_name:
103
145
  _permission_name = self.method_permission_name.get(f.__name__)
104
146
  if _permission_name:
105
- permission_str = "{}{}".format(PERMISSION_PREFIX, _permission_name)
147
+ permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
106
148
  if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
107
149
  permission_str, self.class_permission_name
108
150
  ):
109
151
  return f(self, *args, **kwargs)
110
152
  else:
111
153
  log.warning(
112
- LOGMSG_ERR_SEC_ACCESS_DENIED.format(
113
- permission_str, self.__class__.__name__
114
- )
154
+ LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__
115
155
  )
116
156
  flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), "danger")
117
157
  return redirect(
@@ -127,12 +167,12 @@ def has_access(f):
127
167
 
128
168
  def has_access_api(f):
129
169
  """
130
- Use this decorator to enable granular security permissions to your API methods.
131
- Permissions will be associated to a role, and roles are associated to users.
170
+ Use this decorator to enable granular security permissions to your API methods.
171
+ Permissions will be associated to a role, and roles are associated to users.
132
172
 
133
- By default the permission's name is the methods name.
173
+ By default the permission's name is the methods name.
134
174
 
135
- this will return a message and HTTP 401 is case of unauthorized access.
175
+ this will return a message and HTTP 403 is case of unauthorized access.
136
176
  """
137
177
  if hasattr(f, "_permission_name"):
138
178
  permission_str = f._permission_name
@@ -140,29 +180,22 @@ def has_access_api(f):
140
180
  permission_str = f.__name__
141
181
 
142
182
  def wraps(self, *args, **kwargs):
143
- permission_str = "{}{}".format(PERMISSION_PREFIX, f._permission_name)
183
+ permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
144
184
  if self.method_permission_name:
145
185
  _permission_name = self.method_permission_name.get(f.__name__)
146
186
  if _permission_name:
147
- permission_str = "{}{}".format(PERMISSION_PREFIX, _permission_name)
187
+ permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
148
188
  if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
149
189
  permission_str, self.class_permission_name
150
190
  ):
151
191
  return f(self, *args, **kwargs)
152
192
  else:
153
193
  log.warning(
154
- LOGMSG_ERR_SEC_ACCESS_DENIED.format(
155
- permission_str, self.__class__.__name__
156
- )
157
- )
158
- response = make_response(
159
- jsonify(
160
- {"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}
161
- ),
162
- 401,
194
+ LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__
163
195
  )
164
- response.headers["Content-Type"] = "application/json"
165
- return response
196
+ if not current_user.is_authenticated:
197
+ return response_unauthorized_mvc(401)
198
+ return response_unauthorized_mvc(403)
166
199
 
167
200
  f._permission_name = permission_str
168
201
  return functools.update_wrapper(wraps, f)
@@ -170,38 +203,38 @@ def has_access_api(f):
170
203
 
171
204
  def permission_name(name):
172
205
  """
173
- Use this decorator to override the name of the permission.
174
- has_access will use the methods name has the permission name
175
- if you want to override this add this decorator to your methods.
176
- This is useful if you want to aggregate methods to permissions
206
+ Use this decorator to override the name of the permission.
207
+ has_access will use the methods name has the permission name
208
+ if you want to override this add this decorator to your methods.
209
+ This is useful if you want to aggregate methods to permissions
177
210
 
178
- It will add '_permission_name' attribute to your method
179
- that will be inspected by BaseView to collect your view's
180
- permissions.
211
+ It will add '_permission_name' attribute to your method
212
+ that will be inspected by BaseView to collect your view's
213
+ permissions.
181
214
 
182
- Note that you should use @has_access to execute after @permission_name
183
- like on the following example.
215
+ Note that you should use @has_access to execute after @permission_name
216
+ like on the following example.
184
217
 
185
- Use it like this to aggregate permissions for your methods::
218
+ Use it like this to aggregate permissions for your methods::
186
219
 
187
- class MyModelView(ModelView):
188
- datamodel = SQLAInterface(MyModel)
220
+ class MyModelView(ModelView):
221
+ datamodel = SQLAInterface(MyModel)
189
222
 
190
- @has_access
191
- @permission_name('GeneralXPTO_Permission')
192
- @expose(url='/xpto')
193
- def xpto(self):
194
- return "Your on xpto"
223
+ @has_access
224
+ @permission_name('GeneralXPTO_Permission')
225
+ @expose(url='/xpto')
226
+ def xpto(self):
227
+ return "Your on xpto"
195
228
 
196
- @has_access
197
- @permission_name('GeneralXPTO_Permission')
198
- @expose(url='/xpto2')
199
- def xpto2(self):
200
- return "Your on xpto2"
229
+ @has_access
230
+ @permission_name('GeneralXPTO_Permission')
231
+ @expose(url='/xpto2')
232
+ def xpto2(self):
233
+ return "Your on xpto2"
201
234
 
202
235
 
203
- :param name:
204
- The name of the permission to override
236
+ :param name:
237
+ The name of the permission to override
205
238
  """
206
239
 
207
240
  def wraps(f):
@@ -209,3 +242,68 @@ def permission_name(name):
209
242
  return f
210
243
 
211
244
  return wraps
245
+
246
+
247
+ def limit(
248
+ limit_value: Union[str, Callable[[], str]],
249
+ key_func: Optional[Callable[[], str]] = None,
250
+ per_method: bool = False,
251
+ methods: Optional[List[str]] = None,
252
+ error_message: Optional[str] = None,
253
+ exempt_when: Optional[Callable[[], bool]] = None,
254
+ override_defaults: bool = True,
255
+ deduct_when: Optional[Callable[[Response], bool]] = None,
256
+ on_breach: Optional[Callable[[RequestLimit], Optional[Response]]] = None,
257
+ cost: Union[int, Callable[[], int]] = 1,
258
+ ):
259
+ """
260
+ Decorator to be used for rate limiting individual routes or blueprints.
261
+
262
+ :param limit_value: rate limit string or a callable that returns a
263
+ string. :ref:`ratelimit-string` for more details.
264
+ :param key_func: function/lambda to extract the unique
265
+ identifier for the rate limit. defaults to remote address of the
266
+ request.
267
+ :param per_method: whether the limit is sub categorized into the
268
+ http method of the request.
269
+ :param methods: if specified, only the methods in this list will
270
+ be rate limited (default: ``None``).
271
+ :param error_message: string (or callable that returns one) to override
272
+ the error message used in the response.
273
+ :param exempt_when: function/lambda used to decide if the rate
274
+ limit should skipped.
275
+ :param override_defaults: whether the decorated limit overrides
276
+ the default limits (Default: ``True``).
277
+
278
+ .. note:: When used with a :class:`~BaseView` the meaning
279
+ of the parameter extends to any parents the blueprint instance is
280
+ registered under. For more details see :ref:`recipes:nested blueprints`
281
+
282
+ :param deduct_when: a function that receives the current
283
+ :class:`flask.Response` object and returns True/False to decide if a
284
+ deduction should be done from the rate limit
285
+ :param on_breach: a function that will be called when this limit
286
+ is breached. If the function returns an instance of :class:`flask.Response`
287
+ that will be the response embedded into the :exc:`RateLimitExceeded` exception
288
+ raised.
289
+ :param cost: The cost of a hit or a function that
290
+ takes no parameters and returns the cost as an integer (Default: ``1``).
291
+ """
292
+
293
+ def wraps(f: Callable[P, R]) -> Callable[P, R]:
294
+ _limit = Limit(
295
+ limit_value=limit_value,
296
+ key_func=key_func,
297
+ per_method=per_method,
298
+ methods=methods,
299
+ error_message=error_message,
300
+ exempt_when=exempt_when,
301
+ override_defaults=override_defaults,
302
+ deduct_when=deduct_when,
303
+ on_breach=on_breach,
304
+ cost=cost,
305
+ )
306
+ f._limit = _limit
307
+ return f
308
+
309
+ return wraps
@@ -1,16 +1,29 @@
1
1
  from flask_babel import lazy_gettext
2
2
  from flask_wtf.recaptcha import RecaptchaField
3
- from wtforms import BooleanField, PasswordField, StringField
4
- from wtforms.validators import DataRequired, Email, EqualTo
3
+ from wtforms import PasswordField, StringField
4
+ from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
5
5
 
6
6
  from ..fieldwidgets import BS3PasswordFieldWidget, BS3TextFieldWidget
7
7
  from ..forms import DynamicForm
8
+ from ..validators import PasswordComplexityValidator
8
9
 
9
10
 
10
- class LoginForm_oid(DynamicForm):
11
- openid = StringField(lazy_gettext("OpenID"), validators=[DataRequired()])
12
- username = StringField(lazy_gettext("User Name"))
13
- remember_me = BooleanField(lazy_gettext("Remember me"), default=False)
11
+ class SelectDataRequired(DataRequired):
12
+ """
13
+ Select required flag on the input field will not work well on Chrome
14
+ Console error:
15
+ An invalid form control with name='roles' is not focusable.
16
+
17
+ This makes a simple override to the DataRequired to be used specifically with
18
+ select fields
19
+ """
20
+
21
+ field_flags = {}
22
+
23
+
24
+ def roles_or_groups_required(form, field):
25
+ if not form["roles"].data and not form["groups"].data:
26
+ raise ValidationError(lazy_gettext("Either select a role or a group"))
14
27
 
15
28
 
16
29
  class LoginForm_db(DynamicForm):
@@ -40,7 +53,7 @@ class ResetPasswordForm(DynamicForm):
40
53
  "Please use a good password policy,"
41
54
  " this application does not check this for you"
42
55
  ),
43
- validators=[DataRequired()],
56
+ validators=[DataRequired(), PasswordComplexityValidator()],
44
57
  widget=BS3PasswordFieldWidget(),
45
58
  )
46
59
  conf_password = PasswordField(
@@ -88,27 +101,3 @@ class RegisterUserDBForm(DynamicForm):
88
101
  widget=BS3PasswordFieldWidget(),
89
102
  )
90
103
  recaptcha = RecaptchaField()
91
-
92
-
93
- class RegisterUserOIDForm(DynamicForm):
94
- username = StringField(
95
- lazy_gettext("User Name"),
96
- validators=[DataRequired()],
97
- widget=BS3TextFieldWidget(),
98
- )
99
- first_name = StringField(
100
- lazy_gettext("First Name"),
101
- validators=[DataRequired()],
102
- widget=BS3TextFieldWidget(),
103
- )
104
- last_name = StringField(
105
- lazy_gettext("Last Name"),
106
- validators=[DataRequired()],
107
- widget=BS3TextFieldWidget(),
108
- )
109
- email = StringField(
110
- lazy_gettext("Email"),
111
- validators=[DataRequired(), Email()],
112
- widget=BS3TextFieldWidget(),
113
- )
114
- recaptcha = RecaptchaField()