flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flask_appbuilder/__init__.py +2 -3
- flask_appbuilder/_compat.py +0 -1
- flask_appbuilder/actions.py +14 -14
- flask_appbuilder/api/__init__.py +741 -527
- flask_appbuilder/api/convert.py +104 -98
- flask_appbuilder/api/manager.py +14 -8
- flask_appbuilder/api/schemas.py +12 -1
- flask_appbuilder/babel/manager.py +12 -16
- flask_appbuilder/base.py +353 -280
- flask_appbuilder/basemanager.py +1 -1
- flask_appbuilder/baseviews.py +241 -164
- flask_appbuilder/charts/jsontools.py +10 -10
- flask_appbuilder/charts/views.py +56 -60
- flask_appbuilder/cli.py +115 -70
- flask_appbuilder/const.py +52 -52
- flask_appbuilder/exceptions.py +67 -5
- flask_appbuilder/fields.py +32 -23
- flask_appbuilder/fieldwidgets.py +34 -27
- flask_appbuilder/filemanager.py +33 -45
- flask_appbuilder/filters.py +11 -13
- flask_appbuilder/forms.py +31 -35
- flask_appbuilder/hooks.py +90 -0
- flask_appbuilder/menu.py +35 -10
- flask_appbuilder/models/base.py +47 -57
- flask_appbuilder/models/decorators.py +13 -13
- flask_appbuilder/models/filters.py +42 -38
- flask_appbuilder/models/generic/__init__.py +29 -29
- flask_appbuilder/models/generic/filters.py +11 -3
- flask_appbuilder/models/generic/interface.py +1 -3
- flask_appbuilder/models/group.py +37 -39
- flask_appbuilder/models/mixins.py +22 -18
- flask_appbuilder/models/sqla/__init__.py +19 -72
- flask_appbuilder/models/sqla/base.py +24 -0
- flask_appbuilder/models/sqla/base_legacy.py +132 -0
- flask_appbuilder/models/sqla/filters.py +132 -19
- flask_appbuilder/models/sqla/interface.py +390 -276
- flask_appbuilder/security/api.py +31 -35
- flask_appbuilder/security/decorators.py +181 -83
- flask_appbuilder/security/forms.py +20 -31
- flask_appbuilder/security/manager.py +715 -489
- flask_appbuilder/security/registerviews.py +29 -112
- flask_appbuilder/security/schemas.py +43 -0
- flask_appbuilder/security/sqla/apis/__init__.py +8 -0
- flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/group/api.py +227 -0
- flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
- flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
- flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/role/api.py +306 -0
- flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
- flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/user/api.py +292 -0
- flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
- flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
- flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
- flask_appbuilder/security/sqla/manager.py +421 -203
- flask_appbuilder/security/sqla/models.py +192 -57
- flask_appbuilder/security/utils.py +9 -0
- flask_appbuilder/security/views.py +232 -229
- flask_appbuilder/static/.DS_Store +0 -0
- flask_appbuilder/static/appbuilder/css/ab.css +20 -12
- flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
- flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
- flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
- flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
- flask_appbuilder/static/appbuilder/js/ab.js +33 -23
- flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
- flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
- flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
- flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
- flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
- flask_appbuilder/templates/appbuilder/baselib.html +9 -3
- flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
- flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
- flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
- flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
- flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
- flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
- flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
- flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
- flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
- flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
- flask_appbuilder/templates/appbuilder/init.html +37 -43
- flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
- flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
- flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
- flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
- flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
- flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
- flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
- flask_appbuilder/upload.py +20 -22
- flask_appbuilder/urltools.py +39 -19
- flask_appbuilder/utils/base.py +76 -0
- flask_appbuilder/utils/legacy.py +33 -0
- flask_appbuilder/utils/limit.py +20 -0
- flask_appbuilder/validators.py +73 -14
- flask_appbuilder/views.py +75 -424
- flask_appbuilder/widgets.py +50 -51
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
- flask_appbuilder/console.py +0 -426
- flask_appbuilder/models/mongoengine/__init__.py +0 -0
- flask_appbuilder/models/mongoengine/fields.py +0 -65
- flask_appbuilder/models/mongoengine/filters.py +0 -145
- flask_appbuilder/models/mongoengine/interface.py +0 -328
- flask_appbuilder/security/mongoengine/__init__.py +0 -0
- flask_appbuilder/security/mongoengine/manager.py +0 -402
- flask_appbuilder/security/mongoengine/models.py +0 -120
- flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
- flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
- flask_appbuilder/static/appbuilder/img/aol.png +0 -0
- flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
- flask_appbuilder/static/appbuilder/img/google.png +0 -0
- flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
- flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
- flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
- flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
- flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
- flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
- flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
- flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
- flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
- flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
- flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
- flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
- flask_appbuilder/tests/__init__.py +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
- flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
- flask_appbuilder/tests/_test_auth_oauth.py +0 -419
- flask_appbuilder/tests/_test_ldapsearch.py +0 -135
- flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
- flask_appbuilder/tests/app.db +0 -0
- flask_appbuilder/tests/base.py +0 -90
- flask_appbuilder/tests/config_api.py +0 -21
- flask_appbuilder/tests/const.py +0 -9
- flask_appbuilder/tests/mongoengine/__init__.py +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/models.py +0 -41
- flask_appbuilder/tests/sqla/__init__.py +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/models.py +0 -340
- flask_appbuilder/tests/test_0_fixture.py +0 -39
- flask_appbuilder/tests/test_api.py +0 -2790
- flask_appbuilder/tests/test_fab_cli.py +0 -72
- flask_appbuilder/tests/test_menu.py +0 -122
- flask_appbuilder/tests/test_mongoengine.py +0 -572
- flask_appbuilder/tests/test_mvc.py +0 -1710
- flask_appbuilder/tests/test_sqlalchemy.py +0 -24
- flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
flask_appbuilder/security/api.py
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
from flask import request
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
create_refresh_token,
|
|
5
|
-
get_jwt_identity,
|
|
6
|
-
jwt_refresh_token_required,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
from ..api import BaseApi, safe
|
|
10
|
-
from ..const import (
|
|
1
|
+
from flask import request, Response
|
|
2
|
+
from flask_appbuilder.api import BaseApi, safe
|
|
3
|
+
from flask_appbuilder.const import (
|
|
11
4
|
API_SECURITY_ACCESS_TOKEN_KEY,
|
|
12
|
-
API_SECURITY_PASSWORD_KEY,
|
|
13
5
|
API_SECURITY_PROVIDER_DB,
|
|
14
|
-
API_SECURITY_PROVIDER_KEY,
|
|
15
6
|
API_SECURITY_PROVIDER_LDAP,
|
|
16
|
-
API_SECURITY_REFRESH_KEY,
|
|
17
7
|
API_SECURITY_REFRESH_TOKEN_KEY,
|
|
18
|
-
API_SECURITY_USERNAME_KEY,
|
|
19
8
|
API_SECURITY_VERSION,
|
|
20
9
|
)
|
|
21
|
-
from
|
|
10
|
+
from flask_appbuilder.security.schemas import login_post
|
|
11
|
+
from flask_appbuilder.views import expose
|
|
12
|
+
from flask_jwt_extended import (
|
|
13
|
+
create_access_token,
|
|
14
|
+
create_refresh_token,
|
|
15
|
+
get_jwt_identity,
|
|
16
|
+
jwt_required,
|
|
17
|
+
)
|
|
18
|
+
from marshmallow import ValidationError
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
class SecurityApi(BaseApi):
|
|
25
|
-
|
|
26
22
|
resource_name = "security"
|
|
27
23
|
version = API_SECURITY_VERSION
|
|
28
24
|
openapi_spec_tag = "Security"
|
|
@@ -35,7 +31,7 @@ class SecurityApi(BaseApi):
|
|
|
35
31
|
|
|
36
32
|
@expose("/login", methods=["POST"])
|
|
37
33
|
@safe
|
|
38
|
-
def login(self):
|
|
34
|
+
def login(self) -> Response:
|
|
39
35
|
"""Login endpoint for the API returns a JWT and optionally a refresh token
|
|
40
36
|
---
|
|
41
37
|
post:
|
|
@@ -88,20 +84,20 @@ class SecurityApi(BaseApi):
|
|
|
88
84
|
"""
|
|
89
85
|
if not request.is_json:
|
|
90
86
|
return self.response_400(message="Request payload is not JSON")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return self.response_400(message="Missing required parameter")
|
|
87
|
+
try:
|
|
88
|
+
login_payload = login_post.load(request.json)
|
|
89
|
+
except ValidationError as error:
|
|
90
|
+
return self.response_400(message=error.messages)
|
|
91
|
+
|
|
97
92
|
# AUTH
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
user = None
|
|
94
|
+
if login_payload["provider"] == API_SECURITY_PROVIDER_DB:
|
|
95
|
+
user = self.appbuilder.sm.auth_user_db(
|
|
96
|
+
login_payload["username"], login_payload["password"]
|
|
97
|
+
)
|
|
98
|
+
elif login_payload["provider"] == API_SECURITY_PROVIDER_LDAP:
|
|
99
|
+
user = self.appbuilder.sm.auth_user_ldap(
|
|
100
|
+
login_payload["username"], login_payload["password"]
|
|
105
101
|
)
|
|
106
102
|
if not user:
|
|
107
103
|
return self.response_401()
|
|
@@ -109,18 +105,18 @@ class SecurityApi(BaseApi):
|
|
|
109
105
|
# Identity can be any data that is json serializable
|
|
110
106
|
resp = dict()
|
|
111
107
|
resp[API_SECURITY_ACCESS_TOKEN_KEY] = create_access_token(
|
|
112
|
-
identity=user.id, fresh=True
|
|
108
|
+
identity=str(user.id), fresh=True
|
|
113
109
|
)
|
|
114
|
-
if refresh:
|
|
110
|
+
if "refresh" in login_payload and login_payload["refresh"]:
|
|
115
111
|
resp[API_SECURITY_REFRESH_TOKEN_KEY] = create_refresh_token(
|
|
116
|
-
identity=user.id
|
|
112
|
+
identity=str(user.id)
|
|
117
113
|
)
|
|
118
114
|
return self.response(200, **resp)
|
|
119
115
|
|
|
120
116
|
@expose("/refresh", methods=["POST"])
|
|
121
|
-
@
|
|
117
|
+
@jwt_required(refresh=True)
|
|
122
118
|
@safe
|
|
123
|
-
def refresh(self):
|
|
119
|
+
def refresh(self) -> Response:
|
|
124
120
|
"""
|
|
125
121
|
Security endpoint for the refresh token, so we can obtain a new
|
|
126
122
|
token without forcing the user to login again
|
|
@@ -1,42 +1,80 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import logging
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
from typing import Callable, List, Optional, TypeVar, Union
|
|
4
|
+
|
|
5
|
+
from flask import (
|
|
6
|
+
current_app,
|
|
7
|
+
flash,
|
|
8
|
+
jsonify,
|
|
9
|
+
make_response,
|
|
10
|
+
redirect,
|
|
11
|
+
request,
|
|
12
|
+
Response,
|
|
13
|
+
url_for,
|
|
14
|
+
)
|
|
15
|
+
from flask_appbuilder._compat import as_unicode
|
|
16
|
+
from flask_appbuilder.const import (
|
|
10
17
|
FLAMSG_ERR_SEC_ACCESS_DENIED,
|
|
11
18
|
LOGMSG_ERR_SEC_ACCESS_DENIED,
|
|
12
19
|
PERMISSION_PREFIX,
|
|
13
20
|
)
|
|
21
|
+
from flask_appbuilder.utils.limit import Limit
|
|
22
|
+
from flask_jwt_extended import verify_jwt_in_request
|
|
23
|
+
from flask_limiter import RequestLimit
|
|
24
|
+
from flask_login import current_user
|
|
25
|
+
from typing_extensions import ParamSpec
|
|
14
26
|
|
|
15
27
|
log = logging.getLogger(__name__)
|
|
16
28
|
|
|
29
|
+
R = TypeVar("R")
|
|
30
|
+
P = ParamSpec("P")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def no_cache(view: Callable[..., Response]) -> Callable[..., Response]:
|
|
34
|
+
@functools.wraps(view)
|
|
35
|
+
def wrapped_view(*args, **kwargs) -> Response:
|
|
36
|
+
response = make_response(view(*args, **kwargs))
|
|
37
|
+
response.headers[
|
|
38
|
+
"Cache-Control"
|
|
39
|
+
] = "no-store, no-cache, must-revalidate, max-age=0"
|
|
40
|
+
response.headers["Pragma"] = "no-cache"
|
|
41
|
+
response.headers["Expires"] = "0"
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
return wrapped_view
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def response_unauthorized_mvc(status_code: int) -> Response:
|
|
48
|
+
response = make_response(
|
|
49
|
+
jsonify({"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}),
|
|
50
|
+
status_code,
|
|
51
|
+
)
|
|
52
|
+
response.headers["Content-Type"] = "application/json"
|
|
53
|
+
return response
|
|
54
|
+
|
|
17
55
|
|
|
18
56
|
def protect(allow_browser_login=False):
|
|
19
57
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
Use this decorator to enable granular security permissions
|
|
59
|
+
to your API methods (BaseApi and child classes).
|
|
60
|
+
Permissions will be associated to a role, and roles are associated to users.
|
|
61
|
+
|
|
62
|
+
allow_browser_login will accept signed cookies obtained from the normal MVC app::
|
|
63
|
+
|
|
64
|
+
class MyApi(BaseApi):
|
|
65
|
+
@expose('/dosonmething', methods=['GET'])
|
|
66
|
+
@protect(allow_browser_login=True)
|
|
67
|
+
@safe
|
|
68
|
+
def do_something(self):
|
|
69
|
+
....
|
|
70
|
+
|
|
71
|
+
@expose('/dosonmethingelse', methods=['GET'])
|
|
72
|
+
@protect()
|
|
73
|
+
@safe
|
|
74
|
+
def do_something_else(self):
|
|
75
|
+
....
|
|
76
|
+
|
|
77
|
+
By default the permission's name is the methods name.
|
|
40
78
|
"""
|
|
41
79
|
|
|
42
80
|
def _protect(f):
|
|
@@ -47,25 +85,31 @@ def protect(allow_browser_login=False):
|
|
|
47
85
|
|
|
48
86
|
def wraps(self, *args, **kwargs):
|
|
49
87
|
# Apply method permission name override if exists
|
|
50
|
-
permission_str = "{}{
|
|
88
|
+
permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
|
|
51
89
|
if self.method_permission_name:
|
|
52
90
|
_permission_name = self.method_permission_name.get(f.__name__)
|
|
53
91
|
if _permission_name:
|
|
54
|
-
permission_str = "{}{}"
|
|
92
|
+
permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
|
|
55
93
|
class_permission_name = self.class_permission_name
|
|
94
|
+
# Check if permission is allowed on the class
|
|
56
95
|
if permission_str not in self.base_permissions:
|
|
57
|
-
return self.
|
|
96
|
+
return self.response_403()
|
|
97
|
+
# Check if the resource is public
|
|
58
98
|
if current_app.appbuilder.sm.is_item_public(
|
|
59
99
|
permission_str, class_permission_name
|
|
60
100
|
):
|
|
61
101
|
return f(self, *args, **kwargs)
|
|
102
|
+
# if no browser login then verify JWT
|
|
62
103
|
if not (self.allow_browser_login or allow_browser_login):
|
|
63
104
|
verify_jwt_in_request()
|
|
105
|
+
# Verify resource access
|
|
64
106
|
if current_app.appbuilder.sm.has_access(
|
|
65
107
|
permission_str, class_permission_name
|
|
66
108
|
):
|
|
67
109
|
return f(self, *args, **kwargs)
|
|
110
|
+
# If browser login?
|
|
68
111
|
elif self.allow_browser_login or allow_browser_login:
|
|
112
|
+
# no session cookie (but we allow it), then try JWT
|
|
69
113
|
if not current_user.is_authenticated:
|
|
70
114
|
verify_jwt_in_request()
|
|
71
115
|
if current_app.appbuilder.sm.has_access(
|
|
@@ -73,11 +117,9 @@ def protect(allow_browser_login=False):
|
|
|
73
117
|
):
|
|
74
118
|
return f(self, *args, **kwargs)
|
|
75
119
|
log.warning(
|
|
76
|
-
LOGMSG_ERR_SEC_ACCESS_DENIED
|
|
77
|
-
permission_str, class_permission_name
|
|
78
|
-
)
|
|
120
|
+
LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, class_permission_name
|
|
79
121
|
)
|
|
80
|
-
return self.
|
|
122
|
+
return self.response_403()
|
|
81
123
|
|
|
82
124
|
f._permission_name = permission_str
|
|
83
125
|
return functools.update_wrapper(wraps, f)
|
|
@@ -87,10 +129,10 @@ def protect(allow_browser_login=False):
|
|
|
87
129
|
|
|
88
130
|
def has_access(f):
|
|
89
131
|
"""
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
Use this decorator to enable granular security permissions to your methods.
|
|
133
|
+
Permissions will be associated to a role, and roles are associated to users.
|
|
92
134
|
|
|
93
|
-
|
|
135
|
+
By default the permission's name is the methods name.
|
|
94
136
|
"""
|
|
95
137
|
if hasattr(f, "_permission_name"):
|
|
96
138
|
permission_str = f._permission_name
|
|
@@ -98,20 +140,18 @@ def has_access(f):
|
|
|
98
140
|
permission_str = f.__name__
|
|
99
141
|
|
|
100
142
|
def wraps(self, *args, **kwargs):
|
|
101
|
-
permission_str = "{}{
|
|
143
|
+
permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
|
|
102
144
|
if self.method_permission_name:
|
|
103
145
|
_permission_name = self.method_permission_name.get(f.__name__)
|
|
104
146
|
if _permission_name:
|
|
105
|
-
permission_str = "{}{}"
|
|
147
|
+
permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
|
|
106
148
|
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
|
|
107
149
|
permission_str, self.class_permission_name
|
|
108
150
|
):
|
|
109
151
|
return f(self, *args, **kwargs)
|
|
110
152
|
else:
|
|
111
153
|
log.warning(
|
|
112
|
-
LOGMSG_ERR_SEC_ACCESS_DENIED.
|
|
113
|
-
permission_str, self.__class__.__name__
|
|
114
|
-
)
|
|
154
|
+
LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__
|
|
115
155
|
)
|
|
116
156
|
flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), "danger")
|
|
117
157
|
return redirect(
|
|
@@ -127,12 +167,12 @@ def has_access(f):
|
|
|
127
167
|
|
|
128
168
|
def has_access_api(f):
|
|
129
169
|
"""
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
Use this decorator to enable granular security permissions to your API methods.
|
|
171
|
+
Permissions will be associated to a role, and roles are associated to users.
|
|
132
172
|
|
|
133
|
-
|
|
173
|
+
By default the permission's name is the methods name.
|
|
134
174
|
|
|
135
|
-
|
|
175
|
+
this will return a message and HTTP 403 is case of unauthorized access.
|
|
136
176
|
"""
|
|
137
177
|
if hasattr(f, "_permission_name"):
|
|
138
178
|
permission_str = f._permission_name
|
|
@@ -140,29 +180,22 @@ def has_access_api(f):
|
|
|
140
180
|
permission_str = f.__name__
|
|
141
181
|
|
|
142
182
|
def wraps(self, *args, **kwargs):
|
|
143
|
-
permission_str = "{}{
|
|
183
|
+
permission_str = f"{PERMISSION_PREFIX}{f._permission_name}"
|
|
144
184
|
if self.method_permission_name:
|
|
145
185
|
_permission_name = self.method_permission_name.get(f.__name__)
|
|
146
186
|
if _permission_name:
|
|
147
|
-
permission_str = "{}{}"
|
|
187
|
+
permission_str = f"{PERMISSION_PREFIX}{_permission_name}"
|
|
148
188
|
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
|
|
149
189
|
permission_str, self.class_permission_name
|
|
150
190
|
):
|
|
151
191
|
return f(self, *args, **kwargs)
|
|
152
192
|
else:
|
|
153
193
|
log.warning(
|
|
154
|
-
LOGMSG_ERR_SEC_ACCESS_DENIED.
|
|
155
|
-
permission_str, self.__class__.__name__
|
|
156
|
-
)
|
|
157
|
-
)
|
|
158
|
-
response = make_response(
|
|
159
|
-
jsonify(
|
|
160
|
-
{"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}
|
|
161
|
-
),
|
|
162
|
-
401,
|
|
194
|
+
LOGMSG_ERR_SEC_ACCESS_DENIED, permission_str, self.__class__.__name__
|
|
163
195
|
)
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
if not current_user.is_authenticated:
|
|
197
|
+
return response_unauthorized_mvc(401)
|
|
198
|
+
return response_unauthorized_mvc(403)
|
|
166
199
|
|
|
167
200
|
f._permission_name = permission_str
|
|
168
201
|
return functools.update_wrapper(wraps, f)
|
|
@@ -170,38 +203,38 @@ def has_access_api(f):
|
|
|
170
203
|
|
|
171
204
|
def permission_name(name):
|
|
172
205
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
Use this decorator to override the name of the permission.
|
|
207
|
+
has_access will use the methods name has the permission name
|
|
208
|
+
if you want to override this add this decorator to your methods.
|
|
209
|
+
This is useful if you want to aggregate methods to permissions
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
It will add '_permission_name' attribute to your method
|
|
212
|
+
that will be inspected by BaseView to collect your view's
|
|
213
|
+
permissions.
|
|
181
214
|
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
Note that you should use @has_access to execute after @permission_name
|
|
216
|
+
like on the following example.
|
|
184
217
|
|
|
185
|
-
|
|
218
|
+
Use it like this to aggregate permissions for your methods::
|
|
186
219
|
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
class MyModelView(ModelView):
|
|
221
|
+
datamodel = SQLAInterface(MyModel)
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
@has_access
|
|
224
|
+
@permission_name('GeneralXPTO_Permission')
|
|
225
|
+
@expose(url='/xpto')
|
|
226
|
+
def xpto(self):
|
|
227
|
+
return "Your on xpto"
|
|
195
228
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
@has_access
|
|
230
|
+
@permission_name('GeneralXPTO_Permission')
|
|
231
|
+
@expose(url='/xpto2')
|
|
232
|
+
def xpto2(self):
|
|
233
|
+
return "Your on xpto2"
|
|
201
234
|
|
|
202
235
|
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
:param name:
|
|
237
|
+
The name of the permission to override
|
|
205
238
|
"""
|
|
206
239
|
|
|
207
240
|
def wraps(f):
|
|
@@ -209,3 +242,68 @@ def permission_name(name):
|
|
|
209
242
|
return f
|
|
210
243
|
|
|
211
244
|
return wraps
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def limit(
|
|
248
|
+
limit_value: Union[str, Callable[[], str]],
|
|
249
|
+
key_func: Optional[Callable[[], str]] = None,
|
|
250
|
+
per_method: bool = False,
|
|
251
|
+
methods: Optional[List[str]] = None,
|
|
252
|
+
error_message: Optional[str] = None,
|
|
253
|
+
exempt_when: Optional[Callable[[], bool]] = None,
|
|
254
|
+
override_defaults: bool = True,
|
|
255
|
+
deduct_when: Optional[Callable[[Response], bool]] = None,
|
|
256
|
+
on_breach: Optional[Callable[[RequestLimit], Optional[Response]]] = None,
|
|
257
|
+
cost: Union[int, Callable[[], int]] = 1,
|
|
258
|
+
):
|
|
259
|
+
"""
|
|
260
|
+
Decorator to be used for rate limiting individual routes or blueprints.
|
|
261
|
+
|
|
262
|
+
:param limit_value: rate limit string or a callable that returns a
|
|
263
|
+
string. :ref:`ratelimit-string` for more details.
|
|
264
|
+
:param key_func: function/lambda to extract the unique
|
|
265
|
+
identifier for the rate limit. defaults to remote address of the
|
|
266
|
+
request.
|
|
267
|
+
:param per_method: whether the limit is sub categorized into the
|
|
268
|
+
http method of the request.
|
|
269
|
+
:param methods: if specified, only the methods in this list will
|
|
270
|
+
be rate limited (default: ``None``).
|
|
271
|
+
:param error_message: string (or callable that returns one) to override
|
|
272
|
+
the error message used in the response.
|
|
273
|
+
:param exempt_when: function/lambda used to decide if the rate
|
|
274
|
+
limit should skipped.
|
|
275
|
+
:param override_defaults: whether the decorated limit overrides
|
|
276
|
+
the default limits (Default: ``True``).
|
|
277
|
+
|
|
278
|
+
.. note:: When used with a :class:`~BaseView` the meaning
|
|
279
|
+
of the parameter extends to any parents the blueprint instance is
|
|
280
|
+
registered under. For more details see :ref:`recipes:nested blueprints`
|
|
281
|
+
|
|
282
|
+
:param deduct_when: a function that receives the current
|
|
283
|
+
:class:`flask.Response` object and returns True/False to decide if a
|
|
284
|
+
deduction should be done from the rate limit
|
|
285
|
+
:param on_breach: a function that will be called when this limit
|
|
286
|
+
is breached. If the function returns an instance of :class:`flask.Response`
|
|
287
|
+
that will be the response embedded into the :exc:`RateLimitExceeded` exception
|
|
288
|
+
raised.
|
|
289
|
+
:param cost: The cost of a hit or a function that
|
|
290
|
+
takes no parameters and returns the cost as an integer (Default: ``1``).
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def wraps(f: Callable[P, R]) -> Callable[P, R]:
|
|
294
|
+
_limit = Limit(
|
|
295
|
+
limit_value=limit_value,
|
|
296
|
+
key_func=key_func,
|
|
297
|
+
per_method=per_method,
|
|
298
|
+
methods=methods,
|
|
299
|
+
error_message=error_message,
|
|
300
|
+
exempt_when=exempt_when,
|
|
301
|
+
override_defaults=override_defaults,
|
|
302
|
+
deduct_when=deduct_when,
|
|
303
|
+
on_breach=on_breach,
|
|
304
|
+
cost=cost,
|
|
305
|
+
)
|
|
306
|
+
f._limit = _limit
|
|
307
|
+
return f
|
|
308
|
+
|
|
309
|
+
return wraps
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
from flask_babel import lazy_gettext
|
|
2
2
|
from flask_wtf.recaptcha import RecaptchaField
|
|
3
|
-
from wtforms import
|
|
4
|
-
from wtforms.validators import DataRequired, Email, EqualTo
|
|
3
|
+
from wtforms import PasswordField, StringField
|
|
4
|
+
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
|
|
5
5
|
|
|
6
6
|
from ..fieldwidgets import BS3PasswordFieldWidget, BS3TextFieldWidget
|
|
7
7
|
from ..forms import DynamicForm
|
|
8
|
+
from ..validators import PasswordComplexityValidator
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
class SelectDataRequired(DataRequired):
|
|
12
|
+
"""
|
|
13
|
+
Select required flag on the input field will not work well on Chrome
|
|
14
|
+
Console error:
|
|
15
|
+
An invalid form control with name='roles' is not focusable.
|
|
16
|
+
|
|
17
|
+
This makes a simple override to the DataRequired to be used specifically with
|
|
18
|
+
select fields
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
field_flags = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def roles_or_groups_required(form, field):
|
|
25
|
+
if not form["roles"].data and not form["groups"].data:
|
|
26
|
+
raise ValidationError(lazy_gettext("Either select a role or a group"))
|
|
14
27
|
|
|
15
28
|
|
|
16
29
|
class LoginForm_db(DynamicForm):
|
|
@@ -40,7 +53,7 @@ class ResetPasswordForm(DynamicForm):
|
|
|
40
53
|
"Please use a good password policy,"
|
|
41
54
|
" this application does not check this for you"
|
|
42
55
|
),
|
|
43
|
-
validators=[DataRequired()],
|
|
56
|
+
validators=[DataRequired(), PasswordComplexityValidator()],
|
|
44
57
|
widget=BS3PasswordFieldWidget(),
|
|
45
58
|
)
|
|
46
59
|
conf_password = PasswordField(
|
|
@@ -88,27 +101,3 @@ class RegisterUserDBForm(DynamicForm):
|
|
|
88
101
|
widget=BS3PasswordFieldWidget(),
|
|
89
102
|
)
|
|
90
103
|
recaptcha = RecaptchaField()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class RegisterUserOIDForm(DynamicForm):
|
|
94
|
-
username = StringField(
|
|
95
|
-
lazy_gettext("User Name"),
|
|
96
|
-
validators=[DataRequired()],
|
|
97
|
-
widget=BS3TextFieldWidget(),
|
|
98
|
-
)
|
|
99
|
-
first_name = StringField(
|
|
100
|
-
lazy_gettext("First Name"),
|
|
101
|
-
validators=[DataRequired()],
|
|
102
|
-
widget=BS3TextFieldWidget(),
|
|
103
|
-
)
|
|
104
|
-
last_name = StringField(
|
|
105
|
-
lazy_gettext("Last Name"),
|
|
106
|
-
validators=[DataRequired()],
|
|
107
|
-
widget=BS3TextFieldWidget(),
|
|
108
|
-
)
|
|
109
|
-
email = StringField(
|
|
110
|
-
lazy_gettext("Email"),
|
|
111
|
-
validators=[DataRequired(), Email()],
|
|
112
|
-
widget=BS3TextFieldWidget(),
|
|
113
|
-
)
|
|
114
|
-
recaptcha = RecaptchaField()
|