diff --git a/fittrackee/application/app_config.py b/fittrackee/application/app_config.py index 320a2eaf..8061a39e 100644 --- a/fittrackee/application/app_config.py +++ b/fittrackee/application/app_config.py @@ -67,7 +67,7 @@ def get_application_config() -> Union[Dict, HttpResponse]: @config_blueprint.route('/config', methods=['PATCH']) -@require_auth(scopes='write', as_admin=True) +@require_auth(scopes=['application:write'], as_admin=True) def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]: """ Update Application config diff --git a/fittrackee/oauth2/client.py b/fittrackee/oauth2/client.py index c2092cd9..b1bd2d4d 100644 --- a/fittrackee/oauth2/client.py +++ b/fittrackee/oauth2/client.py @@ -3,28 +3,37 @@ from typing import Dict from werkzeug.security import gen_salt -from fittrackee.oauth2.models import OAuth2Client from fittrackee.users.models import User -DEFAULT_SCOPE = 'read' -VALID_SCOPES = ['read', 'write'] +from .exceptions import InvalidOAuth2Scopes +from .models import OAuth2Client + +VALID_SCOPES = [ + 'application:write', + 'profile:read', + 'profile:write', + 'users:read', + 'users:write', + 'workouts:read', + 'workouts:write', +] def check_scope(scope: str) -> str: """ Verify if provided scope is valid. - If not, it returns the default scope ('read'). """ - valid_scopes = [] if not isinstance(scope, str) or not scope: - return DEFAULT_SCOPE + raise InvalidOAuth2Scopes() + valid_scopes = [] scopes = scope.split() for value in scopes: if value in VALID_SCOPES: valid_scopes.append(value) - if len(valid_scopes) == 0: - valid_scopes.append(DEFAULT_SCOPE) + + if not valid_scopes: + raise InvalidOAuth2Scopes() return ' '.join(valid_scopes) diff --git a/fittrackee/oauth2/exceptions.py b/fittrackee/oauth2/exceptions.py new file mode 100644 index 00000000..cffd7534 --- /dev/null +++ b/fittrackee/oauth2/exceptions.py @@ -0,0 +1,2 @@ +class InvalidOAuth2Scopes(Exception): + ... diff --git a/fittrackee/oauth2/routes.py b/fittrackee/oauth2/routes.py index 42c4a144..1bd21511 100644 --- a/fittrackee/oauth2/routes.py +++ b/fittrackee/oauth2/routes.py @@ -5,8 +5,6 @@ from flask import Blueprint, Response, request from urllib3.util import parse_url from fittrackee import db -from fittrackee.oauth2.models import OAuth2Client, OAuth2Token -from fittrackee.oauth2.server import require_auth from fittrackee.responses import ( HttpResponse, InvalidPayloadErrorResponse, @@ -15,7 +13,9 @@ from fittrackee.responses import ( from fittrackee.users.models import User from .client import create_oauth_client -from .server import authorization_server +from .exceptions import InvalidOAuth2Scopes +from .models import OAuth2Client, OAuth2Token +from .server import authorization_server, require_auth oauth_blueprint = Blueprint('oauth', __name__) @@ -86,7 +86,13 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]: ) ) - new_client = create_oauth_client(client_metadata, auth_user) + try: + new_client = create_oauth_client(client_metadata, auth_user) + except InvalidOAuth2Scopes: + return InvalidPayloadErrorResponse( + message=('OAuth client invalid scopes') + ) + db.session.add(new_client) db.session.commit() return ( @@ -134,10 +140,10 @@ def get_client_by_id( return get_client(auth_user, client_id=client_id, client_client_id=None) -@oauth_blueprint.route('/oauth/apps/', methods=['DELETE']) +@oauth_blueprint.route('/oauth/apps/', methods=['DELETE']) @require_auth() def delete_client( - auth_user: User, client_id: str + auth_user: User, client_id: int ) -> Union[Tuple[Dict, int], HttpResponse]: client = OAuth2Client.query.filter_by( id=client_id, diff --git a/fittrackee/tests/application/test_app_config_api.py b/fittrackee/tests/application/test_app_config_api.py index 493312e3..5836b918 100644 --- a/fittrackee/tests/application/test_app_config_api.py +++ b/fittrackee/tests/application/test_app_config_api.py @@ -324,3 +324,39 @@ class TestUpdateConfig(ApiTestCaseMixin): data = json.loads(response.data.decode()) assert 'success' in data['status'] assert data['data']['admin_contact'] is None + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', True), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.patch( + '/api/config', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/mixins.py b/fittrackee/tests/mixins.py index cde6a6ee..47827348 100644 --- a/fittrackee/tests/mixins.py +++ b/fittrackee/tests/mixins.py @@ -268,15 +268,14 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin): @staticmethod def assert_not_insufficient_scope_error(response: TestResponse) -> None: assert response.status_code != 403 - if response.status_code != 204: - data = json.loads(response.data.decode()) - if 'error' in data: - assert 'insufficient_scope' not in data['error'] - if 'error_description' in data: - assert ( - 'The request requires higher privileges than provided by ' - 'the access token.' - ) != data['error_description'] + + def assert_response_scope( + self, response: TestResponse, can_access: bool + ) -> None: + if can_access: + self.assert_not_insufficient_scope_error(response) + else: + self.assert_insufficient_scope(response) class CallArgsMixin: diff --git a/fittrackee/tests/oauth2/test_oauth2_client.py b/fittrackee/tests/oauth2/test_oauth2_client.py index 70883f44..62ca39c6 100644 --- a/fittrackee/tests/oauth2/test_oauth2_client.py +++ b/fittrackee/tests/oauth2/test_oauth2_client.py @@ -6,6 +6,7 @@ import pytest from flask import Flask from fittrackee.oauth2.client import check_scope, create_oauth_client +from fittrackee.oauth2.exceptions import InvalidOAuth2Scopes from fittrackee.oauth2.models import OAuth2Client from fittrackee.users.models import User @@ -15,7 +16,7 @@ TEST_METADATA = { 'client_name': random_string(), 'client_uri': random_string(), 'redirect_uris': [random_domain()], - 'scope': 'read write', + 'scope': 'profile:read', } @@ -127,13 +128,21 @@ class TestCreateOAuth2Client: def test_oauth_client_has_expected_scope( self, app: Flask, user_1: User ) -> None: - scope = 'write' + scope = 'workouts:write' client_metadata: Dict = {**TEST_METADATA, 'scope': scope} oauth_client = create_oauth_client(client_metadata, user_1) assert oauth_client.scope == scope + def test_it_raises_error_when_scope_is_invalid( + self, app: Flask, user_1: User + ) -> None: + client_metadata: Dict = {**TEST_METADATA, 'scope': random_string()} + + with pytest.raises(InvalidOAuth2Scopes): + create_oauth_client(client_metadata, user_1) + def test_oauth_client_has_expected_token_endpoint_auth_method( self, app: Flask, user_1: User ) -> None: @@ -165,19 +174,23 @@ class TestOAuthCheckScopes: @pytest.mark.parametrize( 'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']] ) - def test_it_returns_read_if_scope_is_invalid( + def test_it_raises_error_when_scope_is_invalid( self, input_scope: Any ) -> None: - assert check_scope(input_scope) == 'read' + with pytest.raises(InvalidOAuth2Scopes): + check_scope(input_scope) @pytest.mark.parametrize( 'input_scope,expected_scope', [ - ('read', 'read'), - ('read ' + random_string(), 'read'), - ('write', 'write'), - ('write read', 'write read'), - ('write read ' + random_string(), 'write read'), + ('profile:read', 'profile:read'), + ('profile:read ' + random_string(), 'profile:read'), + ('profile:write', 'profile:write'), + ('profile:read profile:write', 'profile:read profile:write'), + ( + 'profile:write profile:read ' + random_string(), + 'profile:write profile:read', + ), ], ) def test_it_return_only_valid_scopes( diff --git a/fittrackee/tests/oauth2/test_oauth2_routes.py b/fittrackee/tests/oauth2/test_oauth2_routes.py index 9acac183..b50cf53c 100644 --- a/fittrackee/tests/oauth2/test_oauth2_routes.py +++ b/fittrackee/tests/oauth2/test_oauth2_routes.py @@ -83,6 +83,30 @@ class TestOAuthClientCreation(ApiTestCaseMixin): error_message=f'OAuth client metadata missing keys: {missing_key}', ) + def test_it_returns_error_when_scope_is_invalid( + self, app: Flask, user_1: User + ) -> None: + invalid_scope = self.random_string() + metadata: Dict = { + **TEST_OAUTH_CLIENT_METADATA, + 'scope': invalid_scope, + } + client, auth_token = self.get_test_client_and_auth_token( + app, user_1.email + ) + + response = client.post( + self.route, + data=json.dumps(metadata), + content_type='application/json', + headers=dict(Authorization=f'Bearer {auth_token}'), + ) + + self.assert_400( + response, + error_message=('OAuth client invalid scopes'), + ) + def test_it_creates_oauth_client(self, app: Flask, user_1: User) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email diff --git a/fittrackee/tests/oauth2/test_oauth2_scopes.py b/fittrackee/tests/oauth2/test_oauth2_scopes.py index c818d701..abc4df93 100644 --- a/fittrackee/tests/oauth2/test_oauth2_scopes.py +++ b/fittrackee/tests/oauth2/test_oauth2_scopes.py @@ -1,58 +1,28 @@ -from json import dumps - import pytest from flask import Flask -from werkzeug.test import TestResponse from fittrackee.users.models import User from ..mixins import ApiTestCaseMixin -from ..utils import random_short_id, random_string -class OAuth2ScopesTestCase(ApiTestCaseMixin): - def assert_expected_response( - self, response: TestResponse, client_scope: str, endpoint_scope: str - ) -> None: - if client_scope == endpoint_scope: - self.assert_not_insufficient_scope_error(response) - else: - self.assert_insufficient_scope(response) - - -class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase): - scope = 'read' - +class TestOAuth2Scopes(ApiTestCaseMixin): @pytest.mark.parametrize( - 'endpoint_url', + 'endpoint_url,scope', [ - '/api/auth/profile', - '/api/records', - '/api/sports', - '/api/sports/1', - f'/api/stats/{random_string()}/by_sport', - f'/api/stats/{random_string()}/by_time', - '/api/users/test', - '/api/workouts', - f'/api/workouts/{random_short_id()}', - f'/api/workouts/{random_short_id()}/chart_data', - f'/api/workouts/{random_short_id()}/chart_data/segment/1', - f'/api/workouts/{random_short_id()}/gpx', - f'/api/workouts/{random_short_id()}/gpx/download', - f'/api/workouts/{random_short_id()}/gpx/segment/1', + ('/api/auth/profile', 'profile:read'), + ('/api/workouts', 'workouts:read'), ], ) - def test_access_to_get_endpoints( - self, app: Flask, user_1: User, endpoint_url: str + def test_oauth_client_can_access_authorized_endpoints( + self, app: Flask, user_1: User, endpoint_url: str, scope: str ) -> None: ( client, oauth_client, access_token, _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) + ) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope) response = client.get( endpoint_url, @@ -60,208 +30,29 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase): headers=dict(Authorization=f'Bearer {access_token}'), ) - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='read' - ) - - @pytest.mark.parametrize( - 'endpoint_url', - ['/api/users'], - ) - def test_access_to_endpoints_as_admin( - self, app: Flask, user_1_admin: User, endpoint_url: str - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1_admin, scope=self.scope - ) - - response = client.get( - endpoint_url, - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='read' - ) - - @pytest.mark.parametrize( - 'endpoint_url', - [ - '/api/auth/picture', - '/api/auth/profile/edit', - '/api/auth/profile/edit/preferences', - '/api/auth/profile/edit/sports', - '/api/workouts', - '/api/workouts/no_gpx', - ], - ) - def test_access_post_endpoints( - self, app: Flask, user_1: User, endpoint_url: str - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) - - response = client.post( - endpoint_url, - data=dumps(dict()), - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='write' - ) - - @pytest.mark.parametrize( - 'endpoint_url', - [ - '/api/auth/profile/edit/account', - '/api/workouts/0', - ], - ) - def test_access_to_patch_endpoints( - self, app: Flask, user_1: User, endpoint_url: str - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) - - response = client.patch( - endpoint_url, - data=dumps(dict()), - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='write' - ) - - @pytest.mark.parametrize( - 'endpoint_url', - [ - '/api/config', - '/api/sports/1', - f'/api/users/{random_string()}', - ], - ) - def test_access_to_patch_endpoints_as_admin( - self, app: Flask, user_1_admin: User, endpoint_url: str - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1_admin, scope=self.scope - ) - - response = client.patch( - endpoint_url, - data=dumps(dict()), - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='write' - ) - - @pytest.mark.parametrize( - 'endpoint_url', - [ - '/api/auth/picture', - '/api/auth/profile/reset/sports/1', - f'/api/users/{random_string()}', - '/api/workouts/0', - ], - ) - def test_access_to_delete_endpoints( - self, app: Flask, user_1: User, endpoint_url: str - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) - user_1.picture = random_string() - - response = client.delete( - endpoint_url, - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - - self.assert_expected_response( - response, client_scope=self.scope, endpoint_scope='write' - ) - - -class TestOAuth2ScopesWithWriteAccess(TestOAuth2ScopesWithReadAccess): - scope = 'write' - - -class TestOAuth2ScopesWithReadAndWriteAccess(ApiTestCaseMixin): - scope = 'read write' - - def test_client_can_access_endpoint_with_read_scope( - self, app: Flask, user_1: User - ) -> None: - ( - client, - oauth_client, - access_token, - _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) - - response = client.get( - '/api/auth/profile', - content_type='application/json', - headers=dict(Authorization=f'Bearer {access_token}'), - ) - self.assert_not_insufficient_scope_error(response) - def test_client_with_read_can_access_endpoints_with_write_scope( - self, app: Flask, user_1: User + @pytest.mark.parametrize( + 'endpoint_url,scope', + [ + ('/api/auth/profile', 'workouts:read'), + ('/api/workouts', 'profile:read'), + ], + ) + def test_oauth_client_can_not_access_unauthorized_endpoints( + self, app: Flask, user_1: User, endpoint_url: str, scope: str ) -> None: ( client, oauth_client, access_token, _, - ) = self.create_oauth_client_and_issue_token( - app, user_1, scope=self.scope - ) + ) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope) - response = client.post( - '/api/auth/picture', - data=dumps(dict()), + response = client.get( + endpoint_url, content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) - self.assert_not_insufficient_scope_error(response) + self.assert_insufficient_scope(response) diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index 2b7b7510..08725dc9 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -494,6 +494,38 @@ class TestUserProfile(ApiTestCaseMixin): assert data['status'] == 'success' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', True), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, app: Flask, user_1: User, client_scope: str, can_access: bool + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + '/api/auth/profile', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserProfileUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( @@ -559,6 +591,42 @@ class TestUserProfileUpdate(ApiTestCaseMixin): assert data['message'] == 'user profile updated' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/auth/profile/edit', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserAccountUpdate(ApiTestCaseMixin): @staticmethod @@ -1193,6 +1261,42 @@ class TestUserAccountUpdate(ApiTestCaseMixin): password_change_email_mock, ) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.patch( + '/api/auth/profile/edit/account', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserPreferencesUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( @@ -1254,6 +1358,42 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin): assert data['message'] == 'user preferences updated' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/auth/profile/edit/preferences', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserSportPreferencesUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( @@ -1436,6 +1576,42 @@ class TestUserSportPreferencesUpdate(ApiTestCaseMixin): assert data['data']['is_active'] assert data['data']['stopped_speed_threshold'] == 0.5 + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/auth/profile/edit/sports', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserSportPreferencesReset(ApiTestCaseMixin): def test_it_returns_error_if_sport_does_not_exist( @@ -1491,6 +1667,44 @@ class TestUserSportPreferencesReset(ApiTestCaseMixin): assert response.status_code == 204 + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + sport_1_cycling: Sport, + user_sport_1_preference: UserSportPreference, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.delete( + f'/api/auth/profile/reset/sports/{sport_1_cycling.id}', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUserPicture(ApiTestCaseMixin): def test_it_returns_error_if_file_is_missing( @@ -1620,6 +1834,42 @@ class TestUserPicture(ApiTestCaseMixin): assert 'avatar.png' not in user_1.picture assert 'avatar2.png' in user_1.picture + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', True), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/auth/picture', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestRegistrationConfiguration(ApiTestCaseMixin): def test_it_returns_error_if_it_exceeds_max_users( diff --git a/fittrackee/tests/users/test_users_api.py b/fittrackee/tests/users/test_users_api.py index 55c5703e..c79d1ca2 100644 --- a/fittrackee/tests/users/test_users_api.py +++ b/fittrackee/tests/users/test_users_api.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from io import BytesIO from unittest.mock import MagicMock, patch +import pytest from flask import Flask from fittrackee.users.models import User, UserSportPreference @@ -131,6 +132,42 @@ class TestGetUser(ApiTestCaseMixin): self.assert_404_with_entity(response, 'user') + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', True), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.get( + '/api/users/not_existing', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetUsers(ApiTestCaseMixin): def test_it_returns_error_if_user_has_no_admin_rights( @@ -885,6 +922,42 @@ class TestGetUsers(ApiTestCaseMixin): 'total': 3, } + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', True), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.get( + '/api/users', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetUserPicture(ApiTestCaseMixin): def test_it_return_error_if_user_has_no_picture( @@ -1360,6 +1433,43 @@ class TestUpdateUser(ApiTestCaseMixin): assert user['is_active'] is True assert user_2.confirmation_token is None + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', True), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + user_2: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.patch( + f'/api/users/{user_2.username}', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestDeleteUser(ApiTestCaseMixin): def test_user_can_delete_its_own_account( @@ -1573,3 +1683,40 @@ class TestDeleteUser(ApiTestCaseMixin): ) self.assert_403(response, 'error, registration is disabled') + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', True), + ('workouts:read', False), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + user_2: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.delete( + f'/api/users/{user_2.username}', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/utils.py b/fittrackee/tests/utils.py index f1d7963b..aa8ebaf4 100644 --- a/fittrackee/tests/utils.py +++ b/fittrackee/tests/utils.py @@ -47,5 +47,5 @@ TEST_OAUTH_CLIENT_METADATA = { 'client_name': random_string(), 'client_uri': random_domain(), 'redirect_uris': [random_domain()], - 'scope': 'read write', + 'scope': 'profile:read workouts:read', } diff --git a/fittrackee/tests/workouts/test_records_api.py b/fittrackee/tests/workouts/test_records_api.py index 7d4f53d2..50d92e68 100644 --- a/fittrackee/tests/workouts/test_records_api.py +++ b/fittrackee/tests/workouts/test_records_api.py @@ -1,5 +1,6 @@ import json +import pytest from flask import Flask from fittrackee.users.models import User @@ -897,3 +898,40 @@ class TestGetRecords(ApiTestCaseMixin): response = client.get('/api/records') self.assert_401(response) + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + user_2: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.get( + '/api/records', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/workouts/test_sports_api.py b/fittrackee/tests/workouts/test_sports_api.py index cf6e8a3a..0a2ac24e 100644 --- a/fittrackee/tests/workouts/test_sports_api.py +++ b/fittrackee/tests/workouts/test_sports_api.py @@ -1,5 +1,6 @@ import json +import pytest from flask import Flask from fittrackee import db @@ -138,6 +139,43 @@ class TestGetSports(ApiTestCaseMixin): sport_2_running.serialize(is_admin=True) ) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + '/api/sports', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetSport(ApiTestCaseMixin): def test_it_gets_a_sport( @@ -241,6 +279,43 @@ class TestGetSport(ApiTestCaseMixin): sport_1_cycling_inactive.serialize(is_admin=True) ) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + f'/api/sports/{sport_1_cycling.id}', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestUpdateSport(ApiTestCaseMixin): def test_it_disables_a_sport( @@ -442,3 +517,41 @@ class TestUpdateSport(ApiTestCaseMixin): data = self.assert_404(response) assert len(data['data']['sports']) == 0 + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', True), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + user_2: User, + sport_1_cycling: Sport, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.patch( + f'/api/sports/{sport_1_cycling.id}', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/workouts/test_stats_api.py b/fittrackee/tests/workouts/test_stats_api.py index 0c78feb5..742e3f92 100644 --- a/fittrackee/tests/workouts/test_stats_api.py +++ b/fittrackee/tests/workouts/test_stats_api.py @@ -1,5 +1,6 @@ import json +import pytest from flask import Flask from fittrackee.users.models import User @@ -862,6 +863,42 @@ class TestGetStatsByTime(ApiTestCaseMixin): } } + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + f'/api/stats/{user_1.username}/by_time', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetStatsBySport(ApiTestCaseMixin): def test_it_returns_error_if_user_is_not_authenticated( @@ -1007,6 +1044,42 @@ class TestGetStatsBySport(ApiTestCaseMixin): self.assert_500(response) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + f'/api/stats/{user_1.username}/by_sport', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetAllStats(ApiTestCaseMixin): def test_it_returns_error_if_user_is_not_authenticated( @@ -1089,3 +1162,39 @@ class TestGetAllStats(ApiTestCaseMixin): ) self.assert_403(response) + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1_admin: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1_admin, scope=client_scope + ) + + response = client.get( + '/api/stats/all', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/workouts/test_workouts_api_0_get.py b/fittrackee/tests/workouts/test_workouts_api_0_get.py index cafcb620..2c912221 100644 --- a/fittrackee/tests/workouts/test_workouts_api_0_get.py +++ b/fittrackee/tests/workouts/test_workouts_api_0_get.py @@ -3,6 +3,7 @@ from typing import List from unittest.mock import patch from uuid import uuid4 +import pytest from flask import Flask from fittrackee.users.models import User @@ -101,6 +102,42 @@ class TestGetWorkouts(ApiTestCaseMixin): self.assert_401(response, 'provide a valid auth token') + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + '/api/workouts', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestGetWorkoutsWithPagination(ApiTestCaseMixin): def test_it_gets_workouts_with_default_pagination( @@ -1158,6 +1195,55 @@ class TestGetWorkout(ApiTestCaseMixin): self.assert_404_with_message(response, 'Map does not exist') + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + @pytest.mark.parametrize( + 'endpoint', + [ + '/api/workouts/{workout_short_id}', + '/api/workouts/{workout_short_id}/gpx', + '/api/workouts/{workout_short_id}/chart_data', + '/api/workouts/{workout_short_id}/gpx/segment/1', + '/api/workouts/{workout_short_id}/chart_data/segment/1', + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + client_scope: str, + can_access: bool, + endpoint: str, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + endpoint.format(workout_short_id=workout_cycling_user_1.short_id), + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestDownloadWorkoutGpx(ApiTestCaseMixin): def test_it_returns_404_if_workout_does_not_exist( @@ -1242,3 +1328,41 @@ class TestDownloadWorkoutGpx(ApiTestCaseMixin): mimetype='application/gpx+xml', as_attachment=True, ) + + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', True), + ('workouts:write', False), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.get( + f'/api/workouts/{workout_cycling_user_1.short_id}/gpx/download', + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) diff --git a/fittrackee/tests/workouts/test_workouts_api_1_post.py b/fittrackee/tests/workouts/test_workouts_api_1_post.py index f75320ec..d056eea8 100644 --- a/fittrackee/tests/workouts/test_workouts_api_1_post.py +++ b/fittrackee/tests/workouts/test_workouts_api_1_post.py @@ -621,6 +621,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin): ) assert 'data' not in data + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', True), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/workouts', + data=dict(), + headers=dict( + content_type='multipart/form-data', + Authorization=f'Bearer {access_token}', + ), + ) + + self.assert_response_scope(response, can_access) + class TestPostWorkoutWithoutGpx(ApiTestCaseMixin): def test_it_returns_error_if_user_is_not_authenticated( @@ -763,6 +802,45 @@ class TestPostWorkoutWithoutGpx(ApiTestCaseMixin): assert len(data['data']['workouts'][0]['segments']) == 0 assert len(data['data']['workouts'][0]['records']) == 0 + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', True), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.post( + '/api/workouts/no_gpx', + data=dict(), + headers=dict( + content_type='multipart/form-data', + Authorization=f'Bearer {access_token}', + ), + ) + + self.assert_response_scope(response, can_access) + class TestPostWorkoutWithZipArchive(ApiTestCaseMixin): def test_it_adds_workouts_with_zip_archive( diff --git a/fittrackee/tests/workouts/test_workouts_api_2_patch.py b/fittrackee/tests/workouts/test_workouts_api_2_patch.py index cf3143bc..09be7314 100644 --- a/fittrackee/tests/workouts/test_workouts_api_2_patch.py +++ b/fittrackee/tests/workouts/test_workouts_api_2_patch.py @@ -221,6 +221,45 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin): self.assert_500(response) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', True), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.patch( + f'/api/workouts/{workout_cycling_user_1.short_id}', + data=dict(), + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestEditWorkoutWithoutGpx(ApiTestCaseMixin): def test_it_updates_a_workout_wo_gpx( diff --git a/fittrackee/tests/workouts/test_workouts_api_3_delete.py b/fittrackee/tests/workouts/test_workouts_api_3_delete.py index 86c5684f..d780883a 100644 --- a/fittrackee/tests/workouts/test_workouts_api_3_delete.py +++ b/fittrackee/tests/workouts/test_workouts_api_3_delete.py @@ -1,5 +1,6 @@ import os +import pytest from flask import Flask from fittrackee.files import get_absolute_file_path @@ -80,6 +81,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin): self.assert_500(response) + @pytest.mark.parametrize( + 'client_scope, can_access', + [ + ('application:write', False), + ('profile:read', False), + ('profile:write', False), + ('users:read', False), + ('users:write', False), + ('workouts:read', False), + ('workouts:write', True), + ], + ) + def test_expected_scopes_are_defined( + self, + app: Flask, + user_1: User, + sport_1_cycling: Sport, + workout_cycling_user_1: Workout, + client_scope: str, + can_access: bool, + ) -> None: + ( + client, + oauth_client, + access_token, + _, + ) = self.create_oauth_client_and_issue_token( + app, user_1, scope=client_scope + ) + + response = client.delete( + f'/api/workouts/{workout_cycling_user_1.short_id}', + data=dict(), + content_type='application/json', + headers=dict(Authorization=f'Bearer {access_token}'), + ) + + self.assert_response_scope(response, can_access) + class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin): def test_it_deletes_a_workout_wo_gpx( diff --git a/fittrackee/users/auth.py b/fittrackee/users/auth.py index 50325ed2..c31aef96 100644 --- a/fittrackee/users/auth.py +++ b/fittrackee/users/auth.py @@ -252,7 +252,7 @@ def login_user() -> Union[Dict, HttpResponse]: @auth_blueprint.route('/auth/profile', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['profile:read']) def get_authenticated_user_profile( auth_user: User, ) -> Union[Dict, HttpResponse]: @@ -354,7 +354,7 @@ def get_authenticated_user_profile( @auth_blueprint.route('/auth/profile/edit', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: """ edit authenticated user profile @@ -502,7 +502,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: @auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: """ update authenticated user email and password @@ -712,7 +712,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: @auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: """ edit authenticated user preferences @@ -853,7 +853,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: @auth_blueprint.route('/auth/profile/edit/sports', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def edit_user_sport_preferences( auth_user: User, ) -> Union[Dict, HttpResponse]: @@ -959,7 +959,7 @@ def edit_user_sport_preferences( @auth_blueprint.route( '/auth/profile/reset/sports/', methods=['DELETE'] ) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def reset_user_sport_preferences( auth_user: User, sport_id: int ) -> Union[Tuple[Dict, int], HttpResponse]: @@ -1014,7 +1014,7 @@ def reset_user_sport_preferences( @auth_blueprint.route('/auth/picture', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: """ update authenticated user picture @@ -1102,7 +1102,7 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: @auth_blueprint.route('/auth/picture', methods=['DELETE']) -@require_auth(scopes='write') +@require_auth(scopes=['profile:write']) def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: """ delete authenticated user picture diff --git a/fittrackee/users/users.py b/fittrackee/users/users.py index f9c387f4..a26aaec7 100644 --- a/fittrackee/users/users.py +++ b/fittrackee/users/users.py @@ -34,7 +34,7 @@ USER_PER_PAGE = 10 @users_blueprint.route('/users', methods=['GET']) -@require_auth(scopes='read', as_admin=True) +@require_auth(scopes=['users:read'], as_admin=True) def get_users(auth_user: User) -> Dict: """ Get all users (regardless their account status), if authenticated user @@ -235,7 +235,7 @@ def get_users(auth_user: User) -> Dict: @users_blueprint.route('/users/', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['users:read']) def get_single_user( auth_user: User, user_name: str ) -> Union[Dict, HttpResponse]: @@ -394,7 +394,7 @@ def get_picture(user_name: str) -> Any: @users_blueprint.route('/users/', methods=['PATCH']) -@require_auth(scopes='write', as_admin=True) +@require_auth(scopes=['users:write'], as_admin=True) def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: """ Update user account @@ -593,7 +593,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: @users_blueprint.route('/users/', methods=['DELETE']) -@require_auth(scopes='write') +@require_auth(scopes=['users:write']) def delete_user( auth_user: User, user_name: str ) -> Union[Tuple[Dict, int], HttpResponse]: diff --git a/fittrackee/workouts/records.py b/fittrackee/workouts/records.py index e7519318..544a6b03 100644 --- a/fittrackee/workouts/records.py +++ b/fittrackee/workouts/records.py @@ -11,7 +11,7 @@ records_blueprint = Blueprint('records', __name__) @records_blueprint.route('/records', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_records(auth_user: User) -> Dict: """ Get all records for authenticated user. diff --git a/fittrackee/workouts/sports.py b/fittrackee/workouts/sports.py index 8016ecb8..290c4d19 100644 --- a/fittrackee/workouts/sports.py +++ b/fittrackee/workouts/sports.py @@ -19,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__) @sports_blueprint.route('/sports', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_sports(auth_user: User) -> Dict: """ Get all sports @@ -195,7 +195,7 @@ def get_sports(auth_user: User) -> Dict: @sports_blueprint.route('/sports/', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: """ Get a sport @@ -304,7 +304,7 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: @sports_blueprint.route('/sports/', methods=['PATCH']) -@require_auth(scopes='write', as_admin=True) +@require_auth(scopes=['workouts:write'], as_admin=True) def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: """ Update a sport diff --git a/fittrackee/workouts/stats.py b/fittrackee/workouts/stats.py index 2a3d4ca1..3ce2b962 100644 --- a/fittrackee/workouts/stats.py +++ b/fittrackee/workouts/stats.py @@ -174,7 +174,7 @@ def get_workouts( @stats_blueprint.route('/stats//by_time', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workouts_by_time( auth_user: User, user_name: str ) -> Union[Dict, HttpResponse]: @@ -281,7 +281,7 @@ def get_workouts_by_time( @stats_blueprint.route('/stats//by_sport', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workouts_by_sport( auth_user: User, user_name: str ) -> Union[Dict, HttpResponse]: @@ -377,7 +377,7 @@ def get_workouts_by_sport( @stats_blueprint.route('/stats/all', methods=['GET']) -@require_auth(as_admin=True) +@require_auth(scopes=['workouts:read'], as_admin=True) def get_application_stats(auth_user: User) -> Dict: """ Get all application statistics diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index 72a9d748..9a55ab01 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -56,7 +56,7 @@ MAX_WORKOUTS_PER_PAGE = 100 @workouts_blueprint.route('/workouts', methods=['GET']) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: """ Get workouts for the authenticated user. @@ -298,7 +298,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: @workouts_blueprint.route( '/workouts/', methods=['GET'] ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workout( auth_user: User, workout_short_id: str ) -> Union[Dict, HttpResponse]: @@ -462,7 +462,7 @@ def get_workout_data( @workouts_blueprint.route( '/workouts//gpx', methods=['GET'] ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workout_gpx( auth_user: User, workout_short_id: str ) -> Union[Dict, HttpResponse]: @@ -512,7 +512,7 @@ def get_workout_gpx( @workouts_blueprint.route( '/workouts//chart_data', methods=['GET'] ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_workout_chart_data( auth_user: User, workout_short_id: str ) -> Union[Dict, HttpResponse]: @@ -582,7 +582,7 @@ def get_workout_chart_data( '/workouts//gpx/segment/', methods=['GET'], ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_segment_gpx( auth_user: User, workout_short_id: str, segment_id: int ) -> Union[Dict, HttpResponse]: @@ -634,7 +634,7 @@ def get_segment_gpx( '', methods=['GET'], ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def get_segment_chart_data( auth_user: User, workout_short_id: str, segment_id: int ) -> Union[Dict, HttpResponse]: @@ -705,7 +705,7 @@ def get_segment_chart_data( @workouts_blueprint.route( '/workouts//gpx/download', methods=['GET'] ) -@require_auth(scopes='read') +@require_auth(scopes=['workouts:read']) def download_workout_gpx( auth_user: User, workout_short_id: str ) -> Union[HttpResponse, Response]: @@ -848,7 +848,7 @@ def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]: @workouts_blueprint.route('/workouts', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['workouts:write']) def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: """ Post a workout with a gpx file @@ -1016,7 +1016,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: @workouts_blueprint.route('/workouts/no_gpx', methods=['POST']) -@require_auth(scopes='write') +@require_auth(scopes=['workouts:write']) def post_workout_no_gpx( auth_user: User, ) -> Union[Tuple[Dict, int], HttpResponse]: @@ -1165,7 +1165,7 @@ def post_workout_no_gpx( @workouts_blueprint.route( '/workouts/', methods=['PATCH'] ) -@require_auth(scopes='write') +@require_auth(scopes=['workouts:write']) def update_workout( auth_user: User, workout_short_id: str ) -> Union[Dict, HttpResponse]: @@ -1313,7 +1313,7 @@ def update_workout( @workouts_blueprint.route( '/workouts/', methods=['DELETE'] ) -@require_auth(scopes='write') +@require_auth(scopes=['workouts:write']) def delete_workout( auth_user: User, workout_short_id: str ) -> Union[Tuple[Dict, int], HttpResponse]: