diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index d33c768649d85edfdde1555ec389adb4e53fc51f..5364b2e631bb50b9fc9feb9e1cd71d1f39ecda74 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -2,7 +2,9 @@ from functools import wraps from flask_jwt_extended import verify_jwt_in_request from flask_jwt_extended.utils import get_jwt +from flask_jwt_extended.view_decorators import jwt_required from flask_smorest import Blueprint, abort +from marshmallow import Schema, fields Blueprint.PAGINATION_HEADER_FIELD_NAME = "pagination" @@ -29,56 +31,75 @@ def _has_access(in_claim, in_route): return not in_route or in_claim and in_claim == in_route -def protect_route(allowed_roles=None, allowed_views=None): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - verify_jwt_in_request() - jwt = get_jwt() +# class AuthorizationHeadersSchema(Schema): - # Authorize request if roles has access to the route # +# Authorization = fields.String(required=True) - nonlocal allowed_roles - allowed_roles = allowed_roles or [] - role = jwt.get("role") - if _is_allowed(allowed_roles, role): - return func(*args, **kwargs) - # Authorize request if view has access and is trying to access the - # competition its in. Also check team if client is a team. - # Allow request if route doesn't belong to any competition. - - nonlocal allowed_views - allowed_views = allowed_views or [] - view = jwt.get("view") - if not _is_allowed(allowed_views, view): - abort( - http_codes.UNAUTHORIZED, - f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.", - ) - - claim_competition_id = jwt.get("competition_id") - route_competition_id = kwargs.get("competition_id") - if not _has_access(claim_competition_id, route_competition_id): - abort( - http_codes.UNAUTHORIZED, - f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.", - ) - - if view == "Team": - claim_team_id = jwt.get("team_id") - route_team_id = kwargs.get("team_id") - if not _has_access(claim_team_id, route_team_id): +class ExtendedBlueprint(Blueprint): + def authorization(self, allowed_roles=None, allowed_views=None): + def decorator(func): + + # func = self.arguments(AuthorizationHeadersSchema, location="headers")(func) + func = self.alt_response(http_codes.UNAUTHORIZED, None, description="Unauthorized")(func) + + @wraps(func) + def wrapper(*args, **kwargs): + + # Check that allowed_roles and allowed_views have correct type + nonlocal allowed_roles + nonlocal allowed_views + allowed_roles = allowed_roles or [] + allowed_views = allowed_views or [] + assert ( + isinstance(allowed_roles, list) or allowed_roles == "*" + ), f"Allowed roles must be a list or '*', not '{allowed_roles}'" + assert ( + isinstance(allowed_views, list) or allowed_views == "*" + ), f"Allowed views must be a list or '*', not '{allowed_views}'" + + verify_jwt_in_request() + jwt = get_jwt() + + # Authorize request if roles has access to the route # + + role = jwt.get("role") + if _is_allowed(allowed_roles, role): + return func(*args, **kwargs) + + # Authorize request if view has access and is trying to access the + # competition its in. Also check team if client is a team. + # Allow request if route doesn't belong to any competition. + + view = jwt.get("view") + if not _is_allowed(allowed_views, view): abort( http_codes.UNAUTHORIZED, - f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.", + f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.", ) - return func(*args, **kwargs) + claim_competition_id = jwt.get("competition_id") + route_competition_id = kwargs.get("competition_id") + if not _has_access(claim_competition_id, route_competition_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.", + ) + + if view == "Team": + claim_team_id = jwt.get("team_id") + route_team_id = kwargs.get("team_id") + if not _has_access(claim_team_id, route_team_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.", + ) + + return func(*args, **kwargs) - return wrapper + return wrapper - return decorator + return decorator from flask_smorest import Api diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py index a3143a7cea6093d1fe9d06210f780c7930c70f1d..e454a5faaa0e1fbf492b61b88c4271fbcbfb19fa 100644 --- a/server/app/apis/alternatives.py +++ b/server/app/apis/alternatives.py @@ -5,17 +5,16 @@ Default route: /api/competitions/<competition_id>/slides/<slide_id>/questions/<q import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, QuestionAlternativeSchema from app.database import models from app.database.models import Question, QuestionAlternative from flask.views import MethodView -from flask_smorest import Blueprint, abort +from flask_smorest import abort -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "alternative", "alternative", url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives", @@ -43,13 +42,13 @@ class AlternativeEditArgsSchema(BaseSchema): @blp.route("") class Alternatives(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionAlternativeSchema(many=True)) def get(self, competition_id, slide_id, question_id): """ Gets the all question alternatives to the specified question. """ return dbc.get.question_alternative_list(competition_id, slide_id, question_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(AlternativeAddArgsSchema) @blp.response(http_codes.OK, QuestionAlternativeSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Could not add alternative") @@ -63,14 +62,14 @@ class Alternatives(MethodView): @blp.route("/<alternative_id>") class QuestionAlternatives(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionAlternativeSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find alternative") def get(self, competition_id, slide_id, question_id, alternative_id): """ Gets the specified question alternative. """ return dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(AlternativeEditArgsSchema) @blp.response(http_codes.OK, QuestionAlternativeSchema) @blp.alt_response(http_codes.BAD_REQUEST, None, description="Paramters to edit alternative with is incorrect") @@ -111,7 +110,7 @@ class QuestionAlternatives(MethodView): return dbc.edit.default(item, **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find alternative") @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete alternative") diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index 2bcbf159604eae735f781c5cbbe122d0bcb89986..b1116a14a1f57cb9b413cfeeffcd31c20ce52ffd 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers """ import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, QuestionAlternativeAnswerSchema from app.database import models from flask.views import MethodView -from flask_smorest import Blueprint, abort, response -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "answer", "answer", url_prefix="/api/competitions/<competition_id>/teams/<team_id>/answers", @@ -30,7 +28,7 @@ class AnswerAddArgsSchema(BaseSchema): @blp.route("") class QuestionAlternativeList(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) def get(self, competition_id, team_id): """ Gets all question answers that the specified team has given. """ @@ -39,13 +37,13 @@ class QuestionAlternativeList(MethodView): @blp.route("/<answer_id>") class QuestionAlternativeAnswers(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) def get(self, competition_id, team_id, answer_id): """ Gets the specified question answer. """ return dbc.get.question_alternative_answer(competition_id, team_id, answer_id) - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.arguments(AnswerAddArgsSchema) @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) def put(self, args, competition_id, team_id, answer_id): diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index b6379f281becb2d6acf7d0a48388098faf27e545..434ddfaf616228d7e045a1e2db9a1cae161d2437 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -3,24 +3,23 @@ All API calls concerning question answers. Default route: /api/auth """ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta import app.database.controller as dbc import marshmallow as ma from app.core.codes import verify_code -from app.core.schemas import UserSchema from app.core.sockets import is_active_competition from app.database.controller.delete import whitelist_to_blacklist -from app.database.models import User, Whitelist +from app.database.models import Whitelist from flask import current_app, has_app_context from flask.views import MethodView from flask_jwt_extended import create_access_token, get_jti from flask_jwt_extended.utils import get_jti, get_jwt -from flask_smorest import Blueprint, abort, arguments +from flask_smorest import abort -from . import http_codes, protect_route +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "auth", "auth", url_prefix="/api/auth", description="Logging in as a user or with a code, and logging out" ) @@ -59,7 +58,7 @@ def get_code_claims(item_code): @blp.route("/test") class AuthSignup(MethodView): - @protect_route(allowed_roles=["Admin"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["Admin"], allowed_views=["*"]) @blp.response(http_codes.NO_CONTENT, None) def get(self): """ Tests that the user is admin or is in a competition. """ @@ -119,7 +118,7 @@ class AuthLogin(MethodView): @blp.route("/logout") class AuthLogout(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.NO_CONTENT, None) def post(self): """ Logs out. """ diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index aa8b806511d8679704296a679ce238aecfa54d83..e712428dd7ce1202b8e64086dbbaaf3b9e8521ae 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -4,22 +4,20 @@ Default route: /api/competitions/<competition_id>/codes """ import app.database.controller as dbc -from app.apis import protect_route from app.core.schemas import CodeSchema from app.database.models import Code from flask.views import MethodView -from flask_smorest import Blueprint -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "code", "code", url_prefix="/api/competitions/<competition_id>/codes", description="Operations on codes" ) @blp.route("") class CodesList(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["Operator"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["Operator"]) @blp.response(http_codes.OK, CodeSchema(many=True)) def get(self, competition_id): """ Gets the all competition codes. """ @@ -28,7 +26,7 @@ class CodesList(MethodView): @blp.route("/<code_id>") class CodesById(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, CodeSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Code not found") def put(self, competition_id, code_id): diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index 10399bd8d7bdb8e3f95162914f45dfe09e544184..037446900869d3604e68cecff200cdb5c9f779d8 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -4,16 +4,17 @@ Default route: /api/competitions """ import app.database.controller as dbc -from app.apis import http_codes, protect_route +from app.apis import ExtendedBlueprint, http_codes from app.core import ma from app.core.rich_schemas import CompetitionSchemaRich from app.core.schemas import BaseSchema, CompetitionSchema from app.database import models from app.database.models import Competition from flask.views import MethodView -from flask_smorest import Blueprint, arguments -blp = Blueprint("competitions", "competitions", url_prefix="/api/competitions", description="Operations competitions") +blp = ExtendedBlueprint( + "competitions", "competitions", url_prefix="/api/competitions", description="Operations competitions" +) class CompetitionAddArgsSchema(BaseSchema): @@ -47,7 +48,7 @@ class CompetitionSearchArgsSchema(BaseSchema): @blp.route("") class Competitions(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(CompetitionAddArgsSchema) @blp.response(http_codes.OK, CompetitionSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be added") @@ -58,14 +59,14 @@ class Competitions(MethodView): @blp.route("/<competition_id>") class CompetitionById(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, CompetitionSchemaRich) @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found") def get(self, competition_id): """ Gets the specified competition. """ return dbc.get.competition(competition_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(CompetitionEditArgsSchema) @blp.response(http_codes.OK, CompetitionSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found") @@ -74,7 +75,7 @@ class CompetitionById(MethodView): """ Edits the specified competition with the specified arguments. """ return dbc.edit.default(dbc.get.one(Competition, competition_id), **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found") @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be deleted") @@ -86,7 +87,7 @@ class CompetitionById(MethodView): @blp.route("/search") class CompetitionSearch(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(CompetitionSearchArgsSchema, location="query") @blp.paginate() @blp.response(http_codes.OK, CompetitionSchema(many=True)) @@ -97,7 +98,7 @@ class CompetitionSearch(MethodView): @blp.route("/<competition_id>/copy") class SlidesOrder(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, CompetitionSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Competition not found") @blp.alt_response(http_codes.CONFLICT, None, description="Competition could not be copied") diff --git a/server/app/apis/components.py b/server/app/apis/components.py index d0e45da6020652c0e627f17f01d5b5e7785d878d..8e3f8800c4f183a2824eb40d6b81cf8f2b71d9f8 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -4,17 +4,13 @@ Default route: /api/competitions/<competition_id>/slides/<slide_id>/components """ import app.database.controller as dbc -from app.apis import protect_route -from app.core import ma from app.core.schemas import BaseSchema, ComponentSchema -from app.database import models from flask.views import MethodView -from flask_smorest import Blueprint, abort from marshmallow import fields -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "component", "component", url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/components", @@ -54,13 +50,13 @@ class ComponentEditArgsSchema(BaseSchema): @blp.route("") class Components(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, ComponentSchema(many=True)) def get(self, competition_id, slide_id): """ Gets all components in the specified slide and competition. """ return dbc.get.component_list(competition_id, slide_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(ComponentAddArgsSchema) @blp.response(http_codes.OK, ComponentSchema) def post(self, args, competition_id, slide_id): @@ -70,14 +66,14 @@ class Components(MethodView): @blp.route("/<component_id>") class ComponentById(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, ComponentSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component") def get(self, competition_id, slide_id, component_id): """ Gets the specified component. """ return dbc.get.component(competition_id, slide_id, component_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(ComponentEditArgsSchema) @blp.response(http_codes.OK, ComponentSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component") @@ -86,7 +82,7 @@ class ComponentById(MethodView): """ Edits the specified component using the provided arguments. """ return dbc.edit.default(dbc.get.component(competition_id, slide_id, component_id), **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find component") @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete component") @@ -98,7 +94,7 @@ class ComponentById(MethodView): @blp.route("/<component_id>/copy/<view_type_id>") class ComponentCopy(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, ComponentSchema) def post(self, competition_id, slide_id, component_id, view_type_id): """ Creates a deep copy of the specified component. """ diff --git a/server/app/apis/media.py b/server/app/apis/media.py index 05e7f0c5e2ecb4ef511ec7e512a1d25c6c300224..b24515e1a6dbb7ce7ebb872c321a2488abc19633 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -5,18 +5,17 @@ Default route: /api/media import app.core.files as files import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, MediaSchema from app.database import models from flask import request from flask.views import MethodView from flask_jwt_extended import get_jwt_identity -from flask_smorest import Blueprint, abort +from flask_smorest import abort from flask_uploads import UploadNotAllowed from sqlalchemy import exc -from . import http_codes +from . import ExtendedBlueprint, http_codes class ImageSearchArgsSchema(BaseSchema): @@ -26,12 +25,12 @@ class ImageSearchArgsSchema(BaseSchema): filename = ma.auto_field(required=False) -blp = Blueprint("media", "media", url_prefix="/api/media", description="Operations on media") +blp = ExtendedBlueprint("media", "media", url_prefix="/api/media", description="Operations on media") @blp.route("/images") class Images(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(ImageSearchArgsSchema, location="query") @blp.paginate() @blp.response(http_codes.OK, MediaSchema(many=True)) @@ -39,7 +38,7 @@ class Images(MethodView): """ Gets a list of all images with the specified filename. """ return dbc.search.image(pagination_parameters, **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, MediaSchema) @blp.alt_response(http_codes.BAD_REQUEST, None, description="Could not save image") @blp.alt_response(http_codes.INTERNAL_SERVER_ERROR, None, description="Could not save image") @@ -63,14 +62,14 @@ class Images(MethodView): @blp.route("/images/<media_id>") class ImageById(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, MediaSchema) @blp.alt_response(http_codes.NOT_FOUND, MediaSchema, description="Could not find image") def get(self, media_id): """ Gets the specified image. """ return dbc.get.one(models.Media, media_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.response(http_codes.CONFLICT, None, description="Could not delete image it is used by something") @blp.response(http_codes.BAD_REQUEST, None, description="Failed to delete image") diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index 902407ddf20278b452365766e83c149b7c0a7d31..033b91679839d182fe4b514f2d01c9bb5294fa67 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -5,7 +5,6 @@ Default route: /api/misc import app.database.controller as dbc import marshmallow as ma -from app.apis import protect_route from app.core.schemas import ( BaseSchema, CitySchema, @@ -16,15 +15,13 @@ from app.core.schemas import ( ViewTypeSchema, ) from app.database import models -from app.database.controller.add import competition from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType from flask.views import MethodView -from flask_smorest import Blueprint, response from marshmallow_sqlalchemy import auto_field -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint("misc", "misc", url_prefix="/api/misc", description="Roles, regions, types and statistics") +blp = ExtendedBlueprint("misc", "misc", url_prefix="/api/misc", description="Roles, regions, types and statistics") class TypesResponseSchema(BaseSchema): @@ -49,7 +46,7 @@ class Types(MethodView): @blp.route("/roles") class RoleList(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, RoleSchema(many=True)) def get(self): """ Gets a list of all roles. """ @@ -65,13 +62,13 @@ class CityAddArgsSchema(BaseSchema): @blp.route("/cities") class CitiesList(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, CitySchema(many=True)) def get(self): """ Gets a list of all cities. """ return dbc.get.all(City) - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.arguments(CitySchema) @blp.response(http_codes.OK, CitySchema(many=True)) def post(self, args): @@ -82,7 +79,7 @@ class CitiesList(MethodView): @blp.route("/cities/<city_id>") class Cities(MethodView): - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.arguments(CitySchema) @blp.response(http_codes.OK, CitySchema(many=True)) @blp.alt_response(http_codes.NOT_FOUND, None, description="City not found") @@ -92,7 +89,7 @@ class Cities(MethodView): dbc.edit.default(dbc.get.one(City, city_id), **args) return dbc.get.all(City) - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.response(http_codes.OK, CitySchema(many=True)) @blp.alt_response(http_codes.NOT_FOUND, None, description="City not found") @blp.alt_response(http_codes.CONFLICT, None, description="The city can't be updated with the provided values") @@ -110,7 +107,7 @@ class StatisticsResponseSchema(BaseSchema): @blp.route("/statistics") class Statistics(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, StatisticsResponseSchema) def get(self): """ Gets statistics. """ diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 60ed21006c095cf3787d8c302bc4489c4edfaf10..f3190d11a8a067975da8dec8790e574f4eae5c35 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -4,17 +4,14 @@ Default route: /api/competitions/<competition_id> """ import app.database.controller as dbc -from app.apis import protect_route from app.core import ma -from app.core.schemas import BaseSchema, QuestionSchema, SlideSchema +from app.core.schemas import BaseSchema, QuestionSchema from app.database import models -from app.database.controller.edit import default from flask.views import MethodView -from flask_smorest import Blueprint -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "question", "question", url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions", @@ -44,13 +41,13 @@ class QuestionEditArgsSchema(BaseSchema): @blp.route("") class Questions(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, QuestionSchema(many=True)) def get(self, competition_id, slide_id): """ Gets all questions in the specified competition and slide. """ return dbc.get.question_list(competition_id, slide_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(QuestionAddArgsSchema) @blp.response(http_codes.OK, QuestionSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Could not add question") @@ -61,7 +58,7 @@ class Questions(MethodView): @blp.route("/<question_id>") class QuestionById(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, QuestionSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question") def get(self, competition_id, slide_id, question_id): @@ -70,7 +67,7 @@ class QuestionById(MethodView): """ return dbc.get.question(competition_id, slide_id, question_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(QuestionEditArgsSchema) @blp.response(http_codes.OK, QuestionSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question") @@ -79,7 +76,7 @@ class QuestionById(MethodView): """ Edits the specified question with the provided arguments. """ return dbc.edit.default(dbc.get.question(competition_id, slide_id, question_id), **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find question") @blp.alt_response(http_codes.CONFLICT, None, description="Could not delete question") diff --git a/server/app/apis/scores.py b/server/app/apis/scores.py index c90e4e5c569f917bf20af832e2ca6d5fe444cac4..a906185c09d369e2956d25e978286fbc892a4b6e 100644 --- a/server/app/apis/scores.py +++ b/server/app/apis/scores.py @@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers/quesit """ import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, QuestionScoreSchema from app.database import models from flask.views import MethodView -from flask_smorest import Blueprint, abort, response -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "score", "score", url_prefix="/api/competitions/<competition_id>/teams/<team_id>/scores", @@ -30,7 +28,7 @@ class ScoreAddArgsSchema(BaseSchema): @blp.route("") class QuestionScoreList(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionScoreSchema(many=True)) def get(self, competition_id, team_id): """ Gets all question answers that the specified team has given. """ @@ -39,14 +37,14 @@ class QuestionScoreList(MethodView): @blp.route("/<question_id>") class QuestionScores(MethodView): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.response(http_codes.OK, QuestionScoreSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Cant find answer") def get(self, competition_id, team_id, question_id): """ Gets the score for the provided team on the provided question. """ return dbc.get.question_score(competition_id, team_id, question_id) - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + @blp.authorization(allowed_roles=["*"], allowed_views=["*"]) @blp.arguments(ScoreAddArgsSchema) @blp.response(http_codes.OK, QuestionScoreSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Cant find score") diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index a8d447fa0995c8ad216f73580eecf4622ec0bcfb..177c57179779e83b3652e5dbe7e2f5832c2af3d2 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -4,17 +4,16 @@ Default route: /api/competitions/<competition_id>/slides """ import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, SlideSchema from app.database import models from app.database.models import Competition, Slide from flask.views import MethodView -from flask_smorest import Blueprint, abort +from flask_smorest import abort -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "slide", "slide", url_prefix="/api/competitions/<competition_id>/slides", @@ -34,13 +33,13 @@ class SlideEditArgsSchema(BaseSchema): @blp.route("") class Slides(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, SlideSchema(many=True)) def get(self, competition_id): """ Gets all slides from the specified competition. """ return dbc.get.slide_list(competition_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, SlideSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Can't add slide") def post(self, competition_id): @@ -50,14 +49,14 @@ class Slides(MethodView): @blp.route("/<slide_id>") class Slides(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, SlideSchema) @blp.alt_response(http_codes.NOT_FOUND, None) def get(self, competition_id, slide_id): """ Gets the specified slide. """ return dbc.get.slide(competition_id, slide_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(SlideEditArgsSchema) @blp.response(http_codes.OK, SlideSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Can't edit slide") @@ -77,7 +76,7 @@ class Slides(MethodView): return dbc.edit.default(item_slide, **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Slide not found") @blp.alt_response(http_codes.CONFLICT, None, description="Can't delete slide") @@ -89,7 +88,7 @@ class Slides(MethodView): @blp.route("/<slide_id>/copy") class SlideCopy(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, SlideSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Can't find slide") def post(self, competition_id, slide_id): diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 4071a89ddee43e8d7783a2a78f71507c99e15230..03cf0be7113f9b7e140cca707b703f184cc5747b 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -4,16 +4,14 @@ Default route: /api/competitions/<competition_id>/teams """ import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, TeamSchema from app.database import models from flask.views import MethodView -from flask_smorest import Blueprint -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "team", "team", url_prefix="/api/competitions/<competition_id>/teams", @@ -37,13 +35,13 @@ class TeamEditArgsSchema(BaseSchema): @blp.route("") class Teams(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, TeamSchema(many=True)) def get(self, competition_id): """ Gets all teams to the specified competition. """ return dbc.get.team_list(competition_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(TeamAddArgsSchema) @blp.response(http_codes.OK, TeamSchema) @blp.alt_response(http_codes.CONFLICT, None, description="Could not add team") @@ -54,14 +52,14 @@ class Teams(MethodView): @blp.route("/<team_id>") class TeamsById(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, TeamSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team") def get(self, competition_id, team_id): """ Gets the specified team. """ return dbc.get.team(competition_id, team_id) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(TeamEditArgsSchema) @blp.response(http_codes.OK, TeamSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team") @@ -69,7 +67,7 @@ class TeamsById(MethodView): """ Edits the specified team using the provided arguments. """ return dbc.edit.default(dbc.get.team(competition_id, team_id), **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="Could not find team") def delete(self, competition_id, team_id): diff --git a/server/app/apis/users.py b/server/app/apis/users.py index a982bb0b0656445cf05368d57698224dd745467f..28849ab6e5fd3885e669f8cd92fca22ec43c53f7 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -5,19 +5,18 @@ Default route: /api/users import app.database.controller as dbc -from app.apis import protect_route from app.core import ma from app.core.schemas import BaseSchema, UserSchema from app.database import models from app.database.models import User, Whitelist from flask.views import MethodView from flask_jwt_extended.utils import get_jwt_identity -from flask_smorest import Blueprint, abort +from flask_smorest import abort from marshmallow import fields -from . import http_codes +from . import ExtendedBlueprint, http_codes -blp = Blueprint( +blp = ExtendedBlueprint( "users", "users", url_prefix="/api/users", description="Adding, updating, deleting and searching for users" ) @@ -55,20 +54,20 @@ class UserSearchArgsSchema(BaseSchema): @blp.route("") class Users(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, UserSchema) def get(self): """ Get currently logged in user. """ return dbc.get.one(User, get_jwt_identity()) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(UserEditArgsSchema) @blp.response(http_codes.OK, UserSchema) def put(self, args): """ Edit current user. """ return _edit_user(dbc.get.one(User, get_jwt_identity()), args) - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.arguments(UserAddArgsSchema) @blp.response(http_codes.OK, UserSchema) def post(self, args): @@ -78,14 +77,14 @@ class Users(MethodView): @blp.route("/<user_id>") class UsersById(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.response(http_codes.OK, UserSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found") def get(self, user_id): """ Get user with <user_id> """ return dbc.get.one(User, user_id) - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.arguments(UserEditArgsSchema) @blp.response(http_codes.OK, UserSchema) @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found") @@ -94,7 +93,7 @@ class UsersById(MethodView): """ Edits user with <user_id> """ return _edit_user(dbc.get.one(User, user_id), args) - @protect_route(allowed_roles=["Admin"]) + @blp.authorization(allowed_roles=["Admin"]) @blp.response(http_codes.NO_CONTENT, None) @blp.alt_response(http_codes.NOT_FOUND, None, description="User not found") @blp.alt_response(http_codes.CONFLICT, None, description="The user can't be deleted") @@ -108,7 +107,7 @@ class UsersById(MethodView): @blp.route("/search") class UserSearch(MethodView): - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=["*"]) @blp.arguments(UserSearchArgsSchema, location="query") @blp.paginate() @blp.response(http_codes.OK, UserSchema(many=True))