API - update OAuth2 scopes

This commit is contained in:
Sam 2022-06-15 19:16:14 +02:00
parent 969a92b8d4
commit 8b2543eb61
25 changed files with 1111 additions and 293 deletions

View File

@ -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(scopes='write', as_admin=True) @require_auth(scopes=['application: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

View File

@ -3,28 +3,37 @@ from typing import Dict
from werkzeug.security import gen_salt from werkzeug.security import gen_salt
from fittrackee.oauth2.models import OAuth2Client
from fittrackee.users.models import User from fittrackee.users.models import User
DEFAULT_SCOPE = 'read' from .exceptions import InvalidOAuth2Scopes
VALID_SCOPES = ['read', 'write'] 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: def check_scope(scope: str) -> str:
""" """
Verify if provided scope is valid. Verify if provided scope is valid.
If not, it returns the default scope ('read').
""" """
valid_scopes = []
if not isinstance(scope, str) or not scope: if not isinstance(scope, str) or not scope:
return DEFAULT_SCOPE raise InvalidOAuth2Scopes()
valid_scopes = []
scopes = scope.split() scopes = scope.split()
for value in scopes: for value in scopes:
if value in VALID_SCOPES: if value in VALID_SCOPES:
valid_scopes.append(value) valid_scopes.append(value)
if len(valid_scopes) == 0:
valid_scopes.append(DEFAULT_SCOPE) if not valid_scopes:
raise InvalidOAuth2Scopes()
return ' '.join(valid_scopes) return ' '.join(valid_scopes)

View File

@ -0,0 +1,2 @@
class InvalidOAuth2Scopes(Exception):
...

View File

@ -5,8 +5,6 @@ from flask import Blueprint, Response, request
from urllib3.util import parse_url from urllib3.util import parse_url
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse, HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
@ -15,7 +13,9 @@ from fittrackee.responses import (
from fittrackee.users.models import User from fittrackee.users.models import User
from .client import create_oauth_client 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__) oauth_blueprint = Blueprint('oauth', __name__)
@ -86,7 +86,13 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
) )
) )
try:
new_client = create_oauth_client(client_metadata, auth_user) 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.add(new_client)
db.session.commit() db.session.commit()
return ( return (
@ -134,10 +140,10 @@ def get_client_by_id(
return get_client(auth_user, client_id=client_id, client_client_id=None) return get_client(auth_user, client_id=client_id, client_client_id=None)
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['DELETE']) @oauth_blueprint.route('/oauth/apps/<int:client_id>', methods=['DELETE'])
@require_auth() @require_auth()
def delete_client( def delete_client(
auth_user: User, client_id: str auth_user: User, client_id: int
) -> Union[Tuple[Dict, int], HttpResponse]: ) -> Union[Tuple[Dict, int], HttpResponse]:
client = OAuth2Client.query.filter_by( client = OAuth2Client.query.filter_by(
id=client_id, id=client_id,

View File

@ -324,3 +324,39 @@ class TestUpdateConfig(ApiTestCaseMixin):
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['admin_contact'] is None 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)

View File

@ -268,15 +268,14 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
@staticmethod @staticmethod
def assert_not_insufficient_scope_error(response: TestResponse) -> None: def assert_not_insufficient_scope_error(response: TestResponse) -> None:
assert response.status_code != 403 assert response.status_code != 403
if response.status_code != 204:
data = json.loads(response.data.decode()) def assert_response_scope(
if 'error' in data: self, response: TestResponse, can_access: bool
assert 'insufficient_scope' not in data['error'] ) -> None:
if 'error_description' in data: if can_access:
assert ( self.assert_not_insufficient_scope_error(response)
'The request requires higher privileges than provided by ' else:
'the access token.' self.assert_insufficient_scope(response)
) != data['error_description']
class CallArgsMixin: class CallArgsMixin:

View File

@ -6,6 +6,7 @@ import pytest
from flask import Flask from flask import Flask
from fittrackee.oauth2.client import check_scope, create_oauth_client from fittrackee.oauth2.client import check_scope, create_oauth_client
from fittrackee.oauth2.exceptions import InvalidOAuth2Scopes
from fittrackee.oauth2.models import OAuth2Client from fittrackee.oauth2.models import OAuth2Client
from fittrackee.users.models import User from fittrackee.users.models import User
@ -15,7 +16,7 @@ TEST_METADATA = {
'client_name': random_string(), 'client_name': random_string(),
'client_uri': random_string(), 'client_uri': random_string(),
'redirect_uris': [random_domain()], 'redirect_uris': [random_domain()],
'scope': 'read write', 'scope': 'profile:read',
} }
@ -127,13 +128,21 @@ 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 = 'write' scope = 'workouts: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)
assert oauth_client.scope == scope 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( def test_oauth_client_has_expected_token_endpoint_auth_method(
self, app: Flask, user_1: User self, app: Flask, user_1: User
) -> None: ) -> None:
@ -165,19 +174,23 @@ class TestOAuthCheckScopes:
@pytest.mark.parametrize( @pytest.mark.parametrize(
'input_scope', ['', 1, random_string(), [random_string(), 'readwrite']] '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 self, input_scope: Any
) -> None: ) -> None:
assert check_scope(input_scope) == 'read' with pytest.raises(InvalidOAuth2Scopes):
check_scope(input_scope)
@pytest.mark.parametrize( @pytest.mark.parametrize(
'input_scope,expected_scope', 'input_scope,expected_scope',
[ [
('read', 'read'), ('profile:read', 'profile:read'),
('read ' + random_string(), 'read'), ('profile:read ' + random_string(), 'profile:read'),
('write', 'write'), ('profile:write', 'profile:write'),
('write read', 'write read'), ('profile:read profile:write', 'profile:read profile:write'),
('write read ' + random_string(), 'write read'), (
'profile:write profile:read ' + random_string(),
'profile:write profile:read',
),
], ],
) )
def test_it_return_only_valid_scopes( def test_it_return_only_valid_scopes(

View File

@ -83,6 +83,30 @@ class TestOAuthClientCreation(ApiTestCaseMixin):
error_message=f'OAuth client metadata missing keys: {missing_key}', 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: def test_it_creates_oauth_client(self, app: Flask, user_1: User) -> None:
client, auth_token = self.get_test_client_and_auth_token( client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email app, user_1.email

View File

@ -1,267 +1,58 @@
from json import dumps
import pytest import pytest
from flask import Flask from flask import Flask
from werkzeug.test import TestResponse
from fittrackee.users.models import User from fittrackee.users.models import User
from ..mixins import ApiTestCaseMixin from ..mixins import ApiTestCaseMixin
from ..utils import random_short_id, random_string
class OAuth2ScopesTestCase(ApiTestCaseMixin): class TestOAuth2Scopes(ApiTestCaseMixin):
def assert_expected_response( @pytest.mark.parametrize(
self, response: TestResponse, client_scope: str, endpoint_scope: str 'endpoint_url,scope',
[
('/api/auth/profile', 'profile:read'),
('/api/workouts', 'workouts:read'),
],
)
def test_oauth_client_can_access_authorized_endpoints(
self, app: Flask, user_1: User, endpoint_url: str, scope: str
) -> None: ) -> None:
if client_scope == endpoint_scope: (
client,
oauth_client,
access_token,
_,
) = self.create_oauth_client_and_issue_token(app, user_1, scope=scope)
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_not_insufficient_scope_error(response)
else:
@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=scope)
response = client.get(
endpoint_url,
content_type='application/json',
headers=dict(Authorization=f'Bearer {access_token}'),
)
self.assert_insufficient_scope(response) 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)

View File

@ -494,6 +494,38 @@ class TestUserProfile(ApiTestCaseMixin):
assert data['status'] == 'success' assert data['status'] == 'success'
assert data['data'] == jsonify_dict(user_1.serialize(user_1)) 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): class TestUserProfileUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty( def test_it_returns_error_if_payload_is_empty(
@ -559,6 +591,42 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
assert data['message'] == 'user profile updated' assert data['message'] == 'user profile updated'
assert data['data'] == jsonify_dict(user_1.serialize(user_1)) 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): class TestUserAccountUpdate(ApiTestCaseMixin):
@staticmethod @staticmethod
@ -1193,6 +1261,42 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
password_change_email_mock, 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): class TestUserPreferencesUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty( def test_it_returns_error_if_payload_is_empty(
@ -1254,6 +1358,42 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
assert data['message'] == 'user preferences updated' assert data['message'] == 'user preferences updated'
assert data['data'] == jsonify_dict(user_1.serialize(user_1)) 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): class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
def test_it_returns_error_if_payload_is_empty( def test_it_returns_error_if_payload_is_empty(
@ -1436,6 +1576,42 @@ class TestUserSportPreferencesUpdate(ApiTestCaseMixin):
assert data['data']['is_active'] assert data['data']['is_active']
assert data['data']['stopped_speed_threshold'] == 0.5 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): class TestUserSportPreferencesReset(ApiTestCaseMixin):
def test_it_returns_error_if_sport_does_not_exist( def test_it_returns_error_if_sport_does_not_exist(
@ -1491,6 +1667,44 @@ class TestUserSportPreferencesReset(ApiTestCaseMixin):
assert response.status_code == 204 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): class TestUserPicture(ApiTestCaseMixin):
def test_it_returns_error_if_file_is_missing( 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 'avatar.png' not in user_1.picture
assert 'avatar2.png' 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): class TestRegistrationConfiguration(ApiTestCaseMixin):
def test_it_returns_error_if_it_exceeds_max_users( def test_it_returns_error_if_it_exceeds_max_users(

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from flask import Flask from flask import Flask
from fittrackee.users.models import User, UserSportPreference from fittrackee.users.models import User, UserSportPreference
@ -131,6 +132,42 @@ class TestGetUser(ApiTestCaseMixin):
self.assert_404_with_entity(response, 'user') 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): class TestGetUsers(ApiTestCaseMixin):
def test_it_returns_error_if_user_has_no_admin_rights( def test_it_returns_error_if_user_has_no_admin_rights(
@ -885,6 +922,42 @@ class TestGetUsers(ApiTestCaseMixin):
'total': 3, '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): class TestGetUserPicture(ApiTestCaseMixin):
def test_it_return_error_if_user_has_no_picture( 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['is_active'] is True
assert user_2.confirmation_token is None 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): class TestDeleteUser(ApiTestCaseMixin):
def test_user_can_delete_its_own_account( def test_user_can_delete_its_own_account(
@ -1573,3 +1683,40 @@ class TestDeleteUser(ApiTestCaseMixin):
) )
self.assert_403(response, 'error, registration is disabled') 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)

View File

@ -47,5 +47,5 @@ TEST_OAUTH_CLIENT_METADATA = {
'client_name': random_string(), 'client_name': random_string(),
'client_uri': random_domain(), 'client_uri': random_domain(),
'redirect_uris': [random_domain()], 'redirect_uris': [random_domain()],
'scope': 'read write', 'scope': 'profile:read workouts:read',
} }

View File

@ -1,5 +1,6 @@
import json import json
import pytest
from flask import Flask from flask import Flask
from fittrackee.users.models import User from fittrackee.users.models import User
@ -897,3 +898,40 @@ class TestGetRecords(ApiTestCaseMixin):
response = client.get('/api/records') response = client.get('/api/records')
self.assert_401(response) 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)

View File

@ -1,5 +1,6 @@
import json import json
import pytest
from flask import Flask from flask import Flask
from fittrackee import db from fittrackee import db
@ -138,6 +139,43 @@ class TestGetSports(ApiTestCaseMixin):
sport_2_running.serialize(is_admin=True) 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): class TestGetSport(ApiTestCaseMixin):
def test_it_gets_a_sport( def test_it_gets_a_sport(
@ -241,6 +279,43 @@ class TestGetSport(ApiTestCaseMixin):
sport_1_cycling_inactive.serialize(is_admin=True) 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): class TestUpdateSport(ApiTestCaseMixin):
def test_it_disables_a_sport( def test_it_disables_a_sport(
@ -442,3 +517,41 @@ class TestUpdateSport(ApiTestCaseMixin):
data = self.assert_404(response) data = self.assert_404(response)
assert len(data['data']['sports']) == 0 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)

View File

@ -1,5 +1,6 @@
import json import json
import pytest
from flask import Flask from flask import Flask
from fittrackee.users.models import User 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): class TestGetStatsBySport(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated( def test_it_returns_error_if_user_is_not_authenticated(
@ -1007,6 +1044,42 @@ class TestGetStatsBySport(ApiTestCaseMixin):
self.assert_500(response) 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): class TestGetAllStats(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated( def test_it_returns_error_if_user_is_not_authenticated(
@ -1089,3 +1162,39 @@ class TestGetAllStats(ApiTestCaseMixin):
) )
self.assert_403(response) 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)

View File

@ -3,6 +3,7 @@ from typing import List
from unittest.mock import patch from unittest.mock import patch
from uuid import uuid4 from uuid import uuid4
import pytest
from flask import Flask from flask import Flask
from fittrackee.users.models import User from fittrackee.users.models import User
@ -101,6 +102,42 @@ class TestGetWorkouts(ApiTestCaseMixin):
self.assert_401(response, 'provide a valid auth token') 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): class TestGetWorkoutsWithPagination(ApiTestCaseMixin):
def test_it_gets_workouts_with_default_pagination( 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') 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): class TestDownloadWorkoutGpx(ApiTestCaseMixin):
def test_it_returns_404_if_workout_does_not_exist( def test_it_returns_404_if_workout_does_not_exist(
@ -1242,3 +1328,41 @@ class TestDownloadWorkoutGpx(ApiTestCaseMixin):
mimetype='application/gpx+xml', mimetype='application/gpx+xml',
as_attachment=True, 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)

View File

@ -621,6 +621,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
) )
assert 'data' not in data 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): class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_returns_error_if_user_is_not_authenticated( 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]['segments']) == 0
assert len(data['data']['workouts'][0]['records']) == 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): class TestPostWorkoutWithZipArchive(ApiTestCaseMixin):
def test_it_adds_workouts_with_zip_archive( def test_it_adds_workouts_with_zip_archive(

View File

@ -221,6 +221,45 @@ class TestEditWorkoutWithGpx(ApiTestCaseMixin):
self.assert_500(response) 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): class TestEditWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_updates_a_workout_wo_gpx( def test_it_updates_a_workout_wo_gpx(

View File

@ -1,5 +1,6 @@
import os import os
import pytest
from flask import Flask from flask import Flask
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
@ -80,6 +81,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
self.assert_500(response) 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): class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):
def test_it_deletes_a_workout_wo_gpx( def test_it_deletes_a_workout_wo_gpx(

View File

@ -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(scopes='read') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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(scopes='write') @require_auth(scopes=['profile: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

View File

@ -34,7 +34,7 @@ USER_PER_PAGE = 10
@users_blueprint.route('/users', methods=['GET']) @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: 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(scopes='read') @require_auth(scopes=['users: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(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]: 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(scopes='write') @require_auth(scopes=['users: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]:

View File

@ -11,7 +11,7 @@ records_blueprint = Blueprint('records', __name__)
@records_blueprint.route('/records', methods=['GET']) @records_blueprint.route('/records', methods=['GET'])
@require_auth(scopes='read') @require_auth(scopes=['workouts: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.

View File

@ -19,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
@sports_blueprint.route('/sports', methods=['GET']) @sports_blueprint.route('/sports', methods=['GET'])
@require_auth(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(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]: def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
""" """
Update a sport Update a sport

View File

@ -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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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]:
@ -377,7 +377,7 @@ def get_workouts_by_sport(
@stats_blueprint.route('/stats/all', methods=['GET']) @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: def get_application_stats(auth_user: User) -> Dict:
""" """
Get all application statistics Get all application statistics

View File

@ -56,7 +56,7 @@ MAX_WORKOUTS_PER_PAGE = 100
@workouts_blueprint.route('/workouts', methods=['GET']) @workouts_blueprint.route('/workouts', methods=['GET'])
@require_auth(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='read') @require_auth(scopes=['workouts: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(scopes='write') @require_auth(scopes=['workouts:write'])
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
""" """
Post a workout with a gpx file 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']) @workouts_blueprint.route('/workouts/no_gpx', methods=['POST'])
@require_auth(scopes='write') @require_auth(scopes=['workouts: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]:
@ -1165,7 +1165,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(scopes='write') @require_auth(scopes=['workouts: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]:
@ -1313,7 +1313,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(scopes='write') @require_auth(scopes=['workouts: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]: