Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: major bumps Flask, Click, PyJWT and flask-jwt-extended #1817

Merged
merged 13 commits into from
Mar 21, 2022
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9.7]
python-version: [3.7, 3.8, 3.9.7]
env:
SQLALCHEMY_DATABASE_URI:
postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app
Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7]
python-version: [3.7]
services:
mssql:
image: mongo:4.4.1-bionic
Expand Down
11 changes: 7 additions & 4 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ can run a subset of tests targeting only Postgres.

$ docker-compose up -d


2 - Run Postgres tests

.. code-block:: bash

$ nosetests flask_appbuilder.tests

You can also use tox

.. code-block:: bash

$ tox -e postgres
Expand Down Expand Up @@ -64,16 +69,14 @@ Using Postgres

.. code-block:: bash

$ nosetests -v flask_appbuilder.tests.test_0_fixture

$ nosetests -v flask_appbuilder.tests.test_A_fixture

4 - Run a single test

.. code-block:: bash

$ nosetests -v flask_appbuilder.tests.test_api:APITestCase.test_get_item_dotted_mo_notation


.. note::

If your using SQLite3, the location of the db is: ./flask_appbuilder/tests/app.db
Expand Down
5 changes: 0 additions & 5 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,6 @@ Use config.py to configure the following parameters. By default it will use SQLL
| AUTH_ROLE_PUBLIC | Special Role that holds the public | No |
| | permissions, no authentication needed. | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_STRICT_RESPONSE_CODES | When True, protected endpoints will return | No |
| | HTTP 403 instead of 401. This option will | |
| | be removed and default to True on the next | |
| | major release. defaults to False | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_API_LOGIN_ALLOW_MULTIPLE_PROVIDERS| Allow REST API login with alternative auth | No |
| True|False | providers (default False) | |
+----------------------------------------+--------------------------------------------+-----------+
Expand Down
4 changes: 2 additions & 2 deletions flask_appbuilder/security/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
create_access_token,
create_refresh_token,
get_jwt_identity,
jwt_refresh_token_required,
jwt_required,
)
from marshmallow import ValidationError

Expand Down Expand Up @@ -115,7 +115,7 @@ def login(self) -> Response:
return self.response(200, **resp)

@expose("/refresh", methods=["POST"])
@jwt_refresh_token_required
@jwt_required(refresh=True)
@safe
def refresh(self) -> Response:
"""
Expand Down
25 changes: 6 additions & 19 deletions flask_appbuilder/security/decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import functools
import logging
from typing import TYPE_CHECKING

from flask import (
current_app,
Expand All @@ -24,22 +23,8 @@

log = logging.getLogger(__name__)

if TYPE_CHECKING:
from flask_appbuilder.api import BaseApi


def response_unauthorized(base_class: "BaseApi") -> Response:
if current_app.config.get("AUTH_STRICT_RESPONSE_CODES", False):
return base_class.response_403()
return base_class.response_401()


def response_unauthorized_mvc() -> Response:
status_code = 401
if current_app.appbuilder.sm.current_user and current_app.config.get(
"AUTH_STRICT_RESPONSE_CODES", False
):
status_code = 403
def response_unauthorized_mvc(status_code: int) -> Response:
response = make_response(
jsonify({"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}),
status_code,
Expand Down Expand Up @@ -88,7 +73,7 @@ def wraps(self, *args, **kwargs):
class_permission_name = self.class_permission_name
# Check if permission is allowed on the class
if permission_str not in self.base_permissions:
return response_unauthorized(self)
return self.response_403()
# Check if the resource is public
if current_app.appbuilder.sm.is_item_public(
permission_str, class_permission_name
Expand Down Expand Up @@ -116,7 +101,7 @@ def wraps(self, *args, **kwargs):
permission_str, class_permission_name
)
)
return response_unauthorized(self)
return self.response_403()

f._permission_name = permission_str
return functools.update_wrapper(wraps, f)
Expand Down Expand Up @@ -194,7 +179,9 @@ def wraps(self, *args, **kwargs):
permission_str, self.__class__.__name__
)
)
return response_unauthorized_mvc()
if not current_user.is_authenticated:
return response_unauthorized_mvc(401)
return response_unauthorized_mvc(403)

f._permission_name = permission_str
return functools.update_wrapper(wraps, f)
Expand Down
12 changes: 7 additions & 5 deletions flask_appbuilder/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def create_jwt_manager(self, app) -> JWTManager:
"""
jwt_manager = JWTManager()
jwt_manager.init_app(app)
jwt_manager.user_loader_callback_loader(self.load_user_jwt)
jwt_manager.user_lookup_loader(self.load_user_jwt)
return jwt_manager

