diff --git a/CTFd/api/v1/awards.py b/CTFd/api/v1/awards.py index 3f346fff..cc151338 100644 --- a/CTFd/api/v1/awards.py +++ b/CTFd/api/v1/awards.py @@ -3,9 +3,12 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_standings +from CTFd.constants import RawEnum from CTFd.models import Awards, Users, db from CTFd.schemas.awards import AwardSchema from CTFd.utils.config import is_teams_mode @@ -35,6 +38,55 @@ awards_namespace.schema_model( @awards_namespace.route("") class AwardList(Resource): + @admins_only + @awards_namespace.doc( + description="Endpoint to list Award objects in bulk", + responses={ + 200: ("Success", "AwardListSuccessResponse"), + 400: ( + "An error occured processing the provided or stored data", + "APISimpleErrorResponse", + ), + }, + ) + @validate_args( + { + "user_id": (int, None), + "team_id": (int, None), + "type": (str, None), + "value": (int, None), + "category": (int, None), + "icon": (int, None), + "q": (str, None), + "field": ( + RawEnum( + "AwardFields", + { + "name": "name", + "description": "description", + "category": "category", + "icon": "icon", + }, + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Awards, query=q, field=field) + + awards = Awards.query.filter_by(**query_args).filter(*filters).all() + schema = AwardSchema(many=True) + response = schema.dump(awards) + + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + return {"success": True, "data": response.data} + @admins_only @awards_namespace.doc( description="Endpoint to create an Award object", diff --git a/CTFd/api/v1/challenges.py b/CTFd/api/v1/challenges.py index c33a98a5..44054701 100644 --- a/CTFd/api/v1/challenges.py +++ b/CTFd/api/v1/challenges.py @@ -5,9 +5,12 @@ from flask import abort, render_template, request, url_for from flask_restx import Namespace, Resource from sqlalchemy.sql import and_ +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_standings +from CTFd.constants import RawEnum from CTFd.models import ChallengeFiles as ChallengeFilesModel from CTFd.models import ( Challenges, @@ -86,19 +89,56 @@ class ChallengeList(Resource): ), }, ) - def get(self): + @validate_args( + { + "name": (str, None), + "max_attempts": (int, None), + "value": (int, None), + "category": (str, None), + "type": (str, None), + "state": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "ChallengeFields", + { + "name": "name", + "description": "description", + "category": "category", + "type": "type", + "state": "state", + }, + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + # Build filtering queries + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Challenges, query=q, field=field) + # This can return None (unauth) if visibility is set to public user = get_current_user() # Admins can request to see everything if is_admin() and request.args.get("view") == "admin": - challenges = Challenges.query.order_by(Challenges.value).all() + challenges = ( + Challenges.query.filter_by(**query_args) + .filter(*filters) + .order_by(Challenges.value) + .all() + ) solve_ids = set([challenge.id for challenge in challenges]) else: challenges = ( Challenges.query.filter( and_(Challenges.state != "hidden", Challenges.state != "locked") ) + .filter_by(**query_args) + .filter(*filters) .order_by(Challenges.value) .all() ) diff --git a/CTFd/api/v1/config.py b/CTFd/api/v1/config.py index 1dadd77a..12e68233 100644 --- a/CTFd/api/v1/config.py +++ b/CTFd/api/v1/config.py @@ -3,9 +3,12 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_config, clear_standings +from CTFd.constants import RawEnum from CTFd.models import Configs, db from CTFd.schemas.config import ConfigSchema from CTFd.utils import get_config, set_config @@ -46,8 +49,21 @@ class ConfigList(Resource): ), }, ) - def get(self): - configs = Configs.query.all() + @validate_args( + { + "key": (str, None), + "value": (str, None), + "q": (str, None), + "field": (RawEnum("ConfigFields", {"key": "key", "value": "value"}), None), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Configs, query=q, field=field) + + configs = Configs.query.filter_by(**query_args).filter(*filters).all() schema = ConfigSchema(many=True) response = schema.dump(configs) if response.errors: diff --git a/CTFd/api/v1/files.py b/CTFd/api/v1/files.py index 61696788..46f8ef1d 100644 --- a/CTFd/api/v1/files.py +++ b/CTFd/api/v1/files.py @@ -3,8 +3,11 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum from CTFd.models import Files, db from CTFd.schemas.files import FileSchema from CTFd.utils import uploads @@ -45,9 +48,24 @@ class FilesList(Resource): ), }, ) - def get(self): - file_type = request.args.get("type") - files = Files.query.filter_by(type=file_type).all() + @validate_args( + { + "type": (str, None), + "location": (str, None), + "q": (str, None), + "field": ( + RawEnum("FileFields", {"type": "type", "location": "location"}), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Files, query=q, field=field) + + files = Files.query.filter_by(**query_args).filter(*filters).all() schema = FileSchema(many=True) response = schema.dump(files) diff --git a/CTFd/api/v1/flags.py b/CTFd/api/v1/flags.py index f2010d10..8be4f67f 100644 --- a/CTFd/api/v1/flags.py +++ b/CTFd/api/v1/flags.py @@ -3,8 +3,11 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum from CTFd.models import Flags, db from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class from CTFd.schemas.flags import FlagSchema @@ -45,8 +48,28 @@ class FlagList(Resource): ), }, ) - def get(self): - flags = Flags.query.all() + @validate_args( + { + "challenge_id": (int, None), + "type": (str, None), + "content": (str, None), + "data": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "FlagFields", {"type": "type", "content": "content", "data": "data"} + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Flags, query=q, field=field) + + flags = Flags.query.filter_by(**query_args).filter(*filters).all() schema = FlagSchema(many=True) response = schema.dump(flags) if response.errors: diff --git a/CTFd/api/v1/helpers/models.py b/CTFd/api/v1/helpers/models.py new file mode 100644 index 00000000..0bc8f6fe --- /dev/null +++ b/CTFd/api/v1/helpers/models.py @@ -0,0 +1,7 @@ +def build_model_filters(model, query, field): + filters = [] + if query: + # The field exists as an exposed column + if model.__mapper__.has_property(field): + filters.append(getattr(model, field).like("%{}%".format(query))) + return filters diff --git a/CTFd/api/v1/hints.py b/CTFd/api/v1/hints.py index a5353487..fd4604e5 100644 --- a/CTFd/api/v1/hints.py +++ b/CTFd/api/v1/hints.py @@ -3,8 +3,11 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum from CTFd.models import Hints, HintUnlocks, db from CTFd.schemas.hints import HintSchema from CTFd.utils.decorators import admins_only, authed_only, during_ctf_time_only @@ -45,8 +48,26 @@ class HintList(Resource): ), }, ) - def get(self): - hints = Hints.query.all() + @validate_args( + { + "type": (str, None), + "challenge_id": (int, None), + "content": (str, None), + "cost": (int, None), + "q": (str, None), + "field": ( + RawEnum("HintFields", {"type": "type", "content": "content"}), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Hints, query=q, field=field) + + hints = Hints.query.filter_by(**query_args).filter(*filters).all() response = HintSchema(many=True).dump(hints) if response.errors: diff --git a/CTFd/api/v1/notifications.py b/CTFd/api/v1/notifications.py index 279a6e4c..c10eea50 100644 --- a/CTFd/api/v1/notifications.py +++ b/CTFd/api/v1/notifications.py @@ -3,8 +3,11 @@ from typing import List from flask import current_app, request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum from CTFd.models import Notifications, db from CTFd.schemas.notifications import NotificationSchema from CTFd.utils.decorators import admins_only @@ -46,8 +49,28 @@ class NotificantionList(Resource): ), }, ) - def get(self): - notifications = Notifications.query.all() + @validate_args( + { + "title": (str, None), + "content": (str, None), + "user_id": (int, None), + "team_id": (int, None), + "q": (str, None), + "field": ( + RawEnum("NotificationFields", {"title": "title", "content": "content"}), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Notifications, query=q, field=field) + + notifications = ( + Notifications.query.filter_by(**query_args).filter(*filters).all() + ) schema = NotificationSchema(many=True) result = schema.dump(notifications) if result.errors: diff --git a/CTFd/api/v1/pages.py b/CTFd/api/v1/pages.py index bdec1368..62c19bbf 100644 --- a/CTFd/api/v1/pages.py +++ b/CTFd/api/v1/pages.py @@ -3,10 +3,12 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_pages +from CTFd.constants import RawEnum from CTFd.models import Pages, db from CTFd.schemas.pages import PageSchema from CTFd.utils.decorators import admins_only @@ -59,11 +61,23 @@ class PageList(Resource): "draft": (bool, None), "hidden": (bool, None), "auth_required": (bool, None), + "q": (str, None), + "field": ( + RawEnum( + "PageFields", + {"title": "title", "route": "route", "content": "content"}, + ), + None, + ), }, location="query", ) - def get(self, query): - pages = Pages.query.filter_by(**query).all() + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Pages, query=q, field=field) + + pages = Pages.query.filter_by(**query_args).filter(*filters).all() schema = PageSchema(exclude=["content"], many=True) response = schema.dump(pages) if response.errors: diff --git a/CTFd/api/v1/submissions.py b/CTFd/api/v1/submissions.py index 490797c9..299d6112 100644 --- a/CTFd/api/v1/submissions.py +++ b/CTFd/api/v1/submissions.py @@ -1,8 +1,8 @@ from typing import List -from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import ( @@ -10,6 +10,7 @@ from CTFd.api.v1.schemas import ( PaginatedAPIListSuccessResponse, ) from CTFd.cache import clear_standings +from CTFd.constants import RawEnum from CTFd.models import Submissions, db from CTFd.schemas.submissions import SubmissionSchema from CTFd.utils.decorators import admins_only @@ -52,21 +53,45 @@ class SubmissionsList(Resource): ), }, ) - def get(self): - args = request.args.to_dict() + @validate_args( + { + "challenge_id": (int, None), + "user_id": (int, None), + "team_id": (int, None), + "ip": (str, None), + "provided": (str, None), + "type": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "SubmissionFields", + { + "challenge_id": "challenge_id", + "user_id": "user_id", + "team_id": "team_id", + "ip": "ip", + "provided": "provided", + "type": "type", + }, + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Submissions, query=q, field=field) + + args = query_args schema = SubmissionSchema(many=True) - pagination_args = { - "per_page": int(args.pop("per_page", 50)), - "page": int(args.pop("page", 1)), - } - if args: - submissions = Submissions.query.filter_by(**args).paginate( - **pagination_args, max_per_page=100 - ) - else: - submissions = Submissions.query.paginate( - **pagination_args, max_per_page=100 - ) + + submissions = ( + Submissions.query.filter_by(**args) + .filter(*filters) + .paginate(max_per_page=100) + ) response = schema.dump(submissions.items) diff --git a/CTFd/api/v1/tags.py b/CTFd/api/v1/tags.py index 828b9fce..a7849f91 100644 --- a/CTFd/api/v1/tags.py +++ b/CTFd/api/v1/tags.py @@ -3,8 +3,11 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum from CTFd.models import Tags, db from CTFd.schemas.tags import TagSchema from CTFd.utils.decorators import admins_only @@ -42,9 +45,26 @@ class TagList(Resource): ), }, ) - def get(self): - # TODO: Filter by challenge_id - tags = Tags.query.all() + @validate_args( + { + "challenge_id": (int, None), + "value": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "TagFields", {"challenge_id": "challenge_id", "value": "value"} + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Tags, query=q, field=field) + + tags = Tags.query.filter_by(**query_args).filter(*filters).all() schema = TagSchema(many=True) response = schema.dump(tags) diff --git a/CTFd/api/v1/teams.py b/CTFd/api/v1/teams.py index 2944c54e..dde077f7 100644 --- a/CTFd/api/v1/teams.py +++ b/CTFd/api/v1/teams.py @@ -4,12 +4,15 @@ from typing import List from flask import abort, request, session from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import ( APIDetailedSuccessResponse, PaginatedAPIListSuccessResponse, ) from CTFd.cache import clear_standings, clear_team_session, clear_user_session +from CTFd.constants import RawEnum from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db from CTFd.schemas.awards import AwardSchema from CTFd.schemas.submissions import SubmissionSchema @@ -57,12 +60,44 @@ class TeamList(Resource): ), }, ) - def get(self): + @validate_args( + { + "affiliation": (str, None), + "country": (str, None), + "bracket": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "TeamFields", + { + "name": "name", + "website": "website", + "country": "country", + "bracket": "bracket", + "affiliation": "affiliation", + }, + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Teams, query=q, field=field) + if is_admin() and request.args.get("view") == "admin": - teams = Teams.query.filter_by().paginate(per_page=50, max_per_page=100) + teams = ( + Teams.query.filter_by(**query_args) + .filter(*filters) + .paginate(per_page=50, max_per_page=100) + ) else: - teams = Teams.query.filter_by(hidden=False, banned=False).paginate( - per_page=50, max_per_page=100 + teams = ( + Teams.query.filter_by(hidden=False, banned=False, **query_args) + .filter(*filters) + .paginate(per_page=50, max_per_page=100) ) user_type = get_current_user_type(fallback="user") diff --git a/CTFd/api/v1/unlocks.py b/CTFd/api/v1/unlocks.py index 966a3ecd..b3ca5d49 100644 --- a/CTFd/api/v1/unlocks.py +++ b/CTFd/api/v1/unlocks.py @@ -3,9 +3,12 @@ from typing import List from flask import request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_standings +from CTFd.constants import RawEnum from CTFd.models import Unlocks, db, get_class_by_tablename from CTFd.schemas.awards import AwardSchema from CTFd.schemas.unlocks import UnlockSchema @@ -53,10 +56,28 @@ class UnlockList(Resource): ), }, ) - def get(self): - hints = Unlocks.query.all() + @validate_args( + { + "user_id": (int, None), + "team_id": (int, None), + "target": (int, None), + "type": (str, None), + "q": (str, None), + "field": ( + RawEnum("UnlockFields", {"target": "target", "type": "type"}), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Unlocks, query=q, field=field) + + unlocks = Unlocks.query.filter_by(**query_args).filter(*filters).all() schema = UnlockSchema() - response = schema.dump(hints) + response = schema.dump(unlocks) if response.errors: return {"success": False, "errors": response.errors}, 400 diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py index 9f211a39..36a11465 100644 --- a/CTFd/api/v1/users.py +++ b/CTFd/api/v1/users.py @@ -3,12 +3,15 @@ from typing import List from flask import abort, request from flask_restx import Namespace, Resource +from CTFd.api.v1.helpers.models import build_model_filters +from CTFd.api.v1.helpers.request import validate_args from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic from CTFd.api.v1.schemas import ( APIDetailedSuccessResponse, PaginatedAPIListSuccessResponse, ) from CTFd.cache import clear_standings, clear_user_session +from CTFd.constants import RawEnum from CTFd.models import ( Awards, Notifications, @@ -69,12 +72,44 @@ class UserList(Resource): ), }, ) - def get(self): + @validate_args( + { + "affiliation": (str, None), + "country": (str, None), + "bracket": (str, None), + "q": (str, None), + "field": ( + RawEnum( + "UserFields", + { + "name": "name", + "website": "website", + "country": "country", + "bracket": "bracket", + "affiliation": "affiliation", + }, + ), + None, + ), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Users, query=q, field=field) + if is_admin() and request.args.get("view") == "admin": - users = Users.query.filter_by().paginate(per_page=50, max_per_page=100) + users = ( + Users.query.filter_by(**query_args) + .filter(*filters) + .paginate(per_page=50, max_per_page=100) + ) else: - users = Users.query.filter_by(banned=False, hidden=False).paginate( - per_page=50, max_per_page=100 + users = ( + Users.query.filter_by(banned=False, hidden=False, **query_args) + .filter(*filters) + .paginate(per_page=50, max_per_page=100) ) response = UserSchema(view="user", many=True).dump(users.items)