diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 5eab3f829ae81e17ecc269d98f568eeacc45a68c..e5ec3d2ca1e1d9de6d4c5cca5ec1e3fba40f33ee 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -13,24 +13,62 @@ def validate_editor(db_item, *views): abort(http_codes.UNAUTHORIZED) -def check_jwt(editor=False, *views): - def wrapper(fn): - @wraps(fn) - def decorator(*args, **kwargs): +def _is_allowed(allowed, actual): + return actual and "*" in allowed or actual in allowed + + +def _has_access(in_claim, in_route): + in_route = int(in_route) if in_route else None + return not in_route or in_claim and in_claim == in_route + + +def protect_route(allowed_roles=None, allowed_views=None): + def wrapper(func): + def inner(*args, **kwargs): verify_jwt_in_request() claims = get_jwt_claims() + + # Authorize request if roles has access to the route # + + nonlocal allowed_roles + allowed_roles = allowed_roles or [] role = claims.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 = claims.get("view") - if role == "Admin": - return fn(*args, **kwargs) - elif editor and role == "Editor": - return fn(*args, **kwargs) - elif view in views: - return fn(*args, **kwargs) - else: - abort(http_codes.UNAUTHORIZED) - - return decorator + 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 = claims.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 = claims.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 inner return wrapper diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py index 2adad553ddb34bbd43e1405223c7c45962d66e19..0ce74d53b0a742ea31689d826f898e1b12f33b41 100644 --- a/server/app/apis/alternatives.py +++ b/server/app/apis/alternatives.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionAlternativeDTO from flask_restx import Resource from flask_restx import reqparse @@ -17,12 +17,12 @@ question_alternative_parser.add_argument("value", type=int, default=None, locati @api.route("") @api.param("competition_id, slide_id, question_id") class QuestionAlternativeList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, question_id): items = dbc.get.question_alternative_list(competition_id, slide_id, question_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id, question_id): args = question_alternative_parser.parse_args(strict=True) item = dbc.add.question_alternative(**args, question_id=question_id) @@ -32,19 +32,19 @@ class QuestionAlternativeList(Resource): @api.route("/<alternative_id>") @api.param("competition_id, slide_id, question_id, alternative_id") class QuestionAlternatives(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, question_id, alternative_id): items = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) return item_response(schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, question_id, alternative_id): args = question_alternative_parser.parse_args(strict=True) item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, question_id, alternative_id): item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) dbc.delete.default(item) diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index 79a9c7a9080ae50e6353894ec6780987597c929f..8f72d3bde8610c255083df9deefc69b89a68560a 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionAnswerDTO from flask_restx import Resource from flask_restx import reqparse @@ -22,12 +22,12 @@ question_answer_edit_parser.add_argument("score", type=int, default=None, locati @api.route("") @api.param("competition_id, team_id") class QuestionAnswerList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, team_id): items = dbc.get.question_answer_list(competition_id, team_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def post(self, competition_id, team_id): args = question_answer_parser.parse_args(strict=True) item = dbc.add.question_answer(**args, team_id=team_id) @@ -37,11 +37,12 @@ class QuestionAnswerList(Resource): @api.route("/<answer_id>") @api.param("competition_id, team_id, answer_id") class QuestionAnswers(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, team_id, answer_id): item = dbc.get.question_answer(competition_id, team_id, answer_id) return item_response(schema.dump(item)) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def put(self, competition_id, team_id, answer_id): args = question_answer_edit_parser.parse_args(strict=True) item = dbc.get.question_answer(competition_id, team_id, answer_id) diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 37da9e98d4b5970cd82b47dd54ad940cc53b6736..9c9ed24a876a9b130c385194bb8cc322bef65c9d 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, text_response +from app.apis import item_response, protect_route, text_response from app.core.codes import verify_code from app.core.dto import AuthDTO, CodeDTO from flask_jwt_extended import ( @@ -12,6 +12,8 @@ from flask_jwt_extended import ( ) from flask_restx import Resource from flask_restx import inputs, reqparse +from datetime import timedelta +from app.core import sockets api = AuthDTO.api schema = AuthDTO.schema @@ -33,9 +35,13 @@ def get_user_claims(item_user): return {"role": item_user.role.name, "city_id": item_user.city_id} +def get_code_claims(item_code): + return {"view": item_code.view_type.name, "competition_id": item_code.competition_id, "team_id": item_code.team_id} + + @api.route("/signup") class AuthSignup(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def post(self): args = create_user_parser.parse_args(strict=True) email = args.get("email") @@ -50,7 +56,7 @@ class AuthSignup(Resource): @api.route("/delete/<ID>") @api.param("ID") class AuthDelete(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def delete(self, ID): item_user = dbc.get.user(ID) @@ -86,24 +92,38 @@ class AuthLoginCode(Resource): code = args["code"] if not verify_code(code): - api.abort(codes.BAD_REQUEST, "Invalid code") + api.abort(codes.UNAUTHORIZED, "Invalid code") item_code = dbc.get.code_by_code(code) - return item_response(CodeDTO.schema.dump(item_code)) + + if item_code.competition_id not in sockets.presentations: + api.abort(codes.UNAUTHORIZED, "Competition not active") + + access_token = create_access_token( + item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8) + ) + + response = { + "competition_id": item_code.competition_id, + "view_type_id": item_code.view_type_id, + "team_id": item_code.team_id, + "access_token": access_token, + } + return response @api.route("/logout") class AuthLogout(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def post(self): jti = get_raw_jwt()["jti"] dbc.add.blacklist(jti) - return text_response("User logout") + return text_response("Logout") @api.route("/refresh") class AuthRefresh(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) @jwt_refresh_token_required def post(self): old_jti = get_raw_jwt()["jti"] diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index 8a4105d24a77f07b04da1e1e2ed477746ea7a932..d07e17435aed31417254acfd83ed9315f1477ba3 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -1,5 +1,5 @@ import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core import http_codes as codes from app.core.dto import CodeDTO from app.database.models import Code @@ -13,7 +13,7 @@ list_schema = CodeDTO.list_schema @api.route("") @api.param("competition_id") class CodesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.code_list(competition_id) return list_response(list_schema.dump(items), len(items)) @@ -22,7 +22,7 @@ class CodesList(Resource): @api.route("/<code_id>") @api.param("competition_id, code_id") class CodesById(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, code_id): item = dbc.get.one(Code, code_id) item.code = dbc.utils.generate_unique_code() diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index fded52ec97df7d72acaf531eb5f0033c25227669..2a7b2a95b97ffcfdef986ad877f2b6a39e088539 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -1,7 +1,7 @@ import time import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import CompetitionDTO from app.database.models import Competition from flask_restx import Resource @@ -29,7 +29,7 @@ competition_search_parser.add_argument("city_id", type=int, default=None, locati @api.route("") class CompetitionsList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self): args = competition_parser.parse_args(strict=True) @@ -44,12 +44,13 @@ class CompetitionsList(Resource): @api.route("/<competition_id>") @api.param("competition_id") class Competitions(Resource): + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id): item = dbc.get.competition(competition_id) return item_response(rich_schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id): args = competition_edit_parser.parse_args(strict=True) item = dbc.get.one(Competition, competition_id) @@ -57,7 +58,7 @@ class Competitions(Resource): return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id): item = dbc.get.one(Competition, competition_id) dbc.delete.competition(item) @@ -67,7 +68,7 @@ class Competitions(Resource): @api.route("/search") class CompetitionSearch(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = competition_search_parser.parse_args(strict=True) items, total = dbc.search.competition(**args) @@ -77,7 +78,7 @@ class CompetitionSearch(Resource): @api.route("/<competition_id>/copy") @api.param("competition_id") class SlidesOrder(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): item_competition = dbc.get.competition(competition_id) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 8ab67b8cb8bd001c9641b6331325f328639b581e..c22ce4ad671329538e05a6a6ee7bb5fd9026ca38 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import ComponentDTO from flask_restx import Resource from flask_restx import reqparse @@ -27,12 +27,12 @@ component_create_parser.add_argument("type_id", type=int, required=True, locatio @api.route("/<component_id>") @api.param("competition_id, slide_id, component_id") class ComponentByID(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, component_id): item = dbc.get.component(competition_id, slide_id, component_id) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, component_id): args = component_edit_parser.parse_args(strict=True) item = dbc.get.component(competition_id, slide_id, component_id) @@ -40,7 +40,7 @@ class ComponentByID(Resource): item = dbc.edit.default(item, **args_without_none) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, component_id): item = dbc.get.component(competition_id, slide_id, component_id) dbc.delete.component(item) @@ -50,12 +50,12 @@ class ComponentByID(Resource): @api.route("") @api.param("competition_id, slide_id") class ComponentList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id): items = dbc.get.component_list(competition_id, slide_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): args = component_create_parser.parse_args() item = dbc.add.component(slide_id=slide_id, **args) diff --git a/server/app/apis/media.py b/server/app/apis/media.py index 5d89550ac19aaff78643a34ae0a3059c011aed23..c7de8c4d4c1cbd3f482d386e654a9bfd0063370b 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import MediaDTO from app.core.parsers import search_parser from app.database.models import Media @@ -22,13 +22,13 @@ media_parser_search.add_argument("filename", type=str, default=None, location="a @api.route("/images") class ImageList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = media_parser_search.parse_args(strict=True) items, total = dbc.search.image(**args) return list_response(list_schema.dump(items), total) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self): if "image" not in request.files: api.abort(codes.BAD_REQUEST, "Missing image in request.files") @@ -48,12 +48,12 @@ class ImageList(Resource): @api.route("/images/<ID>") @api.param("ID") class ImageList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, ID): item = dbc.get.one(Media, ID) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, ID): item = dbc.get.one(Media, ID) try: diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index ab52362d46d026d8483c52f5c4d8384f32de755d..20a84e4c17c138b3a94ea6e3902e67154036cabc 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -1,5 +1,5 @@ import app.database.controller as dbc -from app.apis import check_jwt, list_response +from app.apis import list_response, protect_route from app.core import http_codes from app.core.dto import MiscDTO from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType @@ -23,6 +23,7 @@ name_parser.add_argument("name", type=str, required=True, location="json") @api.route("/types") class TypesList(Resource): + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self): result = {} result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) @@ -34,7 +35,7 @@ class TypesList(Resource): @api.route("/roles") class RoleList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): items = dbc.get.all(Role) return list_response(role_schema.dump(items)) @@ -42,12 +43,12 @@ class RoleList(Resource): @api.route("/cities") class CitiesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def post(self): args = name_parser.parse_args(strict=True) dbc.add.city(args["name"]) @@ -58,7 +59,7 @@ class CitiesList(Resource): @api.route("/cities/<ID>") @api.param("ID") class Cities(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def put(self, ID): item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) @@ -67,7 +68,7 @@ class Cities(Resource): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def delete(self, ID): item = dbc.get.one(City, ID) dbc.delete.default(item) @@ -77,7 +78,7 @@ class Cities(Resource): @api.route("/statistics") class Statistics(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): user_count = User.query.count() competition_count = Competition.query.count() diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index aaefafc40608d14bf3a4e8a2a3b7fdfbda559f2c..de40a310db5f11a1b0c03cb20e1546f74682af70 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionDTO from flask_restx import Resource from flask_restx import reqparse @@ -18,7 +18,7 @@ question_parser.add_argument("type_id", type=int, default=None, location="json") @api.route("/questions") @api.param("competition_id") class QuestionList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.question_list_for_competition(competition_id) return list_response(list_schema.dump(items)) @@ -27,12 +27,12 @@ class QuestionList(Resource): @api.route("/slides/<slide_id>/questions") @api.param("competition_id, slide_id") class QuestionListForSlide(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id): items = dbc.get.question_list(competition_id, slide_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): args = question_parser.parse_args(strict=True) item = dbc.add.question(slide_id=slide_id, **args) @@ -42,12 +42,12 @@ class QuestionListForSlide(Resource): @api.route("/slides/<slide_id>/questions/<question_id>") @api.param("competition_id, slide_id, question_id") class QuestionById(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id, question_id): item_question = dbc.get.question(competition_id, slide_id, question_id) return item_response(schema.dump(item_question)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, question_id): args = question_parser.parse_args(strict=True) @@ -56,7 +56,7 @@ class QuestionById(Resource): return item_response(schema.dump(item_question)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, question_id): item_question = dbc.get.question(competition_id, slide_id, question_id) dbc.delete.question(item_question) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index da29633a794cdb1262d2c307b8c80a94c794830e..4357df9e24e95c6febc448f6df5b7fd55912a5a2 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import SlideDTO from flask_restx import Resource from flask_restx import reqparse @@ -19,12 +19,12 @@ slide_parser.add_argument("background_image_id", default=None, type=int, locatio @api.route("") @api.param("competition_id") class SlidesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.slide_list(competition_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): item_slide = dbc.add.slide(competition_id) return item_response(schema.dump(item_slide)) @@ -33,12 +33,12 @@ class SlidesList(Resource): @api.route("/<slide_id>") @api.param("competition_id,slide_id") class Slides(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) return item_response(schema.dump(item_slide)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id): args = slide_parser.parse_args(strict=True) @@ -47,7 +47,7 @@ class Slides(Resource): return item_response(schema.dump(item_slide)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) @@ -58,7 +58,7 @@ class Slides(Resource): @api.route("/<slide_id>/order") @api.param("competition_id,slide_id") class SlideOrder(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id): args = slide_parser.parse_args(strict=True) order = args.get("order") @@ -87,7 +87,7 @@ class SlideOrder(Resource): @api.route("/<slide_id>/copy") @api.param("competition_id,slide_id") class SlideCopy(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 3fcb276c959c8880be30713bf08869d6b30b8dbb..c951ae53e6b7839d1457e60ccce60ea398ee617d 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import TeamDTO from flask_restx import Resource, reqparse from flask_restx import reqparse @@ -16,12 +16,12 @@ team_parser.add_argument("name", type=str, location="json") @api.route("") @api.param("competition_id") class TeamsList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.team_list(competition_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): args = team_parser.parse_args(strict=True) item_team = dbc.add.team(args["name"], competition_id) @@ -31,19 +31,19 @@ class TeamsList(Resource): @api.route("/<team_id>") @api.param("competition_id,team_id") class Teams(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, team_id): item = dbc.get.team(competition_id, team_id) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, team_id): item_team = dbc.get.team(competition_id, team_id) dbc.delete.team(item_team) return {}, codes.NO_CONTENT - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, team_id): args = team_parser.parse_args(strict=True) name = args.get("name") diff --git a/server/app/apis/users.py b/server/app/apis/users.py index a5b2d58b01f9ce8931fd29cba3af9d1d8b825cc0..50470db724ebf3648a2ff3a206a87b53650417ee 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import UserDTO from flask_jwt_extended import get_jwt_identity from flask_restx import Resource @@ -40,12 +40,12 @@ def _edit_user(item_user, args): @api.route("") class UsersList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): item = dbc.get.user(get_jwt_identity()) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self): args = user_parser.parse_args(strict=True) item = dbc.get.user(get_jwt_identity()) @@ -56,12 +56,12 @@ class UsersList(Resource): @api.route("/<ID>") @api.param("ID") class Users(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, ID): item = dbc.get.user(ID) return item_response(schema.dump(item)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def put(self, ID): args = user_parser.parse_args(strict=True) item = dbc.get.user(ID) @@ -71,7 +71,7 @@ class Users(Resource): @api.route("/search") class UserSearch(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = user_search_parser.parse_args(strict=True) items, total = dbc.search.user(**args) diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index a83f5b95e8988a3d0024ca6d2f3b9601426147cc..57e41705cb06f500c8678510ac1ba0629eb9c0e7 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -151,9 +151,13 @@ def competition(name, year, city_id): # Add code for Judge view code(2, item_competition.id) + # Add code for Audience view code(3, item_competition.id) + # Add code for Operator view + code(4, item_competition.id) + item_competition = utils.refresh(item_competition) return item_competition diff --git a/server/app/database/models.py b/server/app/database/models.py index 3012938955f5c9e4fe1e285a0d95478ca7f7b8dd..b697b830cf2a8c4d55d3fb69a5be56ceececa50e 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -230,6 +230,8 @@ class Code(db.Model): competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=True) + view_type = db.relationship("ViewType", uselist=False) + def __init__(self, code, view_type_id, competition_id=None, team_id=None): self.code = code self.view_type_id = view_type_id @@ -240,7 +242,6 @@ class Code(db.Model): class ViewType(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) - codes = db.relationship("Code", backref="view_type") def __init__(self, name): self.name = name diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 199df387e878fdbbde759a80e6aa3d9ea486ea7d..d59428a6380b74abe8853c6c09c4df0bafc031e0 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -3,7 +3,9 @@ This file tests the api function calls. """ import app.core.http_codes as codes +from app.database.controller.add import competition from app.database.models import Slide +from app.core import sockets from tests import app, client, db from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put @@ -342,7 +344,7 @@ def test_question_api(client): slide_order = 1 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 1 + assert body["count"] == 2 # Get questions from another competition that should have some questions CID = 3 @@ -385,3 +387,67 @@ def test_question_api(client): response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND """ + + +def test_authorization(client): + add_default_values() + + # Fake that competition 1 is active + sockets.presentations[1] = {} + + #### TEAM #### + # Login in with team code + response, body = post(client, "/api/auth/login/code", {"code": "111111"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + competition_id = body["competition_id"] + team_id = body["team_id"] + + # Get competition team is in + response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK + + # Try to delete competition team is in + response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Try to get a different competition + response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Get own answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == codes.OK + + # Try to get another teams answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + #### JUDGE #### + # Login in with judge code + response, body = post(client, "/api/auth/login/code", {"code": "222222"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + competition_id = body["competition_id"] + + # Get competition judge is in + response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK + + # Try to delete competition judge is in + response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Try to get a different competition + response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Get team answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == codes.OK + + # Also get antoher teams answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == codes.OK \ No newline at end of file diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index b5f1e54e136b759d9924a534acf2f37e6f7b08cd..5dabdbbe5508538291ce9eb52e078c21c8995132 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -3,14 +3,14 @@ import json import app.core.http_codes as codes import app.database.controller as dbc from app.core import db -from app.database.models import City, Role +from app.database.models import City, Code, Role def add_default_values(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] component_types = ["Text", "Image"] - view_types = ["Team", "Judge", "Audience"] + view_types = ["Team", "Judge", "Audience", "Operator"] roles = ["Admin", "Editor"] cities = ["Linköping", "Testköping"] @@ -40,6 +40,20 @@ def add_default_values(): # Add competitions item_competition = dbc.add.competition("Tom tävling", 2012, item_city.id) + + item_question = dbc.add.question("hej", 5, 1, item_competition.slides[0].id) + + item_team1 = dbc.add.team("Hej lag 3", item_competition.id) + item_team2 = dbc.add.team("Hej lag 4", item_competition.id) + + db.session.add(Code("111111", 1, item_competition.id, item_team1.id)) # Team + db.session.add(Code("222222", 2, item_competition.id)) # Judge + + dbc.add.QuestionAnswer("hej", 5, item_question.id, item_team1) + dbc.add.QuestionAnswer("då", 5, item_question.id, item_team2) + + db.session.commit() + for j in range(2): item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id) # Add two more slides to competition