API - add scope on endpoints
This commit is contained in:
		| @@ -67,7 +67,7 @@ def get_application_config() -> Union[Dict, HttpResponse]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @config_blueprint.route('/config', methods=['PATCH']) | @config_blueprint.route('/config', methods=['PATCH']) | ||||||
| @require_auth(as_admin=True) | @require_auth(scopes='write', as_admin=True) | ||||||
| def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]: | def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Update Application config |     Update Application config | ||||||
|   | |||||||
| @@ -6,6 +6,28 @@ from werkzeug.security import gen_salt | |||||||
| from fittrackee.oauth2.models import OAuth2Client | from fittrackee.oauth2.models import OAuth2Client | ||||||
| from fittrackee.users.models import User | from fittrackee.users.models import User | ||||||
|  |  | ||||||
|  | DEFAULT_SCOPE = 'read' | ||||||
|  | VALID_SCOPES = ['read', '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 | ||||||
|  |  | ||||||
|  |     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) | ||||||
|  |  | ||||||
|  |     return ' '.join(valid_scopes) | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_oauth_client(metadata: Dict, user: User) -> OAuth2Client: | def create_oauth_client(metadata: Dict, user: User) -> OAuth2Client: | ||||||
|     """ |     """ | ||||||
| @@ -18,7 +40,7 @@ def create_oauth_client(metadata: Dict, user: User) -> OAuth2Client: | |||||||
|         'client_name': metadata['client_name'], |         'client_name': metadata['client_name'], | ||||||
|         'client_uri': metadata['client_uri'], |         'client_uri': metadata['client_uri'], | ||||||
|         'redirect_uris': metadata['redirect_uris'], |         'redirect_uris': metadata['redirect_uris'], | ||||||
|         'scope': metadata['scope'], |         'scope': check_scope(metadata['scope']), | ||||||
|         'grant_types': ['authorization_code', 'refresh_token'], |         'grant_types': ['authorization_code', 'refresh_token'], | ||||||
|         'response_types': ['code'], |         'response_types': ['code'], | ||||||
|         'token_endpoint_auth_method': 'client_secret_post', |         'token_endpoint_auth_method': 'client_secret_post', | ||||||
|   | |||||||
| @@ -63,24 +63,33 @@ class ApiTestCaseMixin(RandomMixin): | |||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def create_oauth_client( |     def create_oauth_client( | ||||||
|         user: User, metadata: Optional[Dict] = None |         user: User, | ||||||
|  |         metadata: Optional[Dict] = None, | ||||||
|  |         scope: Optional[str] = None, | ||||||
|     ) -> OAuth2Client: |     ) -> OAuth2Client: | ||||||
|         oauth_client = create_oauth_client( |         client_metadata = ( | ||||||
|             TEST_OAUTH_CLIENT_METADATA if metadata is None else metadata, user |             TEST_OAUTH_CLIENT_METADATA if metadata is None else metadata | ||||||
|         ) |         ) | ||||||
|  |         if scope is not None: | ||||||
|  |             client_metadata['scope'] = scope | ||||||
|  |         oauth_client = create_oauth_client(client_metadata, user) | ||||||
|         db.session.add(oauth_client) |         db.session.add(oauth_client) | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|         return oauth_client |         return oauth_client | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def authorize_client( |     def authorize_client( | ||||||
|         client: FlaskClient, oauth_client: OAuth2Client, auth_token: str |         client: FlaskClient, | ||||||
|  |         oauth_client: OAuth2Client, | ||||||
|  |         auth_token: str, | ||||||
|  |         scope: Optional[str] = None, | ||||||
|     ) -> Union[List[str], str]: |     ) -> Union[List[str], str]: | ||||||
|         response = client.post( |         response = client.post( | ||||||
|             '/api/oauth/authorize', |             '/api/oauth/authorize', | ||||||
|             data={ |             data={ | ||||||
|                 'client_id': oauth_client.client_id, |                 'client_id': oauth_client.client_id, | ||||||
|                 'response_type': 'code', |                 'response_type': 'code', | ||||||
|  |                 'scope': 'read' if not scope else scope, | ||||||
|             }, |             }, | ||||||
|             headers=dict( |             headers=dict( | ||||||
|                 Authorization=f'Bearer {auth_token}', |                 Authorization=f'Bearer {auth_token}', | ||||||
| @@ -92,13 +101,15 @@ class ApiTestCaseMixin(RandomMixin): | |||||||
|         return code |         return code | ||||||
|  |  | ||||||
|     def create_oauth_client_and_issue_token( |     def create_oauth_client_and_issue_token( | ||||||
|         self, app: Flask, user: User |         self, app: Flask, user: User, scope: Optional[str] = None | ||||||
|     ) -> Tuple[FlaskClient, OAuth2Client, str]: |     ) -> Tuple[FlaskClient, OAuth2Client, str]: | ||||||
|         client, auth_token = self.get_test_client_and_auth_token( |         client, auth_token = self.get_test_client_and_auth_token( | ||||||
|             app, user.email |             app, user.email | ||||||
|         ) |         ) | ||||||
|         oauth_client = self.create_oauth_client(user) |         oauth_client = self.create_oauth_client(user, scope=scope) | ||||||
|         code = self.authorize_client(client, oauth_client, auth_token) |         code = self.authorize_client( | ||||||
|  |             client, oauth_client, auth_token, scope=scope | ||||||
|  |         ) | ||||||
|         response = client.post( |         response = client.post( | ||||||
|             '/api/oauth/token', |             '/api/oauth/token', | ||||||
|             data={ |             data={ | ||||||
| @@ -217,6 +228,31 @@ class ApiTestCaseMixin(RandomMixin): | |||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def assert_insufficient_scope(response: TestResponse) -> Dict: | ||||||
|  |         return assert_oauth_errored_response( | ||||||
|  |             response, | ||||||
|  |             403, | ||||||
|  |             error='insufficient_scope', | ||||||
|  |             error_description=( | ||||||
|  |                 'The request requires higher privileges than provided by ' | ||||||
|  |                 'the access token.' | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @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'] | ||||||
|  |  | ||||||
|  |  | ||||||
| class CallArgsMixin: | class CallArgsMixin: | ||||||
|     @staticmethod |     @staticmethod | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| from time import time | from time import time | ||||||
| from typing import Dict | from typing import Any, Dict | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
|  | import pytest | ||||||
| from flask import Flask | from flask import Flask | ||||||
|  |  | ||||||
| from fittrackee.oauth2.client import create_oauth_client | from fittrackee.oauth2.client import check_scope, create_oauth_client | ||||||
| from fittrackee.oauth2.models import OAuth2Client | from fittrackee.oauth2.models import OAuth2Client | ||||||
| from fittrackee.users.models import User | from fittrackee.users.models import User | ||||||
|  |  | ||||||
| @@ -106,7 +107,7 @@ class TestCreateOAuth2Client: | |||||||
|     def test_oauth_client_has_expected_scope( |     def test_oauth_client_has_expected_scope( | ||||||
|         self, app: Flask, user_1: User |         self, app: Flask, user_1: User | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         scope = 'profile' |         scope = 'write' | ||||||
|         client_metadata: Dict = {**TEST_METADATA, 'scope': scope} |         client_metadata: Dict = {**TEST_METADATA, 'scope': scope} | ||||||
|  |  | ||||||
|         oauth_client = create_oauth_client(client_metadata, user_1) |         oauth_client = create_oauth_client(client_metadata, user_1) | ||||||
| @@ -138,3 +139,28 @@ class TestCreateOAuth2Client: | |||||||
|         oauth_client = create_oauth_client(TEST_METADATA, user_1) |         oauth_client = create_oauth_client(TEST_METADATA, user_1) | ||||||
|  |  | ||||||
|         assert oauth_client.user_id == user_1.id |         assert oauth_client.user_id == user_1.id | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestOAuthCheckScopes: | ||||||
|  |     @pytest.mark.parametrize( | ||||||
|  |         'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']] | ||||||
|  |     ) | ||||||
|  |     def test_it_returns_read_if_scope_is_invalid( | ||||||
|  |         self, input_scope: Any | ||||||
|  |     ) -> None: | ||||||
|  |         assert check_scope(input_scope) == 'read' | ||||||
|  |  | ||||||
|  |     @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'), | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |     def test_it_return_only_valid_scopes( | ||||||
|  |         self, input_scope: str, expected_scope: str | ||||||
|  |     ) -> None: | ||||||
|  |         assert check_scope(input_scope) == expected_scope | ||||||
|   | |||||||
							
								
								
									
										259
									
								
								fittrackee/tests/oauth2/test_oauth2_scopes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								fittrackee/tests/oauth2/test_oauth2_scopes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | |||||||
|  | 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' | ||||||
|  |  | ||||||
|  |     @pytest.mark.parametrize( | ||||||
|  |         'endpoint_url', | ||||||
|  |         [ | ||||||
|  |             '/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', | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |     def test_access_to_get_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.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/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 | ||||||
|  |     ) -> None: | ||||||
|  |         ( | ||||||
|  |             client, | ||||||
|  |             oauth_client, | ||||||
|  |             access_token, | ||||||
|  |         ) = self.create_oauth_client_and_issue_token( | ||||||
|  |             app, user_1, scope=self.scope | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = client.post( | ||||||
|  |             '/api/auth/picture', | ||||||
|  |             data=dumps(dict()), | ||||||
|  |             content_type='application/json', | ||||||
|  |             headers=dict(Authorization=f'Bearer {access_token}'), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assert_not_insufficient_scope_error(response) | ||||||
| @@ -2,9 +2,12 @@ import random | |||||||
| import string | import string | ||||||
| from json import loads | from json import loads | ||||||
| from typing import Dict, Optional | from typing import Dict, Optional | ||||||
|  | from uuid import uuid4 | ||||||
|  |  | ||||||
| from flask import json as flask_json | from flask import json as flask_json | ||||||
|  |  | ||||||
|  | from fittrackee.workouts.utils.short_id import encode_uuid | ||||||
|  |  | ||||||
|  |  | ||||||
| def random_string( | def random_string( | ||||||
|     length: Optional[int] = None, |     length: Optional[int] = None, | ||||||
| @@ -32,6 +35,10 @@ def random_email() -> str: | |||||||
|     return random_string(suffix='@example.com') |     return random_string(suffix='@example.com') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def random_short_id() -> str: | ||||||
|  |     return encode_uuid(uuid4()) | ||||||
|  |  | ||||||
|  |  | ||||||
| def jsonify_dict(data: Dict) -> Dict: | def jsonify_dict(data: Dict) -> Dict: | ||||||
|     return loads(flask_json.dumps(data)) |     return loads(flask_json.dumps(data)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -252,7 +252,7 @@ def login_user() -> Union[Dict, HttpResponse]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @auth_blueprint.route('/auth/profile', methods=['GET']) | @auth_blueprint.route('/auth/profile', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_authenticated_user_profile( | def get_authenticated_user_profile( | ||||||
|     auth_user: User, |     auth_user: User, | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -354,7 +354,7 @@ def get_authenticated_user_profile( | |||||||
|  |  | ||||||
|  |  | ||||||
| @auth_blueprint.route('/auth/profile/edit', methods=['POST']) | @auth_blueprint.route('/auth/profile/edit', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: | def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     edit authenticated user profile |     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']) | @auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: | def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     update authenticated user email and password |     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']) | @auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: | def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     edit authenticated user preferences |     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']) | @auth_blueprint.route('/auth/profile/edit/sports', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def edit_user_sport_preferences( | def edit_user_sport_preferences( | ||||||
|     auth_user: User, |     auth_user: User, | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -959,7 +959,7 @@ def edit_user_sport_preferences( | |||||||
| @auth_blueprint.route( | @auth_blueprint.route( | ||||||
|     '/auth/profile/reset/sports/<sport_id>', methods=['DELETE'] |     '/auth/profile/reset/sports/<sport_id>', methods=['DELETE'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def reset_user_sport_preferences( | def reset_user_sport_preferences( | ||||||
|     auth_user: User, sport_id: int |     auth_user: User, sport_id: int | ||||||
| ) -> Union[Tuple[Dict, int], HttpResponse]: | ) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
| @@ -1014,7 +1014,7 @@ def reset_user_sport_preferences( | |||||||
|  |  | ||||||
|  |  | ||||||
| @auth_blueprint.route('/auth/picture', methods=['POST']) | @auth_blueprint.route('/auth/picture', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: | def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     update authenticated user picture |     update authenticated user picture | ||||||
| @@ -1102,7 +1102,7 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @auth_blueprint.route('/auth/picture', methods=['DELETE']) | @auth_blueprint.route('/auth/picture', methods=['DELETE']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: | def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     delete authenticated user picture |     delete authenticated user picture | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ USER_PER_PAGE = 10 | |||||||
|  |  | ||||||
|  |  | ||||||
| @users_blueprint.route('/users', methods=['GET']) | @users_blueprint.route('/users', methods=['GET']) | ||||||
| @require_auth(as_admin=True) | @require_auth(scopes='read', as_admin=True) | ||||||
| def get_users(auth_user: User) -> Dict: | def get_users(auth_user: User) -> Dict: | ||||||
|     """ |     """ | ||||||
|     Get all users (regardless their account status), if authenticated user |     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/<user_name>', methods=['GET']) | @users_blueprint.route('/users/<user_name>', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_single_user( | def get_single_user( | ||||||
|     auth_user: User, user_name: str |     auth_user: User, user_name: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -394,7 +394,7 @@ def get_picture(user_name: str) -> Any: | |||||||
|  |  | ||||||
|  |  | ||||||
| @users_blueprint.route('/users/<user_name>', methods=['PATCH']) | @users_blueprint.route('/users/<user_name>', methods=['PATCH']) | ||||||
| @require_auth(as_admin=True) | @require_auth(scopes='write', as_admin=True) | ||||||
| def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: | def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Update user account |     Update user account | ||||||
| @@ -593,7 +593,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @users_blueprint.route('/users/<user_name>', methods=['DELETE']) | @users_blueprint.route('/users/<user_name>', methods=['DELETE']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def delete_user( | def delete_user( | ||||||
|     auth_user: User, user_name: str |     auth_user: User, user_name: str | ||||||
| ) -> Union[Tuple[Dict, int], HttpResponse]: | ) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ records_blueprint = Blueprint('records', __name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| @records_blueprint.route('/records', methods=['GET']) | @records_blueprint.route('/records', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_records(auth_user: User) -> Dict: | def get_records(auth_user: User) -> Dict: | ||||||
|     """ |     """ | ||||||
|     Get all records for authenticated user. |     Get all records for authenticated user. | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| @sports_blueprint.route('/sports', methods=['GET']) | @sports_blueprint.route('/sports', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_sports(auth_user: User) -> Dict: | def get_sports(auth_user: User) -> Dict: | ||||||
|     """ |     """ | ||||||
|     Get all sports |     Get all sports | ||||||
| @@ -195,7 +195,7 @@ def get_sports(auth_user: User) -> Dict: | |||||||
|  |  | ||||||
|  |  | ||||||
| @sports_blueprint.route('/sports/<int:sport_id>', methods=['GET']) | @sports_blueprint.route('/sports/<int:sport_id>', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: | def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Get a sport |     Get a sport | ||||||
| @@ -304,7 +304,7 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: | |||||||
|  |  | ||||||
|  |  | ||||||
| @sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH']) | @sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH']) | ||||||
| @require_auth(as_admin=True) | @require_auth(scopes='write', as_admin=True) | ||||||
| def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: | def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Update a sport |     Update a sport | ||||||
|   | |||||||
| @@ -174,7 +174,7 @@ def get_workouts( | |||||||
|  |  | ||||||
|  |  | ||||||
| @stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET']) | @stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workouts_by_time( | def get_workouts_by_time( | ||||||
|     auth_user: User, user_name: str |     auth_user: User, user_name: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -281,7 +281,7 @@ def get_workouts_by_time( | |||||||
|  |  | ||||||
|  |  | ||||||
| @stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET']) | @stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workouts_by_sport( | def get_workouts_by_sport( | ||||||
|     auth_user: User, user_name: str |     auth_user: User, user_name: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ MAX_WORKOUTS_PER_PAGE = 100 | |||||||
|  |  | ||||||
|  |  | ||||||
| @workouts_blueprint.route('/workouts', methods=['GET']) | @workouts_blueprint.route('/workouts', methods=['GET']) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: | def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Get workouts for the authenticated user. |     Get workouts for the authenticated user. | ||||||
| @@ -298,7 +298,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>', methods=['GET'] |     '/workouts/<string:workout_short_id>', methods=['GET'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workout( | def get_workout( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -462,7 +462,7 @@ def get_workout_data( | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>/gpx', methods=['GET'] |     '/workouts/<string:workout_short_id>/gpx', methods=['GET'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workout_gpx( | def get_workout_gpx( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -512,7 +512,7 @@ def get_workout_gpx( | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>/chart_data', methods=['GET'] |     '/workouts/<string:workout_short_id>/chart_data', methods=['GET'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_workout_chart_data( | def get_workout_chart_data( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -582,7 +582,7 @@ def get_workout_chart_data( | |||||||
|     '/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>', |     '/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>', | ||||||
|     methods=['GET'], |     methods=['GET'], | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_segment_gpx( | def get_segment_gpx( | ||||||
|     auth_user: User, workout_short_id: str, segment_id: int |     auth_user: User, workout_short_id: str, segment_id: int | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -634,7 +634,7 @@ def get_segment_gpx( | |||||||
|     '<int:segment_id>', |     '<int:segment_id>', | ||||||
|     methods=['GET'], |     methods=['GET'], | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def get_segment_chart_data( | def get_segment_chart_data( | ||||||
|     auth_user: User, workout_short_id: str, segment_id: int |     auth_user: User, workout_short_id: str, segment_id: int | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -705,7 +705,7 @@ def get_segment_chart_data( | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>/gpx/download', methods=['GET'] |     '/workouts/<string:workout_short_id>/gpx/download', methods=['GET'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='read') | ||||||
| def download_workout_gpx( | def download_workout_gpx( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[HttpResponse, Response]: | ) -> 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']) | @workouts_blueprint.route('/workouts', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: | def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
|     """ |     """ | ||||||
|     Post an workout with a gpx file |     Post an 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']) | @workouts_blueprint.route('/workouts/no_gpx', methods=['POST']) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def post_workout_no_gpx( | def post_workout_no_gpx( | ||||||
|     auth_user: User, |     auth_user: User, | ||||||
| ) -> Union[Tuple[Dict, int], HttpResponse]: | ) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
| @@ -1164,7 +1164,7 @@ def post_workout_no_gpx( | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>', methods=['PATCH'] |     '/workouts/<string:workout_short_id>', methods=['PATCH'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def update_workout( | def update_workout( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[Dict, HttpResponse]: | ) -> Union[Dict, HttpResponse]: | ||||||
| @@ -1311,7 +1311,7 @@ def update_workout( | |||||||
| @workouts_blueprint.route( | @workouts_blueprint.route( | ||||||
|     '/workouts/<string:workout_short_id>', methods=['DELETE'] |     '/workouts/<string:workout_short_id>', methods=['DELETE'] | ||||||
| ) | ) | ||||||
| @require_auth() | @require_auth(scopes='write') | ||||||
| def delete_workout( | def delete_workout( | ||||||
|     auth_user: User, workout_short_id: str |     auth_user: User, workout_short_id: str | ||||||
| ) -> Union[Tuple[Dict, int], HttpResponse]: | ) -> Union[Tuple[Dict, int], HttpResponse]: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user