def create_builtin_roles(self):
Expand Down Expand Up @@ -871,7 +871,8 @@ def auth_user_db(self, username, password):
)
log.info(LOGMSG_WAR_SEC_LOGIN_FAILED.format(username))
# Balance failure and success
self.noop_user_update(first_user)
if first_user:
self.noop_user_update(first_user)
return None
elif check_password_hash(user.password, password):
self.update_user_auth_stat(user, True)
Expand Down Expand Up @@ -1499,7 +1500,7 @@ def _get_user_permission_view_menus(
result.update(pvms_names)
return result

def has_access(self, permission_name, view_name):
def has_access(self, permission_name: str, view_name: str) -> bool:
"""
Check if current user or public has access to view or menu
"""
Expand Down Expand Up @@ -2036,8 +2037,9 @@ def import_roles(self, path: str) -> None:
def load_user(self, pk):
return self.get_user_by_id(int(pk))

def load_user_jwt(self, pk):
user = self.load_user(pk)
def load_user_jwt(self, _jwt_header, jwt_data):
identity = jwt_data["sub"]
user = self.load_user(identity)
# Set flask g.user to JWT user, we can't do it on before request
g.user = user
return user
Expand Down
1 change: 0 additions & 1 deletion flask_appbuilder/tests/config_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"SQLALCHEMY_DATABASE_URI"
) or "sqlite:///" + os.path.join(basedir, "app.db")

AUTH_STRICT_RESPONSE_CODES = False
SECRET_KEY = "thisismyscretkey"
SQLALCHEMY_TRACK_MODIFICATIONS = False
WTF_CSRF_ENABLED = False
Expand Down
15 changes: 3 additions & 12 deletions flask_appbuilder/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,14 +627,9 @@ def test_auth_authorization(self):
pk = 1
uri = f"api/v1/model1apirestrictedpermissions/{pk}"

self.app.config["AUTH_STRICT_RESPONSE_CODES"] = True
rv = self.auth_client_delete(client, token, uri)
self.assertEqual(rv.status_code, 403)

self.app.config["AUTH_STRICT_RESPONSE_CODES"] = False
rv = self.auth_client_delete(client, token, uri)
self.assertEqual(rv.status_code, 401)

# Test unauthorized POST
item = dict(
field_string="test{}".format(MODEL1_DATA_SIZE + 1),
Expand All @@ -644,12 +639,8 @@ def test_auth_authorization(self):
)
uri = "api/v1/model1apirestrictedpermissions/"

self.app.config["AUTH_STRICT_RESPONSE_CODES"] = True
rv = self.auth_client_post(client, token, uri, item)
self.assertEqual(rv.status_code, 403)
self.app.config["AUTH_STRICT_RESPONSE_CODES"] = False
rv = self.auth_client_post(client, token, uri, item)
self.assertEqual(rv.status_code, 401)

# Test authorized GET
uri = f"api/v1/model1apirestrictedpermissions/{pk}"
Expand All @@ -666,7 +657,7 @@ def test_auth_builtin_roles(self):
pk = 1
uri = "api/v1/model1api/{}".format(pk)
rv = self.auth_client_delete(client, token, uri)
self.assertEqual(rv.status_code, 401)
self.assertEqual(rv.status_code, 403)

# Test unauthorized POST
item = dict(
Expand All @@ -677,7 +668,7 @@ def test_auth_builtin_roles(self):
)
uri = "api/v1/model1api/"
rv = self.auth_client_post(client, token, uri, item)
self.assertEqual(rv.status_code, 401)
self.assertEqual(rv.status_code, 403)

