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