From 7af0d4de86602c3e8d59eb1d18c425af2b2e8031 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Fri, 21 Feb 2025 20:28:36 +0530 Subject: [PATCH 01/13] docs: added documentation to all API endpoint routes so far --- src/pwncore/routes/admin.py | 35 +++++++- src/pwncore/routes/auth.py | 81 ++++++++++++++++++- src/pwncore/routes/ctf/__init__.py | 118 +++++++++++++++++++++++++-- src/pwncore/routes/ctf/pre_event.py | 120 ++++++++++++++++++++++++++-- src/pwncore/routes/ctf/start.py | 100 ++++++++++++++++++++++- src/pwncore/routes/leaderboard.py | 27 ++++++- src/pwncore/routes/team.py | 111 +++++++++++++++++++++++-- 7 files changed, 567 insertions(+), 25 deletions(-) diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index d6a5a2b..a9ef6d9 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -53,7 +53,19 @@ async def _del_cont(id: str): @atomic() -@router.get("/union") +@router.get("/union", + summary="Calculate and update team coins", + description="""Calculates team points from pre-event CTFs and updates team coins. + + Requires admin authentication via request body. + + Example response on success: + ```json + null + ``` + + Note: Returns 401 if authentication fails. + """) async def calculate_team_coins( response: Response, req: Request ): # Inefficient, anyways will be used only once @@ -84,7 +96,26 @@ async def calculate_team_coins( await team.save() -@router.get("/create") +@router.get("/create", + summary="Initialize database with sample data", + description="""Creates initial database entries including: + - Problems + - Pre-event problems + - Teams + - Users + - Hints + - Solved problems + + Requires admin authentication via request body. + + Example response on success: + ```json + null + ``` + + Note: Returns 401 if authentication fails. + This endpoint should only be used in development environment. + """) async def init_db( response: Response, req: Request ): # Inefficient, anyways will be used only once diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index 24b358f..504a617 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -39,7 +39,52 @@ def normalise_tag(tag: str): @atomic() -@router.post("/signup") +@router.post("/signup", + description="""Create a new team with associated members. + + Request body example: + ```json + { + "name": "TeamAwesome", + "password": "securepassword123", + "tags": ["user1", "user2", "user3"] + } + ``` + + Responses: + - 200: Successful signup + ```json + { + "msg_code": 13 + } + ``` + - 406: Team already exists + ```json + { + "msg_code": 17 + } + ``` + - 404: Users not found + ```json + { + "msg_code": 24, + "tags": ["user2", "user3"] + } + ``` + - 401: Users already in team + ```json + { + "msg_code": 20, + "tags": ["user1"] + } + ``` + - 500: Database error + ```json + { + "msg_code": 0 + } + ``` + """) async def signup_team(team: SignupBody, response: Response): team.name = team.name.strip() members = set(map(normalise_tag, team.tags)) @@ -83,7 +128,39 @@ async def signup_team(team: SignupBody, response: Response): return {"msg_code": config.msg_codes["signup_success"]} -@router.post("/login") +@router.post("/login", + description="""Authenticate a team and receive a JWT token. + + Request body example: + ```json + { + "name": "TeamAwesome", + "password": "securepassword123" + } + ``` + + Responses: + - 200: Successful login + ```json + { + "msg_code": 15, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer" + } + ``` + - 404: Team not found + ```json + { + "msg_code": 10 + } + ``` + - 401: Wrong password + ```json + { + "msg_code": 14 + } + ``` + """) async def team_login(team_data: AuthBody, response: Response): # TODO: Simplified logic since we're not doing refresh tokens. diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 40ce348..6c91de7 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -53,7 +53,24 @@ class Flag(BaseModel): flag: str -@router.get("/completed") +@router.get("/completed", + summary="Get completed problems", + description="""Retrieve all problems solved by the authenticated team. + + Example response: + ```json + [ + { + "id": 1, + "name": "Web 101", + "description": "Basic web exploitation", + "points": 100, + "category": "web", + "visible": true + } + ] + ``` + """) async def completed_problem_get(jwt: RequireJwt): team_id = jwt["team_id"] ViewedHint.filter(team_id=team_id).annotate() @@ -63,7 +80,26 @@ async def completed_problem_get(jwt: RequireJwt): return problems -@router.get("/list") +@router.get("/list", + summary="List all CTF problems", + description="""Get all visible CTF problems with adjusted points based on hints used. + + Example response: + ```json + [ + { + "id": 1, + "name": "Crypto 101", + "description": "Basic cryptography challenge", + "points": 90, + "category": "crypto", + "visible": true + } + ] + ``` + + Note: Points are adjusted based on hints viewed by the team. + """) async def ctf_list(jwt: RequireJwt): team_id = jwt["team_id"] problems = await Problem_Pydantic.from_queryset(Problem.filter(visible=True)) @@ -90,7 +126,29 @@ async def update_points(req: Request, ctf_id: int): @atomic() -@router.post("/{ctf_id}/flag") +@router.post("/{ctf_id}/flag", + summary="Submit flag for problem", + description="""Submit a flag for a specific CTF problem. + + Example request: + ```json + { + "flag": "flag{th1s_1s_4_fl4g}" + } + ``` + + Example response: + ```json + { + "status": true + } + ``` + + Note: + - Returns 404 if problem not found + - Returns 401 if problem already solved + - Returns 500 if database error occurs + """) async def flag_post( req: Request, ctf_id: int, flag: Flag, response: Response, jwt: RequireJwt ): @@ -138,7 +196,23 @@ async def flag_post( @atomic() -@router.get("/{ctf_id}/hint") +@router.get("/{ctf_id}/hint", + summary="Get next available hint", + description="""Retrieve the next available hint for a problem. + + Example response: + ```json + { + "text": "Look at the HTTP headers", + "order": 1 + } + ``` + + Note: + - Returns 404 if problem not found + - Returns 403 if no more hints available + - May deduct coins if team has sufficient balance + """) async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] problem = await Problem.exists(id=ctf_id, visible=True) @@ -177,7 +251,22 @@ async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): } -@router.get("/{ctf_id}/viewed_hints") +@router.get("/{ctf_id}/viewed_hints", + summary="Get viewed hints", + description="""Get all hints viewed by the team for a specific problem. + + Example response: + ```json + [ + { + "id": 1, + "text": "First hint text", + "order": 0, + "problem_id": 1 + } + ] + ``` + """) async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): team_id = jwt["team_id"] viewed_hints = await Hint_Pydantic.from_queryset( @@ -186,7 +275,24 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): return viewed_hints -@router.get("/{ctf_id}") +@router.get("/{ctf_id}", + summary="Get problem details", + description="""Get details of a specific CTF problem. + + Example response: + ```json + { + "id": 1, + "name": "Binary 101", + "description": "Basic binary exploitation", + "points": 150, + "category": "pwn", + "visible": true + } + ``` + + Note: Returns 404 if problem not found or not visible + """) async def ctf_get(ctf_id: int, response: Response): problem = await Problem_Pydantic.from_queryset( Problem.filter(id=ctf_id, visible=True) diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index c806b8d..1550f18 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -30,20 +30,68 @@ class CoinsQuery(BaseModel): tag: str -@router.get("/list") +@router.get("/list", + summary="Get all pre-event CTF problems", + description="""Returns a list of all available pre-event CTF problems. + + Example response: + ```json + [ + { + "id": 1, + "name": "Web Basic", + "description": "Find the flag in website", + "points": 100, + "date": "2024-01-15" + } + ] + ``` + + Note: Flag field is excluded from response for security. + """) async def ctf_list(): problems = await PreEventProblem_Pydantic.from_queryset(PreEventProblem.all()) return problems -@router.get("/today") +@router.get("/today", + summary="Get today's pre-event CTF problems", + description="""Returns list of CTF problems scheduled for current date. + + Example response: + ```json + [ + { + "id": 1, + "name": "Web Basic", + "description": "Find the flag in website", + "points": 100, + "date": "2024-01-15" + } + ] + ``` + + Note: Returns empty list if no problems are scheduled for today. + """) async def ctf_today(): return await PreEventProblem_Pydantic.from_queryset( PreEventProblem().filter(date=datetime.now(_IST).date()) ) -@router.get("/coins/{tag}") +@router.get("/coins/{tag}", + summary="Get user's total coins", + description="""Get total coins earned by a user in pre-event CTFs. + + Example response: + ```json + { + "coins": 300 + } + ``` + + Note: Returns 0 if user not found (msg_code: 11). + """) async def coins_get(tag: str): try: return { @@ -55,8 +103,48 @@ async def coins_get(tag: str): return 0 +@router.post("/{ctf_id}/flag", + summary="Submit flag for pre-event CTF", + description="""Submit a solution flag for a pre-event CTF problem. + + Example request: + ```json + { + "tag": "23BCE1001", + "flag": "flag{solution}", + "email": "user@example.com" + } + ``` + + Success response: + ```json + { + "status": true, + "coins": 300 + } + ``` + + Error responses: + - 404: Problem not found or not for current date + ```json + { + "msg_code": 2 + } + ``` + - 401: Problem already solved + ```json + { + "msg_code": 12 + } + ``` + - 401: User/email conflict + ```json + { + "msg_code": 23 + } + ``` + """) @atomic() -@router.post("/{ctf_id}/flag") async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Response): problem = await PreEventProblem.get_or_none(id=ctf_id) @@ -96,7 +184,29 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re return {"status": status, "coins": coins} -@router.get("/{ctf_id}") +@router.get("/{ctf_id}", + summary="Get specific CTF problem details", + description="""Get complete details of a specific pre-event CTF problem. + + Example response: + ```json + { + "id": 1, + "name": "Web Basic", + "description": "Find the flag in website", + "points": 100, + "date": "2024-01-15" + } + ``` + + Error response: + - 404: Problem not found + ```json + { + "msg_code": 2 + } + ``` + """) async def ctf_get(ctf_id: int, response: Response): problem = await PreEventProblem_Pydantic.from_queryset( PreEventProblem.filter(id=ctf_id) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 684cd9c..a22324d 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -15,7 +15,49 @@ logger = getLogger(__name__) -@router.post("/{ctf_id}/start") +@router.post("/{ctf_id}/start", + summary="Start CTF challenge container", + description="""Start a new Docker container for the specified CTF challenge. + + Example response (success): + ```json + { + "msg_code": 3, + "ports": [8080, 22], + "ctf_id": 1 + } + ``` + + Error responses: + - 404: Challenge not found + ```json + { + "msg_code": 2 + } + ``` + - 400: Container already running + ```json + { + "msg_code": 7, + "ports": [8080, 22], + "ctf_id": 1 + } + ``` + - 400: Container limit reached + ```json + { + "msg_code": 8 + } + ``` + - 500: Database error + ```json + { + "msg_code": 0 + } + ``` + + Note: Requires JWT authentication token. + """) async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): """ image_config contains the raw POST data that gets sent to the Docker Remote API. @@ -130,7 +172,27 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw } -@router.post("/stopall") +@router.post("/stopall", + summary="Stop all team containers", + description="""Stop and remove all Docker containers belonging to the authenticated team. + + Example response (success): + ```json + { + "msg_code": 5 + } + ``` + + Error response: + - 500: Database error + ```json + { + "msg_code": 0 + } + ``` + + Note: Requires JWT authentication token. + """) async def stopall_docker_container(response: Response, jwt: RequireJwt): async with in_transaction(): team_id = jwt["team_id"] # From JWT @@ -155,7 +217,39 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): return {"msg_code": config.msg_codes["containers_team_stop"]} -@router.post("/{ctf_id}/stop") +@router.post("/{ctf_id}/stop", + summary="Stop specific CTF container", + description="""Stop and remove a specific CTF challenge container. + + Example response (success): + ```json + { + "msg_code": 4 + } + ``` + + Error responses: + - 404: Challenge not found + ```json + { + "msg_code": 2 + } + ``` + - 400: Container not found + ```json + { + "msg_code": 6 + } + ``` + - 500: Database error + ```json + { + "msg_code": 0 + } + ``` + + Note: Requires JWT authentication token. + """) async def stop_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): async with in_transaction(): # Let this work on invisible problems incase diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index 2eeec92..3b2befa 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -58,6 +58,31 @@ async def get_lb(self, req: Request): gcache = ExpiringLBCache(30.0) -@router.get("") +@router.get("", + description="""Returns the current CTF leaderboard sorted by total points. + + Example response: + ```json + [ + { + "name": "Team Alpha", + "tpoints": 450 + }, + { + "name": "Team Beta", + "tpoints": 300 + }, + { + "name": "Team Gamma", + "tpoints": 150 + } + ] + ``` + + Notes: + - tpoints = sum of (problem points × penalty multiplier) + team points + - Results are cached for 30 seconds + - Cache is force-expired when problems are solved + """) async def fetch_leaderboard(req: Request): return Response(content=await gcache.get_lb(req), media_type="application/json") diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 6d9b0ed..7697230 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -27,14 +27,46 @@ class UserRemoveBody(BaseModel): tag: str -@router.get("/list") +@router.get("/list", + summary="Get all teams", + description="""Returns a complete list of registered teams. + + Example response: + ```json + [ + { + "id": 1, + "name": "Team Alpha", + "coins": 100, + "points": 250 + } + ] + ``` + """) async def team_list(): teams = await Team_Pydantic.from_queryset(Team.all()) return teams # Unable to test as adding users returns an error -@router.get("/members") +@router.get("/members", + summary="Get team members", + description="""Returns a list of all members in the authenticated team. + + Example response: + ```json + [ + { + "tag": "23BCE1000", + "name": "John Doe", + "email": "john@example.com", + "phone_num": "1234567890" + } + ] + ``` + + Note: Returns an empty list if no members are found. + """) async def team_members(jwt: RequireJwt): team_id = jwt["team_id"] members = await User_Pydantic.from_queryset(User.filter(team_id=team_id)) @@ -42,7 +74,20 @@ async def team_members(jwt: RequireJwt): return members -@router.get("/me") +@router.get("/me", + summary="Get authenticated team details", + description="""Returns the details of the currently authenticated team. + + Example response: + ```json + { + "id": 1, + "name": "Team Alpha", + "coins": 100, + "points": 250 + } + ``` + """) async def get_self_team(jwt: RequireJwt): team_id = jwt["team_id"] @@ -60,7 +105,29 @@ async def get_self_team(jwt: RequireJwt): @atomic() -@router.post("/add") +@router.post("/add", + summary="Add member to team", + description="""Add a new member to the authenticated team. + + Example request: + ```json + { + "tag": "23BCE1000", + "name": "John Doe", + "email": "john@example.com", + "phone_num": "1234567890" + } + ``` + + Example response: + ```json + { + "msg_code": 18 + } + ``` + + Note: Returns error 403 if user already exists in a team. + """) async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] @@ -85,7 +152,26 @@ async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): @atomic() -@router.post("/remove") +@router.post("/remove", + summary="Remove member from team", + description="""Remove an existing member from the authenticated team. + + Example request: + ```json + { + "tag": "23BCE1000" + } + ``` + + Example response: + ```json + { + "msg_code": 19 + } + ``` + + Note: Returns error 403 if user is not found in team. + """) async def remove_member(user_info: UserRemoveBody, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] @@ -102,7 +188,20 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ return {"msg_code": config.msg_codes["user_removed"]} -@router.get("/containers") +@router.get("/containers", + summary="Get team containers", + description="""Get all containers associated with the authenticated team. + + Example response: + ```json + { + "1": [8080, 22], + "2": [8081] + } + ``` + + Note: Object keys are problem IDs and values are lists of exposed ports. + """) async def get_team_containers(response: Response, jwt: RequireJwt): containers = await Container.filter(team_id=jwt["team_id"]).prefetch_related( "ports", "problem" From 3678197ef62223092ca1065d090afbc039b585c5 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Tue, 25 Feb 2025 22:03:44 +0530 Subject: [PATCH 02/13] docs: updated documentation for API endpoints with pydantic models created new file `response_models.py` in `src/pwncore/routes/ctf` used for API endpoint documentation for functions that start/stop/stopall docker containers in `start.py` file --- src/pwncore/routes/admin.py | 45 +++++++++------ src/pwncore/routes/auth.py | 57 ++++++++++++++++++- src/pwncore/routes/ctf/__init__.py | 56 ++++++++++++------- src/pwncore/routes/ctf/pre_event.py | 54 ++++++++++++------ src/pwncore/routes/ctf/response_models.py | 63 +++++++++++++++++++++ src/pwncore/routes/ctf/start.py | 68 +++++++++++------------ src/pwncore/routes/leaderboard.py | 9 ++- src/pwncore/routes/team.py | 25 +++++++-- 8 files changed, 275 insertions(+), 102 deletions(-) create mode 100644 src/pwncore/routes/ctf/response_models.py diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index a9ef6d9..886a32f 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -4,6 +4,8 @@ from fastapi import APIRouter, Request, Response from passlib.hash import bcrypt from tortoise.transactions import atomic, in_transaction +from pydantic import BaseModel +from typing import Optional import pwncore.containerASD as containerASD from pwncore.config import config @@ -52,16 +54,23 @@ async def _del_cont(id: str): await container.delete() +class AdminResponse(BaseModel): + success: bool + message: Optional[str] = None + + @atomic() @router.get("/union", summary="Calculate and update team coins", - description="""Calculates team points from pre-event CTFs and updates team coins. - - Requires admin authentication via request body. + response_model=AdminResponse, + response_description="""Successful calculation of team points and coin updates. - Example response on success: + Example response: ```json - null + { + "success": true, + "message": "Team coins updated successfully" + } ``` Note: Returns 401 if authentication fails. @@ -71,7 +80,7 @@ async def calculate_team_coins( ): # Inefficient, anyways will be used only once if not bcrypt.verify((await req.body()).strip(), ADMIN_HASH): response.status_code = 401 - return + return AdminResponse(success=False, message="Authentication failed") async with in_transaction(): logging.info("Calculating team points form pre-event CTFs:") team_ids = await Team.filter().values_list("id", flat=True) @@ -95,22 +104,20 @@ async def calculate_team_coins( logging.info(f"{team.id}) {team.name}: {team.coins}") await team.save() + return AdminResponse(success=True, message="Team coins updated successfully") + @router.get("/create", summary="Initialize database with sample data", - description="""Creates initial database entries including: - - Problems - - Pre-event problems - - Teams - - Users - - Hints - - Solved problems - - Requires admin authentication via request body. + response_model=AdminResponse, + response_description="""Database initialization with sample data. - Example response on success: + Example response: ```json - null + { + "success": true, + "message": "Database initialized with sample data" + } ``` Note: Returns 401 if authentication fails. @@ -121,7 +128,7 @@ async def init_db( ): # Inefficient, anyways will be used only once if not bcrypt.verify((await req.body()).strip(), ADMIN_HASH): response.status_code = 401 - return + return AdminResponse(success=False, message="Authentication failed") await Problem.create( name="Invisible-Incursion", description="Chod de tujhe se na ho paye", @@ -239,3 +246,5 @@ async def init_db( await SolvedProblem.create(team_id=2, problem_id=1) await SolvedProblem.create(team_id=2, problem_id=2) await SolvedProblem.create(team_id=1, problem_id=2) + + return AdminResponse(success=True, message="Database initialized with sample data") diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index 504a617..6c43e93 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -23,6 +23,7 @@ logger = getLogger(__name__) +# defining Pydantic response models class AuthBody(BaseModel): name: str password: str @@ -34,13 +35,60 @@ class SignupBody(BaseModel): tags: set[str] +# Response Models +class SignupResponse(BaseModel): + """ + msg_code: 13 (signup_success) + """ + msg_code: t.Literal[13] + +class SignupErrorUsersNotFound(BaseModel): + """ + msg_code: 24 (users_not_found) + """ + msg_code: t.Literal[24] + tags: list[str] + +class SignupErrorUsersInTeam(BaseModel): + """ + msg_code: 20 (user_already_in_team) + """ + msg_code: t.Literal[20] + tags: list[str] + +class LoginResponse(BaseModel): + """ + msg_code: 15 (login_success) + """ + msg_code: t.Literal[15] + access_token: str + token_type: str + +class ErrorResponse(BaseModel): + """ + msg_code can be: + 0 (db_error) + 17 (team_exists) + 10 (team_not_found) + 14 (wrong_password) + """ + msg_code: t.Literal[0, 17, 10, 14] + + def normalise_tag(tag: str): return tag.strip().casefold() @atomic() @router.post("/signup", - description="""Create a new team with associated members. + response_model=SignupResponse, + responses={ + 406: {"model": ErrorResponse}, + 404: {"model": SignupErrorUsersNotFound}, + 401: {"model": SignupErrorUsersInTeam}, + 500: {"model": ErrorResponse} + }, + response_description="""Create a new team with associated members. Request body example: ```json @@ -129,7 +177,12 @@ async def signup_team(team: SignupBody, response: Response): @router.post("/login", - description="""Authenticate a team and receive a JWT token. + response_model=LoginResponse, + responses={ + 404: {"model": ErrorResponse}, + 401: {"model": ErrorResponse} + }, + response_description="""Authenticate a team and receive a JWT token. Request body example: ```json diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 6c91de7..2b9b5ee 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -53,9 +53,11 @@ class Flag(BaseModel): flag: str -@router.get("/completed", +@router.get( + "/completed", summary="Get completed problems", - description="""Retrieve all problems solved by the authenticated team. + response_model=list[Problem_Pydantic], + response_description="""Returns all problems solved by the authenticated team. Example response: ```json @@ -80,9 +82,11 @@ async def completed_problem_get(jwt: RequireJwt): return problems -@router.get("/list", +@router.get( + "/list", summary="List all CTF problems", - description="""Get all visible CTF problems with adjusted points based on hints used. + response_model=list[Problem_Pydantic], + response_description="""Returns all visible CTF problems with adjusted points based on hints used. Example response: ```json @@ -126,9 +130,11 @@ async def update_points(req: Request, ctf_id: int): @atomic() -@router.post("/{ctf_id}/flag", +@router.post( + "/{ctf_id}/flag", summary="Submit flag for problem", - description="""Submit a flag for a specific CTF problem. + response_model=dict[str, bool | str], + response_description="""Submit a flag for a specific CTF problem. Example request: ```json @@ -144,10 +150,11 @@ async def update_points(req: Request, ctf_id: int): } ``` - Note: - - Returns 404 if problem not found - - Returns 401 if problem already solved - - Returns 500 if database error occurs + Error responses: + - 404: {"msg_code": 2} - ctf_not_found + - 401: {"msg_code": 12} - ctf_solved + - 500: {"msg_code": 0} - db_error + - 400: {"msg_code": 6} - container_not_found """) async def flag_post( req: Request, ctf_id: int, flag: Flag, response: Response, jwt: RequireJwt @@ -196,9 +203,11 @@ async def flag_post( @atomic() -@router.get("/{ctf_id}/hint", +@router.get( + "/{ctf_id}/hint", summary="Get next available hint", - description="""Retrieve the next available hint for a problem. + response_model=dict[str, str | int], + response_description="""Retrieve the next available hint for a problem. Example response: ```json @@ -208,10 +217,10 @@ async def flag_post( } ``` - Note: - - Returns 404 if problem not found - - Returns 403 if no more hints available - - May deduct coins if team has sufficient balance + Error responses: + - 404: {"msg_code": 2} - ctf_not_found + - 403: {"msg_code": 9} - hint_limit_reached + - 400: {"msg_code": 22} - Insufficient coins """) async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] @@ -251,9 +260,11 @@ async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): } -@router.get("/{ctf_id}/viewed_hints", +@router.get( + "/{ctf_id}/viewed_hints", summary="Get viewed hints", - description="""Get all hints viewed by the team for a specific problem. + response_model=list[Hint_Pydantic], + response_description="""Get all hints viewed by the team for a specific problem. Example response: ```json @@ -275,9 +286,11 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): return viewed_hints -@router.get("/{ctf_id}", +@router.get( + "/{ctf_id}", summary="Get problem details", - description="""Get details of a specific CTF problem. + response_model=Problem_Pydantic, + response_description="""Get details of a specific CTF problem. Example response: ```json @@ -291,7 +304,8 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): } ``` - Note: Returns 404 if problem not found or not visible + Error responses: + - 404: {"msg_code": 2} - ctf_not_found or not visible """) async def ctf_get(ctf_id: int, response: Response): problem = await Problem_Pydantic.from_queryset( diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index 1550f18..1b3d8ba 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -2,6 +2,8 @@ from datetime import datetime, timezone, timedelta +from typing import Union + from fastapi import APIRouter, Response from pydantic import BaseModel from tortoise.transactions import atomic @@ -19,7 +21,7 @@ router = APIRouter(prefix="/pre", tags=["ctf"]) _IST = timezone(timedelta(hours=5, minutes=30)) - +# pydantic response models class PreEventFlag(BaseModel): tag: str flag: str @@ -29,10 +31,22 @@ class PreEventFlag(BaseModel): class CoinsQuery(BaseModel): tag: str +class CoinsResponse(BaseModel): + coins: int + +class FlagSubmissionResponse(BaseModel): + status: bool + coins: int + +class ErrorResponse(BaseModel): + msg_code: int + -@router.get("/list", +@router.get( + "/list", summary="Get all pre-event CTF problems", - description="""Returns a list of all available pre-event CTF problems. + response_model=list[PreEventProblem_Pydantic], + response_description="""Returns a list of all available pre-event CTF problems. Example response: ```json @@ -54,9 +68,11 @@ async def ctf_list(): return problems -@router.get("/today", +@router.get( + "/today", summary="Get today's pre-event CTF problems", - description="""Returns list of CTF problems scheduled for current date. + response_model=list[PreEventProblem_Pydantic], + response_description="""Returns list of CTF problems scheduled for current date. Example response: ```json @@ -79,9 +95,11 @@ async def ctf_today(): ) -@router.get("/coins/{tag}", +@router.get( + "/coins/{tag}", summary="Get user's total coins", - description="""Get total coins earned by a user in pre-event CTFs. + response_model=CoinsResponse, + response_description="""Get total coins earned by a user in pre-event CTFs. Example response: ```json @@ -103,9 +121,11 @@ async def coins_get(tag: str): return 0 -@router.post("/{ctf_id}/flag", +@router.post( + "/{ctf_id}/flag", summary="Submit flag for pre-event CTF", - description="""Submit a solution flag for a pre-event CTF problem. + response_model=Union[FlagSubmissionResponse, ErrorResponse], + response_description="""Submit a solution flag for a pre-event CTF problem. Example request: ```json @@ -125,19 +145,19 @@ async def coins_get(tag: str): ``` Error responses: - - 404: Problem not found or not for current date + - 404: if ctf_not_found or not for current date ```json { "msg_code": 2 } ``` - - 401: Problem already solved + - 401: if ctf_solved already ```json { "msg_code": 12 } ``` - - 401: User/email conflict + - 401: user_or_email_exists ```json { "msg_code": 23 @@ -184,9 +204,11 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re return {"status": status, "coins": coins} -@router.get("/{ctf_id}", - summary="Get specific CTF problem details", - description="""Get complete details of a specific pre-event CTF problem. +@router.get( + "/{ctf_id}", + summary="Get specific CTF problem details", + response_model=Union[list[PreEventProblem_Pydantic], ErrorResponse], + response_description="""Get complete details of a specific pre-event CTF problem. Example response: ```json @@ -200,7 +222,7 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re ``` Error response: - - 404: Problem not found + - 404: ctf_not_found ```json { "msg_code": 2 diff --git a/src/pwncore/routes/ctf/response_models.py b/src/pwncore/routes/ctf/response_models.py new file mode 100644 index 0000000..b98ef9f --- /dev/null +++ b/src/pwncore/routes/ctf/response_models.py @@ -0,0 +1,63 @@ +from pydantic import BaseModel, Field +from typing import List, Literal + +class ContainerStartResponse(BaseModel): + """Pydantic Response model for container start operation + + Message codes: + - 3: Container started successfully + - 7: Container already running + - 8: Container limit reached + """ + msg_code: Literal[3, 7, 8] = Field( + description="Status code indicating operation result: 3=success, 7=already running, 8=limit reached" + ) + ports: List[int] = Field( + default=None, + description="List of mapped container ports" + ) + ctf_id: int = Field( + default=None, + description="ID of the CTF challenge" + ) + +class ContainerStopResponse(BaseModel): + """Pydantic Response model for container stop operations + + Message codes: + - 4: Container stopped successfully + - 5: All team containers stopped successfully + """ + msg_code: Literal[4, 5] = Field( + description="Status code indicating operation result: 4=single stop, 5=stop all" + ) + +class ErrorResponse(BaseModel): + """Pydantic Error response model + + Message codes: + - 0: Database error + - 2: CTF not found + - 6: Container not found + """ + msg_code: Literal[0, 2, 6] = Field( + description="Error code: 0=DB error, 2=CTF not found, 6=container not found" + ) + + class Config: + schema_extra = { + "examples": [ + { + "msg_code": 0, + "description": "Database error occurred" + }, + { + "msg_code": 2, + "description": "CTF challenge not found" + }, + { + "msg_code": 6, + "description": "Container not found" + } + ] + } diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index a22324d..1e9d70b 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -10,6 +10,7 @@ from pwncore.config import config from pwncore.models import Container, Ports, Problem from pwncore.routes.auth import RequireJwt +from pwncore.routes.ctf.response_models import ContainerStartResponse, ContainerStopResponse, ErrorResponse router = APIRouter(tags=["ctf"]) logger = getLogger(__name__) @@ -17,9 +18,15 @@ @router.post("/{ctf_id}/start", summary="Start CTF challenge container", - description="""Start a new Docker container for the specified CTF challenge. + response_model=ContainerStartResponse, + responses={ + 404: {"model": ErrorResponse}, + 400: {"model": ContainerStartResponse}, + 500: {"model": ErrorResponse} + }, + response_description="""Start a new Docker container for the specified CTF challenge. - Example response (success): + Example response (container_start successful): ```json { "msg_code": 3, @@ -27,15 +34,13 @@ "ctf_id": 1 } ``` - - Error responses: - - 404: Challenge not found + Example response (fail - ctf_not_found) ```json { - "msg_code": 2 + "msg_code" : 2 } ``` - - 400: Container already running + Example response (fail - container_already_running): ```json { "msg_code": 7, @@ -43,20 +48,12 @@ "ctf_id": 1 } ``` - - 400: Container limit reached - ```json - { - "msg_code": 8 - } - ``` - - 500: Database error + Example response (failure due to exception - container_stop because of db_error): ```json { "msg_code": 0 } ``` - - Note: Requires JWT authentication token. """) async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): """ @@ -174,24 +171,19 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw @router.post("/stopall", summary="Stop all team containers", - description="""Stop and remove all Docker containers belonging to the authenticated team. + response_model=ContainerStopResponse, + responses={ + 500: {"model": ErrorResponse} + }, + response_description="""Stop and remove all Docker containers belonging to the authenticated team. - Example response (success): + Example response (containers_team_stop successful): ```json { "msg_code": 5 } ``` - - Error response: - - 500: Database error - ```json - { - "msg_code": 0 - } - ``` - - Note: Requires JWT authentication token. + Example response (failure due to exception - db_error) `{ "msg_code" : 0 }` """) async def stopall_docker_container(response: Response, jwt: RequireJwt): async with in_transaction(): @@ -219,36 +211,38 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): @router.post("/{ctf_id}/stop", summary="Stop specific CTF container", - description="""Stop and remove a specific CTF challenge container. + response_model=ContainerStopResponse, + responses={ + 404: {"model": ErrorResponse}, + 400: {"model": ErrorResponse}, + 500: {"model": ErrorResponse} + }, + response_description="""Stop and remove a specific CTF challenge container. - Example response (success): + Example response (container_stop successful): ```json { "msg_code": 4 } ``` - - Error responses: - - 404: Challenge not found + Example response (fail - ctf_not_found) ```json { "msg_code": 2 } ``` - - 400: Container not found + Example response (fail - container_not_found) ```json { "msg_code": 6 } ``` - - 500: Database error + Example response (failure due to exception - db_error) ```json { "msg_code": 0 } ``` - - Note: Requires JWT authentication token. """) async def stop_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): async with in_transaction(): diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index 3b2befa..f401bf8 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -4,7 +4,7 @@ from time import monotonic from fastapi import APIRouter, Request, Response - +from pydantic import BaseModel from tortoise.expressions import RawSQL, Q from pwncore.models import Team @@ -57,9 +57,14 @@ async def get_lb(self, req: Request): gcache = ExpiringLBCache(30.0) +# defining Pydantic response model +class LeaderboardEntry(BaseModel): + name: str + tpoints: int @router.get("", - description="""Returns the current CTF leaderboard sorted by total points. + response_model=list[LeaderboardEntry], + response_description=u"""Returns the current CTF leaderboard sorted by total points. Example response: ```json diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 7697230..692b73f 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -27,9 +27,17 @@ class UserRemoveBody(BaseModel): tag: str +class MessageResponse(BaseModel): + msg_code: int + +class ContainerPortsResponse(BaseModel): + __root__: dict[int, list[int]] + + @router.get("/list", summary="Get all teams", - description="""Returns a complete list of registered teams. + response_model=list[Team_Pydantic], + response_description="""Returns a complete list of registered teams. Example response: ```json @@ -51,7 +59,8 @@ async def team_list(): # Unable to test as adding users returns an error @router.get("/members", summary="Get team members", - description="""Returns a list of all members in the authenticated team. + response_model=list[User_Pydantic], + response_description="""Returns a list of all members in the authenticated team. Example response: ```json @@ -76,7 +85,8 @@ async def team_members(jwt: RequireJwt): @router.get("/me", summary="Get authenticated team details", - description="""Returns the details of the currently authenticated team. + response_model=Team_Pydantic, + response_description="""Returns the details of the currently authenticated team. Example response: ```json @@ -107,7 +117,8 @@ async def get_self_team(jwt: RequireJwt): @atomic() @router.post("/add", summary="Add member to team", - description="""Add a new member to the authenticated team. + response_model=MessageResponse, + response_description="""Add a new member to the authenticated team. Example request: ```json @@ -154,7 +165,8 @@ async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): @atomic() @router.post("/remove", summary="Remove member from team", - description="""Remove an existing member from the authenticated team. + response_model=MessageResponse, + response_description="""Remove an existing member from the authenticated team. Example request: ```json @@ -190,7 +202,8 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ @router.get("/containers", summary="Get team containers", - description="""Get all containers associated with the authenticated team. + response_model=ContainerPortsResponse, + response_description="""Get all containers associated with the authenticated team. Example response: ```json From 9177b67ef58f23e9556bcbeb3007f54151f7a96d Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Tue, 25 Feb 2025 22:11:47 +0530 Subject: [PATCH 03/13] chore: corrected position of @atomic() decorator --- src/pwncore/routes/ctf/pre_event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index 1b3d8ba..52b23d3 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -120,7 +120,7 @@ async def coins_get(tag: str): except DoesNotExist: return 0 - +@atomic() @router.post( "/{ctf_id}/flag", summary="Submit flag for pre-event CTF", @@ -164,7 +164,7 @@ async def coins_get(tag: str): } ``` """) -@atomic() + async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Response): problem = await PreEventProblem.get_or_none(id=ctf_id) From 6361a22dc126db69b01cc89d4866c6f63079e83f Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Tue, 25 Feb 2025 22:30:48 +0530 Subject: [PATCH 04/13] chore: sync fork with main repo --- src/pwncore/routes/admin_dashboard.py | 91 +++++++++++++++++++++++++++ tests/test_login.py | 7 +-- 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/pwncore/routes/admin_dashboard.py diff --git a/src/pwncore/routes/admin_dashboard.py b/src/pwncore/routes/admin_dashboard.py new file mode 100644 index 0000000..540c860 --- /dev/null +++ b/src/pwncore/routes/admin_dashboard.py @@ -0,0 +1,91 @@ +from fastapi import APIRouter, Response +from tortoise.transactions import in_transaction + +import pwncore.containerASD as containerASD +from pwncore.config import config +from pwncore.models import Team, Team_Pydantic, Problem, Container + +router = APIRouter(prefix="/admin/dashboard", tags=["admin-dashboard"]) + +@router.get("/teams", summary="List all teams", description="Returns a list of all teams stored in the database. *Markdown* supported.") +async def list_teams(): + ''' + This route fetches all teams via Team_Pydantic.from_queryset, + allowing admins to see each team's general details. + ''' + return await Team_Pydantic.from_queryset(Team.all()) + +@router.post("/toggle_problem/{problem_id}", + summary="Toggle problem visibility", + description="Allow admins to show/hide a problem. *Markdown* supported." +) +async def toggle_problem_visibility(problem_id: int, response: Response): + ''' + Toggles a problem\'s `visible` flag. If the problem + is currently visible, this will hide it, and vice versa. + ''' + async with in_transaction(): + prob = await Problem.get_or_none(id=problem_id) + if not prob: + response.status_code = 404 + return {"msg_code": config.msg_codes["ctf_not_found"]} + prob.visible = not prob.visible + await prob.save() + return {"visible": prob.visible} + +@router.post("/stop_container/{docker_id}") +async def stop_container(docker_id: str, response: Response): + ''' + Stops and removes a running container identified by its Docker ID, + deleting the container record from the database. + + Markdown is supported here as well. + ''' + async with in_transaction(): + container_rec = await Container.get_or_none(docker_id=docker_id) + if not container_rec: + response.status_code = 404 + return {"msg_code": "container_not_found"} + try: + container = await containerASD.docker_client.containers.get(docker_id) + await container.kill() + await container.delete() + await container_rec.delete() + except Exception: + response.status_code = 500 + return {"msg_code": config.msg_codes["db_error"]} + return {"status": "stopped"} + +@router.post("/adjust_coins/{team_id}") +async def adjust_team_coins(team_id: int, delta: int, response: Response): + ''' + Modifies a team's coin total by adding the given delta (can be negative or positive). + Returns the updated coin balance. + ''' + async with in_transaction(): + team = await Team.get_or_none(id=team_id) + if not team: + response.status_code = 404 + return {"msg_code": config.msg_codes["team_not_found"]} + team.coins += delta + await team.save() + return {"coins": team.coins} + +@router.get("/team_info/{team_id}") +async def get_team_details(team_id: int, response: Response): + ''' + Fetches detailed info about a specific team, including its name, + coins, points, and a list of members (by tag and name). + ''' + team = await Team.get_or_none(id=team_id).prefetch_related("members") + if not team: + response.status_code = 404 + return {"msg_code": config.msg_codes["team_not_found"]} + members = [{"tag": u.tag, "name": u.name} for u in team.members] # type: ignore + return { + "id": team.id, + "name": team.name, + "coins": team.coins, + "points": team.points, + "members": members, + } \ No newline at end of file diff --git a/tests/test_login.py b/tests/test_login.py index dfa6d9e..8218998 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,14 +1,13 @@ from fastapi.testclient import TestClient -from pwncore import app +from src.pwncore import app client = TestClient(app) - # Example test def test_login(): # Send a GET response to the specified endpoint response = client.get("/api/team/login") - + # Evaluate the response against expected values - assert response.status_code == 404 + assert response.status_code == 404 \ No newline at end of file From c8f4c420722577e0ae9eb593c49891d4dd23a5a8 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Fri, 28 Feb 2025 23:07:44 +0530 Subject: [PATCH 05/13] docs: shorten response_description i think its best not to remove response_description, u can understand what the endpoint does just from reading the source code also removed summary as requested --- src/pwncore/models/statusCode_models.py | 25 +++++ src/pwncore/routes/admin.py | 24 +---- src/pwncore/routes/admin_dashboard.py | 91 ----------------- src/pwncore/routes/auth.py | 92 ++++------------- src/pwncore/routes/ctf/__init__.py | 112 +++++---------------- src/pwncore/routes/ctf/pre_event.py | 117 ++++------------------ src/pwncore/routes/ctf/response_models.py | 63 ------------ src/pwncore/routes/ctf/start.py | 84 ++++------------ src/pwncore/routes/leaderboard.py | 20 +--- src/pwncore/routes/team.py | 108 +++++--------------- 10 files changed, 141 insertions(+), 595 deletions(-) create mode 100644 src/pwncore/models/statusCode_models.py delete mode 100644 src/pwncore/routes/admin_dashboard.py delete mode 100644 src/pwncore/routes/ctf/response_models.py diff --git a/src/pwncore/models/statusCode_models.py b/src/pwncore/models/statusCode_models.py new file mode 100644 index 0000000..330c1fc --- /dev/null +++ b/src/pwncore/models/statusCode_models.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from typing import List, Optional + +class ErrorResponse(BaseModel): + """ + Generic error response + msg_code: 0 (db_error), 2 (ctf_not_found) + """ + msg_code: int + +class ContainerStartResponse(BaseModel): + """ + Response for container start operation + msg_code: 3 (success), 7 (already running), 8 (limit reached), 0 (db_error) + """ + msg_code: int + ports: Optional[List[int]] = None + ctf_id: Optional[int] = None + +class ContainerStopResponse(BaseModel): + """ + Response for container stop operations + msg_code: 4 (stop success), 5 (stopall success), 6 (not found), 0 (db_error) + """ + msg_code: int diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index 886a32f..475db2b 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -58,20 +58,13 @@ class AdminResponse(BaseModel): success: bool message: Optional[str] = None - +# shorten response_description @atomic() @router.get("/union", - summary="Calculate and update team coins", response_model=AdminResponse, response_description="""Successful calculation of team points and coin updates. - Example response: - ```json - { - "success": true, - "message": "Team coins updated successfully" - } - ``` + Response parameters: boolean `success`, `message` Note: Returns 401 if authentication fails. """) @@ -106,20 +99,13 @@ async def calculate_team_coins( return AdminResponse(success=True, message="Team coins updated successfully") - +# shorten response_description @router.get("/create", - summary="Initialize database with sample data", response_model=AdminResponse, response_description="""Database initialization with sample data. - Example response: - ```json - { - "success": true, - "message": "Database initialized with sample data" - } - ``` - + Response parameters: boolean `success`, `message` + Note: Returns 401 if authentication fails. This endpoint should only be used in development environment. """) diff --git a/src/pwncore/routes/admin_dashboard.py b/src/pwncore/routes/admin_dashboard.py deleted file mode 100644 index 540c860..0000000 --- a/src/pwncore/routes/admin_dashboard.py +++ /dev/null @@ -1,91 +0,0 @@ -from fastapi import APIRouter, Response -from tortoise.transactions import in_transaction - -import pwncore.containerASD as containerASD -from pwncore.config import config -from pwncore.models import Team, Team_Pydantic, Problem, Container - -router = APIRouter(prefix="/admin/dashboard", tags=["admin-dashboard"]) - -@router.get("/teams", summary="List all teams", description="Returns a list of all teams stored in the database. *Markdown* supported.") -async def list_teams(): - ''' - This route fetches all teams via Team_Pydantic.from_queryset, - allowing admins to see each team's general details. - ''' - return await Team_Pydantic.from_queryset(Team.all()) - -@router.post("/toggle_problem/{problem_id}", - summary="Toggle problem visibility", - description="Allow admins to show/hide a problem. *Markdown* supported." -) -async def toggle_problem_visibility(problem_id: int, response: Response): - ''' - Toggles a problem\'s `visible` flag. If the problem - is currently visible, this will hide it, and vice versa. - ''' - async with in_transaction(): - prob = await Problem.get_or_none(id=problem_id) - if not prob: - response.status_code = 404 - return {"msg_code": config.msg_codes["ctf_not_found"]} - prob.visible = not prob.visible - await prob.save() - return {"visible": prob.visible} - -@router.post("/stop_container/{docker_id}") -async def stop_container(docker_id: str, response: Response): - ''' - Stops and removes a running container identified by its Docker ID, - deleting the container record from the database. - - Markdown is supported here as well. - ''' - async with in_transaction(): - container_rec = await Container.get_or_none(docker_id=docker_id) - if not container_rec: - response.status_code = 404 - return {"msg_code": "container_not_found"} - try: - container = await containerASD.docker_client.containers.get(docker_id) - await container.kill() - await container.delete() - await container_rec.delete() - except Exception: - response.status_code = 500 - return {"msg_code": config.msg_codes["db_error"]} - return {"status": "stopped"} - -@router.post("/adjust_coins/{team_id}") -async def adjust_team_coins(team_id: int, delta: int, response: Response): - ''' - Modifies a team's coin total by adding the given delta (can be negative or positive). - Returns the updated coin balance. - ''' - async with in_transaction(): - team = await Team.get_or_none(id=team_id) - if not team: - response.status_code = 404 - return {"msg_code": config.msg_codes["team_not_found"]} - team.coins += delta - await team.save() - return {"coins": team.coins} - -@router.get("/team_info/{team_id}") -async def get_team_details(team_id: int, response: Response): - ''' - Fetches detailed info about a specific team, including its name, - coins, points, and a list of members (by tag and name). - ''' - team = await Team.get_or_none(id=team_id).prefetch_related("members") - if not team: - response.status_code = 404 - return {"msg_code": config.msg_codes["team_not_found"]} - members = [{"tag": u.tag, "name": u.name} for u in team.members] # type: ignore - return { - "id": team.id, - "name": team.name, - "coins": team.coins, - "points": team.points, - "members": members, - } \ No newline at end of file diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index 6c43e93..eba1344 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -78,7 +78,7 @@ class ErrorResponse(BaseModel): def normalise_tag(tag: str): return tag.strip().casefold() - +# shorten response_description @atomic() @router.post("/signup", response_model=SignupResponse, @@ -90,48 +90,16 @@ def normalise_tag(tag: str): }, response_description="""Create a new team with associated members. - Request body example: - ```json - { - "name": "TeamAwesome", - "password": "securepassword123", - "tags": ["user1", "user2", "user3"] - } - ``` - - Responses: - - 200: Successful signup - ```json - { - "msg_code": 13 - } - ``` - - 406: Team already exists - ```json - { - "msg_code": 17 - } - ``` - - 404: Users not found - ```json - { - "msg_code": 24, - "tags": ["user2", "user3"] - } - ``` - - 401: Users already in team - ```json - { - "msg_code": 20, - "tags": ["user1"] - } - ``` - - 500: Database error - ```json - { - "msg_code": 0 - } - ``` + Parameters: + - in request: `name`, `password`, `tags` + - in response: `msg_code`, `tags` _only in error responses_ + + msg_codes for Responses: + - 200: Successful signup : 13 + - 406: Team already exists: 17 + - 404: Users not found: 24 + - 401: Users already in team: 20 + - 500: Database error: 0 """) async def signup_team(team: SignupBody, response: Response): team.name = team.name.strip() @@ -175,7 +143,7 @@ async def signup_team(team: SignupBody, response: Response): return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["signup_success"]} - +# shorten response_description @router.post("/login", response_model=LoginResponse, responses={ @@ -184,35 +152,15 @@ async def signup_team(team: SignupBody, response: Response): }, response_description="""Authenticate a team and receive a JWT token. - Request body example: - ```json - { - "name": "TeamAwesome", - "password": "securepassword123" - } - ``` + Parameters: + - in request: `name`, `password` + - in successful response: `msg_code`, `access_token`,`token_type` + - in error responses: `msg_code` - Responses: - - 200: Successful login - ```json - { - "msg_code": 15, - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "token_type": "bearer" - } - ``` - - 404: Team not found - ```json - { - "msg_code": 10 - } - ``` - - 401: Wrong password - ```json - { - "msg_code": 14 - } - ``` + msg_codes for Responses: + - 200: Successful login: 15 + - 404: Team not found: 10 + - 401: Wrong password: 14 """) async def team_login(team_data: AuthBody, response: Response): # TODO: Simplified logic since we're not doing refresh tokens. diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 2b9b5ee..f250ff8 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -52,26 +52,13 @@ def _invalid_order(): class Flag(BaseModel): flag: str - +# shorten response_description @router.get( "/completed", - summary="Get completed problems", response_model=list[Problem_Pydantic], response_description="""Returns all problems solved by the authenticated team. - Example response: - ```json - [ - { - "id": 1, - "name": "Web 101", - "description": "Basic web exploitation", - "points": 100, - "category": "web", - "visible": true - } - ] - ``` + Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` """) async def completed_problem_get(jwt: RequireJwt): team_id = jwt["team_id"] @@ -81,26 +68,13 @@ async def completed_problem_get(jwt: RequireJwt): ) return problems - +# shorten response_description @router.get( "/list", - summary="List all CTF problems", response_model=list[Problem_Pydantic], response_description="""Returns all visible CTF problems with adjusted points based on hints used. - Example response: - ```json - [ - { - "id": 1, - "name": "Crypto 101", - "description": "Basic cryptography challenge", - "points": 90, - "category": "crypto", - "visible": true - } - ] - ``` + Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` Note: Points are adjusted based on hints viewed by the team. """) @@ -128,33 +102,22 @@ async def update_points(req: Request, ctf_id: int): except Exception: logger.exception("An error occured while updating points") - +# shorten response_description @atomic() @router.post( "/{ctf_id}/flag", - summary="Submit flag for problem", response_model=dict[str, bool | str], response_description="""Submit a flag for a specific CTF problem. - Example request: - ```json - { - "flag": "flag{th1s_1s_4_fl4g}" - } - ``` + Parameters: + - in request: string `flag` + - in response: boolean `status` - Example response: - ```json - { - "status": true - } - ``` - - Error responses: - - 404: {"msg_code": 2} - ctf_not_found - - 401: {"msg_code": 12} - ctf_solved - - 500: {"msg_code": 0} - db_error - - 400: {"msg_code": 6} - container_not_found + msg_codes for Error responses: + - 404: ctf_not_found : 2 + - 401: ctf_solved : 12 + - 500: db_error : 0 + - 400: container_not_found : 6 """) async def flag_post( req: Request, ctf_id: int, flag: Flag, response: Response, jwt: RequireJwt @@ -201,23 +164,16 @@ async def flag_post( return {"status": True} return {"status": False} - +# shorten response_description @atomic() @router.get( "/{ctf_id}/hint", - summary="Get next available hint", response_model=dict[str, str | int], response_description="""Retrieve the next available hint for a problem. - Example response: - ```json - { - "text": "Look at the HTTP headers", - "order": 1 - } - ``` - - Error responses: + Response Parameters: `text`, `order` + + msg_code for Error responses: - 404: {"msg_code": 2} - ctf_not_found - 403: {"msg_code": 9} - hint_limit_reached - 400: {"msg_code": 22} - Insufficient coins @@ -259,24 +215,13 @@ async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): "order": hint.order, } - +# shorten response_description @router.get( "/{ctf_id}/viewed_hints", - summary="Get viewed hints", response_model=list[Hint_Pydantic], response_description="""Get all hints viewed by the team for a specific problem. - Example response: - ```json - [ - { - "id": 1, - "text": "First hint text", - "order": 0, - "problem_id": 1 - } - ] - ``` + Response Parameters: `id`, `text`, `order`, `problem_id` """) async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): team_id = jwt["team_id"] @@ -285,27 +230,16 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): ) return viewed_hints - +# shorten response_description @router.get( "/{ctf_id}", - summary="Get problem details", response_model=Problem_Pydantic, response_description="""Get details of a specific CTF problem. - Example response: - ```json - { - "id": 1, - "name": "Binary 101", - "description": "Basic binary exploitation", - "points": 150, - "category": "pwn", - "visible": true - } - ``` + Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` - Error responses: - - 404: {"msg_code": 2} - ctf_not_found or not visible + msg_code: + - if ctf_not_found or not visible : 2 """) async def ctf_get(ctf_id: int, response: Response): problem = await Problem_Pydantic.from_queryset( diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index 52b23d3..1c084f0 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -41,25 +41,13 @@ class FlagSubmissionResponse(BaseModel): class ErrorResponse(BaseModel): msg_code: int - +# shorten response_description @router.get( "/list", - summary="Get all pre-event CTF problems", response_model=list[PreEventProblem_Pydantic], response_description="""Returns a list of all available pre-event CTF problems. - - Example response: - ```json - [ - { - "id": 1, - "name": "Web Basic", - "description": "Find the flag in website", - "points": 100, - "date": "2024-01-15" - } - ] - ``` + + Parameters = (5): `id`,`name`,`description`,`points`,`date` Note: Flag field is excluded from response for security. """) @@ -67,25 +55,13 @@ async def ctf_list(): problems = await PreEventProblem_Pydantic.from_queryset(PreEventProblem.all()) return problems - +# shorten response_description @router.get( "/today", - summary="Get today's pre-event CTF problems", response_model=list[PreEventProblem_Pydantic], response_description="""Returns list of CTF problems scheduled for current date. - Example response: - ```json - [ - { - "id": 1, - "name": "Web Basic", - "description": "Find the flag in website", - "points": 100, - "date": "2024-01-15" - } - ] - ``` + Parameters = (5): `id`,`name`,`description`,`points`,`date` Note: Returns empty list if no problems are scheduled for today. """) @@ -94,21 +70,13 @@ async def ctf_today(): PreEventProblem().filter(date=datetime.now(_IST).date()) ) - +# shorten response_description @router.get( "/coins/{tag}", - summary="Get user's total coins", response_model=CoinsResponse, response_description="""Get total coins earned by a user in pre-event CTFs. - Example response: - ```json - { - "coins": 300 - } - ``` - - Note: Returns 0 if user not found (msg_code: 11). + Note: Returns msg_code : 11 if user_not_found. """) async def coins_get(tag: str): try: @@ -120,49 +88,21 @@ async def coins_get(tag: str): except DoesNotExist: return 0 +# shorten response_description @atomic() @router.post( "/{ctf_id}/flag", - summary="Submit flag for pre-event CTF", response_model=Union[FlagSubmissionResponse, ErrorResponse], response_description="""Submit a solution flag for a pre-event CTF problem. - Example request: - ```json - { - "tag": "23BCE1001", - "flag": "flag{solution}", - "email": "user@example.com" - } - ``` - - Success response: - ```json - { - "status": true, - "coins": 300 - } - ``` - - Error responses: - - 404: if ctf_not_found or not for current date - ```json - { - "msg_code": 2 - } - ``` - - 401: if ctf_solved already - ```json - { - "msg_code": 12 - } - ``` - - 401: user_or_email_exists - ```json - { - "msg_code": 23 - } - ``` + Request parameters: `tag`, `flag`,`email` + Parameters in response: `status`, `coins` + Note: status may be true or false + + Msg_codes for Error responses: + - 404: if ctf_not_found or not for current date: 2 + - 401: if ctf_solved already: 12 + - 401: (exception) if user_or_email_exists : 23 """) async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Response): @@ -203,31 +143,16 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re return {"status": status, "coins": coins} - +# shorten response_description @router.get( "/{ctf_id}", - summary="Get specific CTF problem details", response_model=Union[list[PreEventProblem_Pydantic], ErrorResponse], response_description="""Get complete details of a specific pre-event CTF problem. - Example response: - ```json - { - "id": 1, - "name": "Web Basic", - "description": "Find the flag in website", - "points": 100, - "date": "2024-01-15" - } - ``` - - Error response: - - 404: ctf_not_found - ```json - { - "msg_code": 2 - } - ``` + Response Parameters = (5): `id`,`name`,`description`,`points`,`date` + + msg_code for Error response: + - if ctf_not_found : 2 """) async def ctf_get(ctf_id: int, response: Response): problem = await PreEventProblem_Pydantic.from_queryset( diff --git a/src/pwncore/routes/ctf/response_models.py b/src/pwncore/routes/ctf/response_models.py deleted file mode 100644 index b98ef9f..0000000 --- a/src/pwncore/routes/ctf/response_models.py +++ /dev/null @@ -1,63 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Literal - -class ContainerStartResponse(BaseModel): - """Pydantic Response model for container start operation - - Message codes: - - 3: Container started successfully - - 7: Container already running - - 8: Container limit reached - """ - msg_code: Literal[3, 7, 8] = Field( - description="Status code indicating operation result: 3=success, 7=already running, 8=limit reached" - ) - ports: List[int] = Field( - default=None, - description="List of mapped container ports" - ) - ctf_id: int = Field( - default=None, - description="ID of the CTF challenge" - ) - -class ContainerStopResponse(BaseModel): - """Pydantic Response model for container stop operations - - Message codes: - - 4: Container stopped successfully - - 5: All team containers stopped successfully - """ - msg_code: Literal[4, 5] = Field( - description="Status code indicating operation result: 4=single stop, 5=stop all" - ) - -class ErrorResponse(BaseModel): - """Pydantic Error response model - - Message codes: - - 0: Database error - - 2: CTF not found - - 6: Container not found - """ - msg_code: Literal[0, 2, 6] = Field( - description="Error code: 0=DB error, 2=CTF not found, 6=container not found" - ) - - class Config: - schema_extra = { - "examples": [ - { - "msg_code": 0, - "description": "Database error occurred" - }, - { - "msg_code": 2, - "description": "CTF challenge not found" - }, - { - "msg_code": 6, - "description": "Container not found" - } - ] - } diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 1e9d70b..b14788e 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -10,14 +10,13 @@ from pwncore.config import config from pwncore.models import Container, Ports, Problem from pwncore.routes.auth import RequireJwt -from pwncore.routes.ctf.response_models import ContainerStartResponse, ContainerStopResponse, ErrorResponse +from pwncore.models.statusCode_models import ContainerStartResponse, ContainerStopResponse, ErrorResponse router = APIRouter(tags=["ctf"]) logger = getLogger(__name__) - +# shorten response_description @router.post("/{ctf_id}/start", - summary="Start CTF challenge container", response_model=ContainerStartResponse, responses={ 404: {"model": ErrorResponse}, @@ -26,34 +25,11 @@ }, response_description="""Start a new Docker container for the specified CTF challenge. - Example response (container_start successful): - ```json - { - "msg_code": 3, - "ports": [8080, 22], - "ctf_id": 1 - } - ``` - Example response (fail - ctf_not_found) - ```json - { - "msg_code" : 2 - } - ``` - Example response (fail - container_already_running): - ```json - { - "msg_code": 7, - "ports": [8080, 22], - "ctf_id": 1 - } - ``` - Example response (failure due to exception - container_stop because of db_error): - ```json - { - "msg_code": 0 - } - ``` + msg_codes: + - (success) container_start : 3 + - (fail) ctf_not_found : 2 + - (fail) container_already_running : 7 + - (fail) db_error : 0 """) async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): """ @@ -168,22 +144,17 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw "ctf_id": ctf_id, } - +# shorten response_description @router.post("/stopall", - summary="Stop all team containers", response_model=ContainerStopResponse, responses={ 500: {"model": ErrorResponse} }, response_description="""Stop and remove all Docker containers belonging to the authenticated team. - Example response (containers_team_stop successful): - ```json - { - "msg_code": 5 - } - ``` - Example response (failure due to exception - db_error) `{ "msg_code" : 0 }` + msg_codes: + - (success) containers_team_stop : 5 + - (fail) db_error : 0 """) async def stopall_docker_container(response: Response, jwt: RequireJwt): async with in_transaction(): @@ -209,8 +180,8 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): return {"msg_code": config.msg_codes["containers_team_stop"]} +# shorten response_description @router.post("/{ctf_id}/stop", - summary="Stop specific CTF container", response_model=ContainerStopResponse, responses={ 404: {"model": ErrorResponse}, @@ -219,30 +190,13 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): }, response_description="""Stop and remove a specific CTF challenge container. - Example response (container_stop successful): - ```json - { - "msg_code": 4 - } - ``` - Example response (fail - ctf_not_found) - ```json - { - "msg_code": 2 - } - ``` - Example response (fail - container_not_found) - ```json - { - "msg_code": 6 - } - ``` - Example response (failure due to exception - db_error) - ```json - { - "msg_code": 0 - } - ``` + msg_codes: + - (success) container_stop : 4 + - (fail) + - ctf_not_found : 2 + - container_not_found : 6 + - db_error : 0 + """) async def stop_docker_container(ctf_id: int, response: Response, jwt: RequireJwt): async with in_transaction(): diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index f401bf8..0c3d7f0 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -14,7 +14,6 @@ router = APIRouter(prefix="/leaderboard", tags=["leaderboard"]) - class ExpiringLBCache: period: float last_update: float @@ -62,27 +61,12 @@ class LeaderboardEntry(BaseModel): name: str tpoints: int +# shorten response_description @router.get("", response_model=list[LeaderboardEntry], response_description=u"""Returns the current CTF leaderboard sorted by total points. - Example response: - ```json - [ - { - "name": "Team Alpha", - "tpoints": 450 - }, - { - "name": "Team Beta", - "tpoints": 300 - }, - { - "name": "Team Gamma", - "tpoints": 150 - } - ] - ``` + Response Parameters: `name`, `tpoints` Notes: - tpoints = sum of (problem points × penalty multiplier) + team points diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 692b73f..564bf5a 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -33,23 +33,13 @@ class MessageResponse(BaseModel): class ContainerPortsResponse(BaseModel): __root__: dict[int, list[int]] - +# shorten response_description @router.get("/list", - summary="Get all teams", response_model=list[Team_Pydantic], response_description="""Returns a complete list of registered teams. - Example response: - ```json - [ - { - "id": 1, - "name": "Team Alpha", - "coins": 100, - "points": 250 - } - ] - ``` + Response Parameters: `id`, `name`, `coins`, `points` + """) async def team_list(): teams = await Team_Pydantic.from_queryset(Team.all()) @@ -57,22 +47,12 @@ async def team_list(): # Unable to test as adding users returns an error +# shorten response_description @router.get("/members", - summary="Get team members", response_model=list[User_Pydantic], response_description="""Returns a list of all members in the authenticated team. - Example response: - ```json - [ - { - "tag": "23BCE1000", - "name": "John Doe", - "email": "john@example.com", - "phone_num": "1234567890" - } - ] - ``` + Response Parameters: `tag`, `name`, `email`, `phone_num` Note: Returns an empty list if no members are found. """) @@ -82,21 +62,13 @@ async def team_members(jwt: RequireJwt): # Incase of no members, it just returns an empty list. return members - +# shorten response_description @router.get("/me", - summary="Get authenticated team details", response_model=Team_Pydantic, response_description="""Returns the details of the currently authenticated team. - Example response: - ```json - { - "id": 1, - "name": "Team Alpha", - "coins": 100, - "points": 250 - } - ``` + Response Parameters: `id`, `name`, `coins`, `points` + """) async def get_self_team(jwt: RequireJwt): team_id = jwt["team_id"] @@ -113,31 +85,19 @@ async def get_self_team(jwt: RequireJwt): return team - +# shorten response_description @atomic() @router.post("/add", - summary="Add member to team", response_model=MessageResponse, response_description="""Add a new member to the authenticated team. - Example request: - ```json - { - "tag": "23BCE1000", - "name": "John Doe", - "email": "john@example.com", - "phone_num": "1234567890" - } - ``` + Request Parameters: `tag`, `name`, `email`, `phone_num` - Example response: - ```json - { - "msg_code": 18 - } - ``` - - Note: Returns error 403 if user already exists in a team. + msg_code for response: + - (success) user_added: 18 + - (fail) + - user_already_in_team: 20 + - db_error: 0 """) async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] @@ -161,28 +121,21 @@ async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["user_added"]} - +# shorten response_description @atomic() @router.post("/remove", - summary="Remove member from team", response_model=MessageResponse, response_description="""Remove an existing member from the authenticated team. - Example request: - ```json - { - "tag": "23BCE1000" - } - ``` - - Example response: - ```json - { - "msg_code": 19 - } - ``` - - Note: Returns error 403 if user is not found in team. + Parameters: + - for request: `tag` + - for response: `msg_code` + + Msg_code for response: + - (success) user_removed: 19 + - (fail) + - user_not_in_team : 21 + - db_error: 0 """) async def remove_member(user_info: UserRemoveBody, response: Response, jwt: RequireJwt): team_id = jwt["team_id"] @@ -201,19 +154,10 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ @router.get("/containers", - summary="Get team containers", response_model=ContainerPortsResponse, response_description="""Get all containers associated with the authenticated team. - Example response: - ```json - { - "1": [8080, 22], - "2": [8081] - } - ``` - - Note: Object keys are problem IDs and values are lists of exposed ports. + Note: Object keys are problem IDs and values are lists of the containers' exposed ports. """) async def get_team_containers(response: Response, jwt: RequireJwt): containers = await Container.filter(team_id=jwt["team_id"]).prefetch_related( From 5144e353c82e4c42815f4df53782f7a712150955 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Fri, 28 Feb 2025 23:19:44 +0530 Subject: [PATCH 06/13] changed from __root__ to ports in pwncore.routes.team.ContainerPortsResponse --- src/pwncore/routes/team.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 564bf5a..889dda6 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -31,7 +31,7 @@ class MessageResponse(BaseModel): msg_code: int class ContainerPortsResponse(BaseModel): - __root__: dict[int, list[int]] + ports: dict[int, list[int]] # shorten response_description @router.get("/list", @@ -152,7 +152,7 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["user_removed"]} - +# shorten response_description @router.get("/containers", response_model=ContainerPortsResponse, response_description="""Get all containers associated with the authenticated team. From fb31efcf42610e3fca22eeaef98bf8a292ed86e9 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sat, 8 Mar 2025 19:47:14 +0530 Subject: [PATCH 07/13] docs: moved all Pydantic response models to one location in `pwncore/models/responseModels` --- .../ctf_ContainerStatusResponse.py | 32 ++++++++++ .../responseModels/leaderboard_response.py | 11 ++++ .../responseModels/preEventCTF_response.py | 40 ++++++++++++ .../models/responseModels/teamAuthResponse.py | 62 +++++++++++++++++++ .../responseModels/user_mgmtResponse.py | 31 ++++++++++ 5 files changed, 176 insertions(+) create mode 100644 src/pwncore/models/responseModels/ctf_ContainerStatusResponse.py create mode 100644 src/pwncore/models/responseModels/leaderboard_response.py create mode 100644 src/pwncore/models/responseModels/preEventCTF_response.py create mode 100644 src/pwncore/models/responseModels/teamAuthResponse.py create mode 100644 src/pwncore/models/responseModels/user_mgmtResponse.py diff --git a/src/pwncore/models/responseModels/ctf_ContainerStatusResponse.py b/src/pwncore/models/responseModels/ctf_ContainerStatusResponse.py new file mode 100644 index 0000000..a3e3b7c --- /dev/null +++ b/src/pwncore/models/responseModels/ctf_ContainerStatusResponse.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel +from typing import List, Optional + +class CTF_ErrorResponse(BaseModel): + """ + Generic error response + msg_code: 0 (db_error), 2 (ctf_not_found) + """ + msg_code: int + +class ContainerStartResponse(BaseModel): + """ + Response for container start operation + msg_code: 3 (success), 7 (already running), 8 (limit reached), 0 (db_error) + """ + msg_code: int + ports: Optional[List[int]] = None + ctf_id: Optional[int] = None + +class ContainerStopResponse(BaseModel): + """ + Response for container stop operations + msg_code: 4 (stop success), 5 (stopall success), 6 (not found), 0 (db_error) + """ + msg_code: int + +class ContainerPortsResponse(BaseModel): + """ + Response for all open ports of containers + ports: array of port numbers + """ + ports: dict[int, list[int]] \ No newline at end of file diff --git a/src/pwncore/models/responseModels/leaderboard_response.py b/src/pwncore/models/responseModels/leaderboard_response.py new file mode 100644 index 0000000..fca45dd --- /dev/null +++ b/src/pwncore/models/responseModels/leaderboard_response.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +# defining Pydantic response model +class LeaderboardEntry(BaseModel): + """ + Returns the leaderboard entry for a team. + name: team name + tpoints: total points + """ + name: str + tpoints: int \ No newline at end of file diff --git a/src/pwncore/models/responseModels/preEventCTF_response.py b/src/pwncore/models/responseModels/preEventCTF_response.py new file mode 100644 index 0000000..6506291 --- /dev/null +++ b/src/pwncore/models/responseModels/preEventCTF_response.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel + +# pydantic response models +class PreEventFlag(BaseModel): + """ + Response for pre-event flag submission + tag: team tag + flag: flag submitted + email: email of the team + """ + tag: str + flag: str + email: str + + +class CoinsQuery(BaseModel): + """ + Response for pre-event coins query + tag: team tag + """ + tag: str + +class CoinsResponse(BaseModel): + """ + Response for pre-event coins query + coins: total coins earned by the team in pre-event CTFs + """ + coins: int + +class FlagSubmissionResponse(BaseModel): + """ + Response for pre-event flag submission + status: bool + coins: total coins earned by the team in pre-event CTFs + """ + status: bool + coins: int + +class preEventCTF_ErrorResponse(BaseModel): + msg_code: int diff --git a/src/pwncore/models/responseModels/teamAuthResponse.py b/src/pwncore/models/responseModels/teamAuthResponse.py new file mode 100644 index 0000000..926c712 --- /dev/null +++ b/src/pwncore/models/responseModels/teamAuthResponse.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel +import typing as t + +# defining Pydantic response models +class AuthBody(BaseModel): + """ + Request body for login + name: name of the user + password: password of the user + """ + name: str + password: str + + +class SignupBody(BaseModel): + name: str + password: str + tags: set[str] + + +# Response Models +class SignupResponse(BaseModel): + """ + msg_code: 13 (signup_success) + """ + msg_code: t.Literal[13] + +class SignupErrorUsersNotFound(BaseModel): + """ + msg_code: 24 (users_not_found) + tags: list[str] + """ + msg_code: t.Literal[24] + tags: list[str] + +class SignupErrorUsersInTeam(BaseModel): + """ + msg_code: 20 (user_already_in_team) + tags: list[str] + """ + msg_code: t.Literal[20] + tags: list[str] + +class LoginResponse(BaseModel): + """ + msg_code: 15 (login_success) + access_token: JWT access token + token_type: "bearer" + """ + msg_code: t.Literal[15] + access_token: str + token_type: str + +class Auth_ErrorResponse(BaseModel): + """ + msg_code can be: + 0 (db_error) + 17 (team_exists) + 10 (team_not_found) + 14 (wrong_password) + """ + msg_code: t.Literal[0, 17, 10, 14] diff --git a/src/pwncore/models/responseModels/user_mgmtResponse.py b/src/pwncore/models/responseModels/user_mgmtResponse.py new file mode 100644 index 0000000..9a22854 --- /dev/null +++ b/src/pwncore/models/responseModels/user_mgmtResponse.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +import typing as t + +class UserAddBody(BaseModel): + """ + Request body for adding a user + tag: team tag + name: name of the user + email: email of the user + phone_num: phone number of the user + """ + tag: str + name: str + email: str + phone_num: str + + +class UserRemoveBody(BaseModel): + """ + Request body for removing a user + tag: team tag + """ + tag: str + + +class MessageResponse(BaseModel): + """ + Response for user management operations + msg_code: message code + """ + msg_code: int \ No newline at end of file From 51a02d487fe28c76d95ab70fb857b832044f9ccc Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sat, 8 Mar 2025 19:51:33 +0530 Subject: [PATCH 08/13] chore: remove the unwanted comment --- src/pwncore/models/statusCode_models.py | 25 ----------- src/pwncore/routes/admin.py | 4 +- src/pwncore/routes/auth.py | 56 ++----------------------- src/pwncore/routes/ctf/__init__.py | 12 +++--- src/pwncore/routes/ctf/pre_event.py | 38 ++++++----------- src/pwncore/routes/ctf/start.py | 22 +++++----- src/pwncore/routes/leaderboard.py | 9 ++-- src/pwncore/routes/team.py | 37 ++++++---------- 8 files changed, 52 insertions(+), 151 deletions(-) delete mode 100644 src/pwncore/models/statusCode_models.py diff --git a/src/pwncore/models/statusCode_models.py b/src/pwncore/models/statusCode_models.py deleted file mode 100644 index 330c1fc..0000000 --- a/src/pwncore/models/statusCode_models.py +++ /dev/null @@ -1,25 +0,0 @@ -from pydantic import BaseModel -from typing import List, Optional - -class ErrorResponse(BaseModel): - """ - Generic error response - msg_code: 0 (db_error), 2 (ctf_not_found) - """ - msg_code: int - -class ContainerStartResponse(BaseModel): - """ - Response for container start operation - msg_code: 3 (success), 7 (already running), 8 (limit reached), 0 (db_error) - """ - msg_code: int - ports: Optional[List[int]] = None - ctf_id: Optional[int] = None - -class ContainerStopResponse(BaseModel): - """ - Response for container stop operations - msg_code: 4 (stop success), 5 (stopall success), 6 (not found), 0 (db_error) - """ - msg_code: int diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index 475db2b..b2692c4 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -58,7 +58,7 @@ class AdminResponse(BaseModel): success: bool message: Optional[str] = None -# shorten response_description + @atomic() @router.get("/union", response_model=AdminResponse, @@ -99,7 +99,7 @@ async def calculate_team_coins( return AdminResponse(success=True, message="Team coins updated successfully") -# shorten response_description + @router.get("/create", response_model=AdminResponse, response_description="""Database initialization with sample data. diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index eba1344..f182322 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -5,11 +5,13 @@ from logging import getLogger import jwt + from fastapi import APIRouter, Depends, Header, HTTPException, Response from passlib.hash import bcrypt from pydantic import BaseModel from tortoise.transactions import atomic +from pwncore.models.responseModels.teamAuthResponse import AuthBody, SignupBody, SignupResponse, SignupErrorUsersNotFound, SignupErrorUsersInTeam, LoginResponse, Auth_ErrorResponse as ErrorResponse from pwncore.config import config from pwncore.models import Team, User @@ -23,62 +25,12 @@ logger = getLogger(__name__) -# defining Pydantic response models -class AuthBody(BaseModel): - name: str - password: str - - -class SignupBody(BaseModel): - name: str - password: str - tags: set[str] - - -# Response Models -class SignupResponse(BaseModel): - """ - msg_code: 13 (signup_success) - """ - msg_code: t.Literal[13] - -class SignupErrorUsersNotFound(BaseModel): - """ - msg_code: 24 (users_not_found) - """ - msg_code: t.Literal[24] - tags: list[str] - -class SignupErrorUsersInTeam(BaseModel): - """ - msg_code: 20 (user_already_in_team) - """ - msg_code: t.Literal[20] - tags: list[str] - -class LoginResponse(BaseModel): - """ - msg_code: 15 (login_success) - """ - msg_code: t.Literal[15] - access_token: str - token_type: str - -class ErrorResponse(BaseModel): - """ - msg_code can be: - 0 (db_error) - 17 (team_exists) - 10 (team_not_found) - 14 (wrong_password) - """ - msg_code: t.Literal[0, 17, 10, 14] def normalise_tag(tag: str): return tag.strip().casefold() -# shorten response_description + @atomic() @router.post("/signup", response_model=SignupResponse, @@ -143,7 +95,7 @@ async def signup_team(team: SignupBody, response: Response): return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["signup_success"]} -# shorten response_description + @router.post("/login", response_model=LoginResponse, responses={ diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index f250ff8..15ec076 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -52,7 +52,7 @@ def _invalid_order(): class Flag(BaseModel): flag: str -# shorten response_description + @router.get( "/completed", response_model=list[Problem_Pydantic], @@ -68,7 +68,7 @@ async def completed_problem_get(jwt: RequireJwt): ) return problems -# shorten response_description + @router.get( "/list", response_model=list[Problem_Pydantic], @@ -102,7 +102,7 @@ async def update_points(req: Request, ctf_id: int): except Exception: logger.exception("An error occured while updating points") -# shorten response_description + @atomic() @router.post( "/{ctf_id}/flag", @@ -164,7 +164,7 @@ async def flag_post( return {"status": True} return {"status": False} -# shorten response_description + @atomic() @router.get( "/{ctf_id}/hint", @@ -215,7 +215,7 @@ async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): "order": hint.order, } -# shorten response_description + @router.get( "/{ctf_id}/viewed_hints", response_model=list[Hint_Pydantic], @@ -230,7 +230,7 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): ) return viewed_hints -# shorten response_description + @router.get( "/{ctf_id}", response_model=Problem_Pydantic, diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index 1c084f0..e3d5619 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -5,7 +5,6 @@ from typing import Union from fastapi import APIRouter, Response -from pydantic import BaseModel from tortoise.transactions import atomic from tortoise.functions import Sum from tortoise.exceptions import DoesNotExist, IntegrityError @@ -18,30 +17,19 @@ ) from pwncore.config import config +from pwncore.models.responseModels.preEventCTF_response import ( + CoinsResponse, + FlagSubmissionResponse, + preEventCTF_ErrorResponse as ErrorResponse, + PreEventFlag, + CoinsQuery +) + router = APIRouter(prefix="/pre", tags=["ctf"]) _IST = timezone(timedelta(hours=5, minutes=30)) -# pydantic response models -class PreEventFlag(BaseModel): - tag: str - flag: str - email: str - - -class CoinsQuery(BaseModel): - tag: str - -class CoinsResponse(BaseModel): - coins: int - -class FlagSubmissionResponse(BaseModel): - status: bool - coins: int - -class ErrorResponse(BaseModel): - msg_code: int -# shorten response_description + @router.get( "/list", response_model=list[PreEventProblem_Pydantic], @@ -55,7 +43,7 @@ async def ctf_list(): problems = await PreEventProblem_Pydantic.from_queryset(PreEventProblem.all()) return problems -# shorten response_description + @router.get( "/today", response_model=list[PreEventProblem_Pydantic], @@ -70,7 +58,7 @@ async def ctf_today(): PreEventProblem().filter(date=datetime.now(_IST).date()) ) -# shorten response_description + @router.get( "/coins/{tag}", response_model=CoinsResponse, @@ -88,7 +76,7 @@ async def coins_get(tag: str): except DoesNotExist: return 0 -# shorten response_description + @atomic() @router.post( "/{ctf_id}/flag", @@ -143,7 +131,7 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re return {"status": status, "coins": coins} -# shorten response_description + @router.get( "/{ctf_id}", response_model=Union[list[PreEventProblem_Pydantic], ErrorResponse], diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index b14788e..d979dfa 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -5,23 +5,23 @@ from fastapi import APIRouter, Response from tortoise.transactions import in_transaction - import pwncore.containerASD as containerASD + from pwncore.config import config from pwncore.models import Container, Ports, Problem from pwncore.routes.auth import RequireJwt -from pwncore.models.statusCode_models import ContainerStartResponse, ContainerStopResponse, ErrorResponse +from pwncore.models.responseModels.ctf_ContainerStatusResponse import ContainerStartResponse, ContainerStopResponse, CTF_ErrorResponse router = APIRouter(tags=["ctf"]) logger = getLogger(__name__) -# shorten response_description + @router.post("/{ctf_id}/start", response_model=ContainerStartResponse, responses={ - 404: {"model": ErrorResponse}, + 404: {"model": CTF_ErrorResponse}, 400: {"model": ContainerStartResponse}, - 500: {"model": ErrorResponse} + 500: {"model": CTF_ErrorResponse} }, response_description="""Start a new Docker container for the specified CTF challenge. @@ -144,11 +144,11 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw "ctf_id": ctf_id, } -# shorten response_description + @router.post("/stopall", response_model=ContainerStopResponse, responses={ - 500: {"model": ErrorResponse} + 500: {"model": CTF_ErrorResponse} }, response_description="""Stop and remove all Docker containers belonging to the authenticated team. @@ -180,13 +180,13 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): return {"msg_code": config.msg_codes["containers_team_stop"]} -# shorten response_description + @router.post("/{ctf_id}/stop", response_model=ContainerStopResponse, responses={ - 404: {"model": ErrorResponse}, - 400: {"model": ErrorResponse}, - 500: {"model": ErrorResponse} + 404: {"model": CTF_ErrorResponse}, + 400: {"model": CTF_ErrorResponse}, + 500: {"model": CTF_ErrorResponse} }, response_description="""Stop and remove a specific CTF challenge container. diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index 0c3d7f0..ca54a6f 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -9,6 +9,8 @@ from pwncore.models import Team +from pwncore.models.responseModels.leaderboard_response import LeaderboardEntry + # Metadata at the top for instant accessibility metadata = {"name": "leaderboard", "description": "Operations on the leaderboard"} @@ -56,12 +58,9 @@ async def get_lb(self, req: Request): gcache = ExpiringLBCache(30.0) -# defining Pydantic response model -class LeaderboardEntry(BaseModel): - name: str - tpoints: int -# shorten response_description + + @router.get("", response_model=list[LeaderboardEntry], response_description=u"""Returns the current CTF leaderboard sorted by total points. diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 889dda6..19c3c67 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -1,13 +1,18 @@ from __future__ import annotations from fastapi import APIRouter, Response -from pydantic import BaseModel from tortoise.transactions import atomic from pwncore.config import config + from pwncore.models import Team, User, Team_Pydantic, User_Pydantic, Container + +from pwncore.models.responseModels.ctf_ContainerStatusResponse import ContainerPortsResponse +from pwncore.models.responseModels.user_mgmtResponse import UserAddBody, UserRemoveBody, MessageResponse + from pwncore.routes.auth import RequireJwt + # from pwncore.routes.leaderboard import gcache # Metadata at the top for instant accessibility @@ -15,25 +20,7 @@ router = APIRouter(prefix="/team", tags=["team"]) - -class UserAddBody(BaseModel): - tag: str - name: str - email: str - phone_num: str - - -class UserRemoveBody(BaseModel): - tag: str - - -class MessageResponse(BaseModel): - msg_code: int - -class ContainerPortsResponse(BaseModel): - ports: dict[int, list[int]] - -# shorten response_description + @router.get("/list", response_model=list[Team_Pydantic], response_description="""Returns a complete list of registered teams. @@ -47,7 +34,7 @@ async def team_list(): # Unable to test as adding users returns an error -# shorten response_description + @router.get("/members", response_model=list[User_Pydantic], response_description="""Returns a list of all members in the authenticated team. @@ -62,7 +49,7 @@ async def team_members(jwt: RequireJwt): # Incase of no members, it just returns an empty list. return members -# shorten response_description + @router.get("/me", response_model=Team_Pydantic, response_description="""Returns the details of the currently authenticated team. @@ -85,7 +72,7 @@ async def get_self_team(jwt: RequireJwt): return team -# shorten response_description + @atomic() @router.post("/add", response_model=MessageResponse, @@ -121,7 +108,7 @@ async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["user_added"]} -# shorten response_description + @atomic() @router.post("/remove", response_model=MessageResponse, @@ -152,7 +139,7 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ return {"msg_code": config.msg_codes["db_error"]} return {"msg_code": config.msg_codes["user_removed"]} -# shorten response_description + @router.get("/containers", response_model=ContainerPortsResponse, response_description="""Get all containers associated with the authenticated team. From 1d8c90009ceade98258f60eed5457649d770d0dd Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sat, 8 Mar 2025 20:02:38 +0530 Subject: [PATCH 09/13] chore: make requested changes --- src/pwncore/routes/admin.py | 8 ++------ src/pwncore/routes/auth.py | 12 ------------ src/pwncore/routes/ctf/__init__.py | 30 +++++------------------------ src/pwncore/routes/ctf/pre_event.py | 16 ++------------- src/pwncore/routes/leaderboard.py | 2 -- src/pwncore/routes/team.py | 17 +--------------- 6 files changed, 10 insertions(+), 75 deletions(-) diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index b2692c4..4ebbb16 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -64,9 +64,7 @@ class AdminResponse(BaseModel): response_model=AdminResponse, response_description="""Successful calculation of team points and coin updates. - Response parameters: boolean `success`, `message` - - Note: Returns 401 if authentication fails. + Note: Returns msg_code 401 if authentication fails. """) async def calculate_team_coins( response: Response, req: Request @@ -104,9 +102,7 @@ async def calculate_team_coins( response_model=AdminResponse, response_description="""Database initialization with sample data. - Response parameters: boolean `success`, `message` - - Note: Returns 401 if authentication fails. + Note: Returns msg_code 401 if authentication fails. This endpoint should only be used in development environment. """) async def init_db( diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index f182322..80a0874 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -24,9 +24,6 @@ router = APIRouter(prefix="/auth", tags=["auth"]) logger = getLogger(__name__) - - - def normalise_tag(tag: str): return tag.strip().casefold() @@ -41,10 +38,6 @@ def normalise_tag(tag: str): 500: {"model": ErrorResponse} }, response_description="""Create a new team with associated members. - - Parameters: - - in request: `name`, `password`, `tags` - - in response: `msg_code`, `tags` _only in error responses_ msg_codes for Responses: - 200: Successful signup : 13 @@ -104,11 +97,6 @@ async def signup_team(team: SignupBody, response: Response): }, response_description="""Authenticate a team and receive a JWT token. - Parameters: - - in request: `name`, `password` - - in successful response: `msg_code`, `access_token`,`token_type` - - in error responses: `msg_code` - msg_codes for Responses: - 200: Successful login: 15 - 404: Team not found: 10 diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 15ec076..b8ff4f5 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -55,11 +55,7 @@ class Flag(BaseModel): @router.get( "/completed", - response_model=list[Problem_Pydantic], - response_description="""Returns all problems solved by the authenticated team. - - Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` - """) + response_model=list[Problem_Pydantic]) async def completed_problem_get(jwt: RequireJwt): team_id = jwt["team_id"] ViewedHint.filter(team_id=team_id).annotate() @@ -72,11 +68,7 @@ async def completed_problem_get(jwt: RequireJwt): @router.get( "/list", response_model=list[Problem_Pydantic], - response_description="""Returns all visible CTF problems with adjusted points based on hints used. - - Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` - - Note: Points are adjusted based on hints viewed by the team. + response_description="""Returns all visible CTF problems with points adjusted based on hints used by the team. """) async def ctf_list(jwt: RequireJwt): team_id = jwt["team_id"] @@ -108,11 +100,7 @@ async def update_points(req: Request, ctf_id: int): "/{ctf_id}/flag", response_model=dict[str, bool | str], response_description="""Submit a flag for a specific CTF problem. - - Parameters: - - in request: string `flag` - - in response: boolean `status` - + msg_codes for Error responses: - 404: ctf_not_found : 2 - 401: ctf_solved : 12 @@ -170,8 +158,6 @@ async def flag_post( "/{ctf_id}/hint", response_model=dict[str, str | int], response_description="""Retrieve the next available hint for a problem. - - Response Parameters: `text`, `order` msg_code for Error responses: - 404: {"msg_code": 2} - ctf_not_found @@ -218,11 +204,7 @@ async def hint_get(ctf_id: int, response: Response, jwt: RequireJwt): @router.get( "/{ctf_id}/viewed_hints", - response_model=list[Hint_Pydantic], - response_description="""Get all hints viewed by the team for a specific problem. - - Response Parameters: `id`, `text`, `order`, `problem_id` - """) + response_model=list[Hint_Pydantic]) async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): team_id = jwt["team_id"] viewed_hints = await Hint_Pydantic.from_queryset( @@ -235,9 +217,7 @@ async def viewed_problem_hints_get(ctf_id: int, jwt: RequireJwt): "/{ctf_id}", response_model=Problem_Pydantic, response_description="""Get details of a specific CTF problem. - - Response Parameters: (6) `id`, `name`, `description`, `points`, `category`, `visible` - + msg_code: - if ctf_not_found or not visible : 2 """) diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index e3d5619..af4e0a9 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -34,10 +34,7 @@ "/list", response_model=list[PreEventProblem_Pydantic], response_description="""Returns a list of all available pre-event CTF problems. - - Parameters = (5): `id`,`name`,`description`,`points`,`date` - - Note: Flag field is excluded from response for security. + Flag field is excluded from response for security. """) async def ctf_list(): problems = await PreEventProblem_Pydantic.from_queryset(PreEventProblem.all()) @@ -48,10 +45,7 @@ async def ctf_list(): "/today", response_model=list[PreEventProblem_Pydantic], response_description="""Returns list of CTF problems scheduled for current date. - - Parameters = (5): `id`,`name`,`description`,`points`,`date` - - Note: Returns empty list if no problems are scheduled for today. + Returns empty list if no problems are scheduled for today. """) async def ctf_today(): return await PreEventProblem_Pydantic.from_queryset( @@ -82,10 +76,6 @@ async def coins_get(tag: str): "/{ctf_id}/flag", response_model=Union[FlagSubmissionResponse, ErrorResponse], response_description="""Submit a solution flag for a pre-event CTF problem. - - Request parameters: `tag`, `flag`,`email` - Parameters in response: `status`, `coins` - Note: status may be true or false Msg_codes for Error responses: - 404: if ctf_not_found or not for current date: 2 @@ -137,8 +127,6 @@ async def pre_event_flag_post(ctf_id: int, post_body: PreEventFlag, response: Re response_model=Union[list[PreEventProblem_Pydantic], ErrorResponse], response_description="""Get complete details of a specific pre-event CTF problem. - Response Parameters = (5): `id`,`name`,`description`,`points`,`date` - msg_code for Error response: - if ctf_not_found : 2 """) diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index ca54a6f..e025b07 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -65,8 +65,6 @@ async def get_lb(self, req: Request): response_model=list[LeaderboardEntry], response_description=u"""Returns the current CTF leaderboard sorted by total points. - Response Parameters: `name`, `tpoints` - Notes: - tpoints = sum of (problem points × penalty multiplier) + team points - Results are cached for 30 seconds diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 19c3c67..cdd2592 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -22,12 +22,7 @@ @router.get("/list", - response_model=list[Team_Pydantic], - response_description="""Returns a complete list of registered teams. - - Response Parameters: `id`, `name`, `coins`, `points` - - """) + response_model=list[Team_Pydantic]) async def team_list(): teams = await Team_Pydantic.from_queryset(Team.all()) return teams @@ -39,8 +34,6 @@ async def team_list(): response_model=list[User_Pydantic], response_description="""Returns a list of all members in the authenticated team. - Response Parameters: `tag`, `name`, `email`, `phone_num` - Note: Returns an empty list if no members are found. """) async def team_members(jwt: RequireJwt): @@ -53,8 +46,6 @@ async def team_members(jwt: RequireJwt): @router.get("/me", response_model=Team_Pydantic, response_description="""Returns the details of the currently authenticated team. - - Response Parameters: `id`, `name`, `coins`, `points` """) async def get_self_team(jwt: RequireJwt): @@ -78,8 +69,6 @@ async def get_self_team(jwt: RequireJwt): response_model=MessageResponse, response_description="""Add a new member to the authenticated team. - Request Parameters: `tag`, `name`, `email`, `phone_num` - msg_code for response: - (success) user_added: 18 - (fail) @@ -113,10 +102,6 @@ async def add_member(user: UserAddBody, response: Response, jwt: RequireJwt): @router.post("/remove", response_model=MessageResponse, response_description="""Remove an existing member from the authenticated team. - - Parameters: - - for request: `tag` - - for response: `msg_code` Msg_code for response: - (success) user_removed: 19 From e01efbf834e5e7f5d10ba68ed88ab79060e6cc0d Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sat, 8 Mar 2025 20:14:05 +0530 Subject: [PATCH 10/13] chore: make requested changes in pwncore/routes/team.py --- src/pwncore/routes/team.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index cdd2592..060b85b 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -31,11 +31,7 @@ async def team_list(): # Unable to test as adding users returns an error @router.get("/members", - response_model=list[User_Pydantic], - response_description="""Returns a list of all members in the authenticated team. - - Note: Returns an empty list if no members are found. - """) + response_model=list[User_Pydantic]) async def team_members(jwt: RequireJwt): team_id = jwt["team_id"] members = await User_Pydantic.from_queryset(User.filter(team_id=team_id)) @@ -44,10 +40,7 @@ async def team_members(jwt: RequireJwt): @router.get("/me", - response_model=Team_Pydantic, - response_description="""Returns the details of the currently authenticated team. - - """) + response_model=Team_Pydantic) async def get_self_team(jwt: RequireJwt): team_id = jwt["team_id"] @@ -126,11 +119,7 @@ async def remove_member(user_info: UserRemoveBody, response: Response, jwt: Requ @router.get("/containers", - response_model=ContainerPortsResponse, - response_description="""Get all containers associated with the authenticated team. - - Note: Object keys are problem IDs and values are lists of the containers' exposed ports. - """) + response_model=ContainerPortsResponse) async def get_team_containers(response: Response, jwt: RequireJwt): containers = await Container.filter(team_id=jwt["team_id"]).prefetch_related( "ports", "problem" From 85b9c5a8767093544524ffae477a6ffa44e8edb0 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sat, 8 Mar 2025 20:29:06 +0530 Subject: [PATCH 11/13] chore: make code little prettier --- ...mtResponse.py => userManagementResponse.py} | 0 src/pwncore/routes/auth.py | 10 +++++++++- src/pwncore/routes/ctf/start.py | 18 +++++++++++------- src/pwncore/routes/leaderboard.py | 5 +---- src/pwncore/routes/team.py | 6 +++++- 5 files changed, 26 insertions(+), 13 deletions(-) rename src/pwncore/models/responseModels/{user_mgmtResponse.py => userManagementResponse.py} (100%) diff --git a/src/pwncore/models/responseModels/user_mgmtResponse.py b/src/pwncore/models/responseModels/userManagementResponse.py similarity index 100% rename from src/pwncore/models/responseModels/user_mgmtResponse.py rename to src/pwncore/models/responseModels/userManagementResponse.py diff --git a/src/pwncore/routes/auth.py b/src/pwncore/routes/auth.py index 80a0874..6613e20 100644 --- a/src/pwncore/routes/auth.py +++ b/src/pwncore/routes/auth.py @@ -11,7 +11,15 @@ from pydantic import BaseModel from tortoise.transactions import atomic -from pwncore.models.responseModels.teamAuthResponse import AuthBody, SignupBody, SignupResponse, SignupErrorUsersNotFound, SignupErrorUsersInTeam, LoginResponse, Auth_ErrorResponse as ErrorResponse +from pwncore.models.responseModels.teamAuthResponse import ( + AuthBody, + SignupBody, + SignupResponse, + SignupErrorUsersNotFound, + SignupErrorUsersInTeam, + LoginResponse, + Auth_ErrorResponse as ErrorResponse +) from pwncore.config import config from pwncore.models import Team, User diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index d979dfa..40f65df 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -10,7 +10,11 @@ from pwncore.config import config from pwncore.models import Container, Ports, Problem from pwncore.routes.auth import RequireJwt -from pwncore.models.responseModels.ctf_ContainerStatusResponse import ContainerStartResponse, ContainerStopResponse, CTF_ErrorResponse +from pwncore.models.responseModels.ctf_ContainerStatusResponse import ( + ContainerStartResponse, + ContainerStopResponse, + CTF_ErrorResponse as ErrorResponse +) router = APIRouter(tags=["ctf"]) logger = getLogger(__name__) @@ -19,9 +23,9 @@ @router.post("/{ctf_id}/start", response_model=ContainerStartResponse, responses={ - 404: {"model": CTF_ErrorResponse}, + 404: {"model": ErrorResponse}, 400: {"model": ContainerStartResponse}, - 500: {"model": CTF_ErrorResponse} + 500: {"model": ErrorResponse} }, response_description="""Start a new Docker container for the specified CTF challenge. @@ -148,7 +152,7 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw @router.post("/stopall", response_model=ContainerStopResponse, responses={ - 500: {"model": CTF_ErrorResponse} + 500: {"model": ErrorResponse} }, response_description="""Stop and remove all Docker containers belonging to the authenticated team. @@ -184,9 +188,9 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt): @router.post("/{ctf_id}/stop", response_model=ContainerStopResponse, responses={ - 404: {"model": CTF_ErrorResponse}, - 400: {"model": CTF_ErrorResponse}, - 500: {"model": CTF_ErrorResponse} + 404: {"model": ErrorResponse}, + 400: {"model": ErrorResponse}, + 500: {"model": ErrorResponse} }, response_description="""Stop and remove a specific CTF challenge container. diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index e025b07..a658a16 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -57,10 +57,7 @@ async def get_lb(self, req: Request): gcache = ExpiringLBCache(30.0) - - - - + @router.get("", response_model=list[LeaderboardEntry], response_description=u"""Returns the current CTF leaderboard sorted by total points. diff --git a/src/pwncore/routes/team.py b/src/pwncore/routes/team.py index 060b85b..9e4962a 100644 --- a/src/pwncore/routes/team.py +++ b/src/pwncore/routes/team.py @@ -8,7 +8,11 @@ from pwncore.models import Team, User, Team_Pydantic, User_Pydantic, Container from pwncore.models.responseModels.ctf_ContainerStatusResponse import ContainerPortsResponse -from pwncore.models.responseModels.user_mgmtResponse import UserAddBody, UserRemoveBody, MessageResponse +from pwncore.models.responseModels.userManagementResponse import ( + UserAddBody, + UserRemoveBody, + MessageResponse +) from pwncore.routes.auth import RequireJwt From 3a90469705aee2097b8e46ca45846e8af2eef221 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sun, 9 Mar 2025 00:42:09 +0530 Subject: [PATCH 12/13] docs: rm response_description --- src/pwncore/models/ctf.py | 4 +- .../responseModels/CTF_StatusResponse.py | 41 +++++++++++++++++++ .../models/responseModels/adminResponse.py | 9 ++++ .../responseModels/preEventCTF_response.py | 7 ---- .../responseModels/teamManagementResponse.py | 34 +++++++++++++++ src/pwncore/routes/ctf/pre_event.py | 13 ++---- src/pwncore/routes/leaderboard.py | 8 +--- 7 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 src/pwncore/models/responseModels/CTF_StatusResponse.py create mode 100644 src/pwncore/models/responseModels/adminResponse.py create mode 100644 src/pwncore/models/responseModels/teamManagementResponse.py diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index 8d90d0c..c41d728 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -5,7 +5,7 @@ from tortoise.models import Model from tortoise import fields from tortoise.contrib.pydantic import pydantic_model_creator - +from pydantic import BaseModel from pwncore.models.user import Team __all__ = ( @@ -19,6 +19,8 @@ "BaseProblem", ) +class Flag(BaseModel): + flag: str class BaseProblem(Model): name = fields.TextField() diff --git a/src/pwncore/models/responseModels/CTF_StatusResponse.py b/src/pwncore/models/responseModels/CTF_StatusResponse.py new file mode 100644 index 0000000..6df04ab --- /dev/null +++ b/src/pwncore/models/responseModels/CTF_StatusResponse.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import List, Optional + +class CTF_ErrorResponse(BaseModel): + """ + Error response for CTF operations + - 404: ctf_not_found : 2 + - 403: hint_limit_reached : 9 + - 412: Insufficient coins : 22 + - 406: container_already_running : 7 + - 429: container_limit_reached : 8 + - 500: db_error : 0 + """ + msg_code: int + +class ContainerStartResponse(BaseModel): + """ + Response for container start operation + msg_codes: + - (success) container_start : 3 + """ + msg_code: int + ports: Optional[List[int]] = None + ctf_id: Optional[int] = None + +class ContainerStopResponse(BaseModel): + """ + Response for container stop operations + - (success) container_stop : 4 + - (fail) + - 404: ctf_not_found : 2 + - 400: container_not_found : 6 + - 500: db_error : 0 + """ + msg_code: int + +class ContainerPortsResponse(BaseModel): + """ + Response for all open ports of containers + """ + ports: dict[int, list[int]] \ No newline at end of file diff --git a/src/pwncore/models/responseModels/adminResponse.py b/src/pwncore/models/responseModels/adminResponse.py new file mode 100644 index 0000000..f4e8796 --- /dev/null +++ b/src/pwncore/models/responseModels/adminResponse.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel +from typing import Optional + +class AdminBaseResponse(BaseModel): + """ + Generic response for admin operations + """ + success: bool + message: Optional[str] = None \ No newline at end of file diff --git a/src/pwncore/models/responseModels/preEventCTF_response.py b/src/pwncore/models/responseModels/preEventCTF_response.py index 6506291..5390385 100644 --- a/src/pwncore/models/responseModels/preEventCTF_response.py +++ b/src/pwncore/models/responseModels/preEventCTF_response.py @@ -4,9 +4,6 @@ class PreEventFlag(BaseModel): """ Response for pre-event flag submission - tag: team tag - flag: flag submitted - email: email of the team """ tag: str flag: str @@ -16,22 +13,18 @@ class PreEventFlag(BaseModel): class CoinsQuery(BaseModel): """ Response for pre-event coins query - tag: team tag """ tag: str class CoinsResponse(BaseModel): """ Response for pre-event coins query - coins: total coins earned by the team in pre-event CTFs """ coins: int class FlagSubmissionResponse(BaseModel): """ Response for pre-event flag submission - status: bool - coins: total coins earned by the team in pre-event CTFs """ status: bool coins: int diff --git a/src/pwncore/models/responseModels/teamManagementResponse.py b/src/pwncore/models/responseModels/teamManagementResponse.py new file mode 100644 index 0000000..48da7af --- /dev/null +++ b/src/pwncore/models/responseModels/teamManagementResponse.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel +import typing as t + + +class UserAddBody(BaseModel): + """ + Request body for adding a user + """ + tag: str + name: str + email: str + phone_num: str + + +class UserRemoveBody(BaseModel): + """ + Request body for removing a user + """ + tag: str + +class MemberStatusResponse(BaseModel): + """ + Response for user management operations in teams + msg_code for response: + - (success) + - user_added: 18 + - user_removed: 19 + + - (fail) + - user_already_in_team: 20 + - user_not_in_team : 21 + - db_error: 0 + """ + msg_code: int \ No newline at end of file diff --git a/src/pwncore/routes/ctf/pre_event.py b/src/pwncore/routes/ctf/pre_event.py index af4e0a9..425bf50 100644 --- a/src/pwncore/routes/ctf/pre_event.py +++ b/src/pwncore/routes/ctf/pre_event.py @@ -33,9 +33,7 @@ @router.get( "/list", response_model=list[PreEventProblem_Pydantic], - response_description="""Returns a list of all available pre-event CTF problems. - Flag field is excluded from response for security. - """) + ) async def ctf_list(): problems = await PreEventProblem_Pydantic.from_queryset(PreEventProblem.all()) return problems @@ -44,9 +42,7 @@ async def ctf_list(): @router.get( "/today", response_model=list[PreEventProblem_Pydantic], - response_description="""Returns list of CTF problems scheduled for current date. - Returns empty list if no problems are scheduled for today. - """) + ) async def ctf_today(): return await PreEventProblem_Pydantic.from_queryset( PreEventProblem().filter(date=datetime.now(_IST).date()) @@ -56,10 +52,7 @@ async def ctf_today(): @router.get( "/coins/{tag}", response_model=CoinsResponse, - response_description="""Get total coins earned by a user in pre-event CTFs. - - Note: Returns msg_code : 11 if user_not_found. - """) + ) async def coins_get(tag: str): try: return { diff --git a/src/pwncore/routes/leaderboard.py b/src/pwncore/routes/leaderboard.py index a658a16..ced051d 100644 --- a/src/pwncore/routes/leaderboard.py +++ b/src/pwncore/routes/leaderboard.py @@ -60,12 +60,6 @@ async def get_lb(self, req: Request): @router.get("", response_model=list[LeaderboardEntry], - response_description=u"""Returns the current CTF leaderboard sorted by total points. - - Notes: - - tpoints = sum of (problem points × penalty multiplier) + team points - - Results are cached for 30 seconds - - Cache is force-expired when problems are solved - """) + ) async def fetch_leaderboard(req: Request): return Response(content=await gcache.get_lb(req), media_type="application/json") From 2a7076e1c2c98e9ee3473014fb05ec64ee75a9a3 Mon Sep 17 00:00:00 2001 From: adithyasunil04 Date: Sun, 9 Mar 2025 00:47:14 +0530 Subject: [PATCH 13/13] rm response_description in admin.py --- src/pwncore/routes/admin.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/pwncore/routes/admin.py b/src/pwncore/routes/admin.py index 4d0f901..95e39c6 100644 --- a/src/pwncore/routes/admin.py +++ b/src/pwncore/routes/admin.py @@ -61,10 +61,7 @@ class AdminResponse(BaseModel): @atomic() @router.get("/union", response_model=AdminResponse, - response_description="""Successful calculation of team points and coin updates. - - Note: Returns msg_code 401 if authentication fails. - """) + ) async def calculate_team_coins( response: Response, req: Request ): # Inefficient, anyways will be used only once @@ -98,12 +95,8 @@ async def calculate_team_coins( @router.get("/create", - response_model=AdminResponse, - response_description="""Database initialization with sample data. - - Note: Returns msg_code 401 if authentication fails. - This endpoint should only be used in development environment. - """) + response_model=AdminResponse + ) async def init_db( response: Response, req: Request ): # Inefficient, anyways will be used only once