# Test authorized GET
uri = "api/v1/model1api/1"
Expand Down Expand Up @@ -2804,7 +2795,7 @@ class Model2PermOverride2(ModelRestApi):
self.assertEqual(rv.status_code, 200)
uri = "api/v1/model2permoverride2/1"
rv = self.auth_client_delete(client, token, uri)
self.assertEqual(rv.status_code, 401)
self.assertEqual(rv.status_code, 403)

# Revert test data
self.appbuilder.get_session.delete(
Expand Down
13 changes: 1 addition & 12 deletions flask_appbuilder/tests/test_mvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,10 +1342,7 @@ def test_api_unauthenticated(self):
Testing unauthenticated access to MVC API
"""
client = self.app.test_client()
self.app.config["AUTH_STRICT_RESPONSE_CODES"] = True
rv = client.get("/model1formattedview/api/read")
self.assertEqual(rv.status_code, 401)
self.app.config["AUTH_STRICT_RESPONSE_CODES"] = False
self.browser_logout(client)
rv = client.get("/model1formattedview/api/read")
self.assertEqual(rv.status_code, 401)

Expand All @@ -1355,21 +1352,13 @@ def test_api_unauthorized(self):
"""
client = self.app.test_client()
self.browser_login(client, USERNAME_READONLY, PASSWORD_READONLY)
self.app.config["AUTH_STRICT_RESPONSE_CODES"] = True

rv = client.post(
"/model1view/api/create",
data=dict(field_string="zzz"),
follow_redirects=True,
)
self.assertEqual(rv.status_code, 403)
self.app.config["AUTH_STRICT_RESPONSE_CODES"] = False
rv = client.post(
"/model1view/api/create",
data=dict(field_string="zzz"),
follow_redirects=True,
)
self.assertEqual(rv.status_code, 401)

def test_api_create(self):
"""
Expand Down
10 changes: 4 additions & 6 deletions flask_appbuilder/tests/test_mvc_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_oauth_login(self):
raw_state = {}
state = jwt.encode(raw_state, self.app.config["SECRET_KEY"], algorithm="HS256")

response = client.get(f"/oauth-authorized/google?state={state.decode('utf-8')}")
response = client.get(f"/oauth-authorized/google?state={state}")
self.assertEqual(response.location, "http://localhost/")

def test_oauth_login_unknown_provider(self):
Expand All @@ -61,9 +61,7 @@ def test_oauth_login_unknown_provider(self):
raw_state = {}
state = jwt.encode(raw_state, self.app.config["SECRET_KEY"], algorithm="HS256")

response = client.get(
f"/oauth-authorized/unknown_provider?state={state.decode('utf-8')}"
)
response = client.get(f"/oauth-authorized/unknown_provider?state={state}")
self.assertEqual(response.location, "http://localhost/login/")

def test_oauth_login_next(self):
Expand All @@ -77,7 +75,7 @@ def test_oauth_login_next(self):
raw_state = {"next": ["http://localhost/users/list/"]}
state = jwt.encode(raw_state, self.app.config["SECRET_KEY"], algorithm="HS256")

response = client.get(f"/oauth-authorized/google?state={state.decode('utf-8')}")
response = client.get(f"/oauth-authorized/google?state={state}")
self.assertEqual(response.location, "http://localhost/users/list/")

def test_oauth_login_next_check(self):
Expand All @@ -91,5 +89,5 @@ def test_oauth_login_next_check(self):
raw_state = {"next": ["http://www.google.com"]}
state = jwt.encode(raw_state, self.app.config["SECRET_KEY"], algorithm="HS256")

response = client.get(f"/oauth-authorized/google?state={state.decode('utf-8')}")
response = client.get(f"/oauth-authorized/google?state={state}")
self.assertEqual(response.location, "http://localhost/")
1 change: 1 addition & 0 deletions requirements-extra.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ pyodbc==4.0.30
requests==2.26.0
Authlib==0.15.4
python-ldap==3.3.1
flask-openid==1.3.0
Loading