diff --git a/backend/bracket/routes/courts.py b/backend/bracket/routes/courts.py index 0d177b383..4dd1583b9 100644 --- a/backend/bracket/routes/courts.py +++ b/backend/bracket/routes/courts.py @@ -108,7 +108,7 @@ async def create_court( database, Court, courts.select().where( - courts.c.id == last_record_id and courts.c.tournament_id == tournament_id + (courts.c.id == last_record_id) & (courts.c.tournament_id == tournament_id) ), ) ) diff --git a/backend/bracket/routes/util.py b/backend/bracket/routes/util.py index 978764d03..267dc5ef4 100644 --- a/backend/bracket/routes/util.py +++ b/backend/bracket/routes/util.py @@ -7,7 +7,7 @@ from bracket.models.db.team import FullTeamWithPlayers, Team from bracket.models.db.tournament import Tournament, TournamentStatus from bracket.models.db.util import RoundWithMatches, StageItemWithRounds, StageWithStageItems -from bracket.schema import matches, rounds, teams +from bracket.schema import matches, rounds, stage_items, stages, teams from bracket.sql.rounds import get_round_by_id from bracket.sql.stage_items import get_stage_item from bracket.sql.stages import get_full_tournament_details @@ -21,7 +21,13 @@ async def round_dependency(tournament_id: TournamentId, round_id: RoundId) -> Ro round_ = await fetch_one_parsed( database, Round, - rounds.select().where(rounds.c.id == round_id and matches.c.tournament_id == tournament_id), + rounds.select() + .select_from( + rounds.join(stage_items, rounds.c.stage_item_id == stage_items.c.id).join( + stages, stage_items.c.stage_id == stages.c.id + ) + ) + .where((rounds.c.id == round_id) & (stages.c.tournament_id == tournament_id)), ) if round_ is None: @@ -40,17 +46,17 @@ async def round_with_matches_dependency( async def stage_dependency(tournament_id: TournamentId, stage_id: StageId) -> StageWithStageItems: - stages = await get_full_tournament_details( + stages_result = await get_full_tournament_details( tournament_id, no_draft_rounds=False, stage_id=stage_id ) - if len(stages) < 1: + if len(stages_result) < 1: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Could not find stage with id {stage_id}", ) - return stages[0] + return stages_result[0] async def stage_item_dependency( @@ -63,9 +69,13 @@ async def match_dependency(tournament_id: TournamentId, match_id: MatchId) -> Ma match = await fetch_one_parsed( database, Match, - matches.select().where( - matches.c.id == match_id and matches.c.tournament_id == tournament_id - ), + matches.select() + .select_from( + matches.join(rounds, matches.c.round_id == rounds.c.id) + .join(stage_items, rounds.c.stage_item_id == stage_items.c.id) + .join(stages, stage_items.c.stage_id == stages.c.id) + ) + .where((matches.c.id == match_id) & (stages.c.tournament_id == tournament_id)), ) if match is None: @@ -81,7 +91,9 @@ async def team_dependency(tournament_id: TournamentId, team_id: TeamId) -> Team: team = await fetch_one_parsed( database, Team, - teams.select().where(teams.c.id == team_id and teams.c.tournament_id == tournament_id), + teams.select().where( + (teams.c.id == team_id) & (teams.c.tournament_id == tournament_id) + ), ) if team is None: diff --git a/backend/tests/integration_tests/api/teams_test.py b/backend/tests/integration_tests/api/teams_test.py index a55a30dab..f9d028150 100644 --- a/backend/tests/integration_tests/api/teams_test.py +++ b/backend/tests/integration_tests/api/teams_test.py @@ -6,11 +6,19 @@ from bracket.models.db.team import Team from bracket.schema import players, teams from bracket.utils.db import fetch_one_parsed_certain -from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TEAM1 +from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TEAM1, DUMMY_TOURNAMENT from bracket.utils.http import HTTPMethod -from tests.integration_tests.api.shared import SUCCESS_RESPONSE, send_tournament_request +from tests.integration_tests.api.shared import ( + SUCCESS_RESPONSE, + send_auth_request, + send_tournament_request, +) from tests.integration_tests.models import AuthContext -from tests.integration_tests.sql import assert_row_count_and_clear, inserted_team +from tests.integration_tests.sql import ( + assert_row_count_and_clear, + inserted_team, + inserted_tournament, +) @pytest.mark.asyncio(loop_scope="session") @@ -153,3 +161,25 @@ async def test_team_upload_and_remove_logo( assert not await aiofiles.os.path.exists( f"static/team-logos/{response['data']['logo_path']}" ) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_cross_tournament_team_access_denied( + startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext +) -> None: + """Regression test: a team from tournament B cannot be accessed via tournament A's URL.""" + async with inserted_tournament( + DUMMY_TOURNAMENT.model_copy( + update={"club_id": auth_context.club.id, "dashboard_endpoint": None} + ) + ) as other_tournament: + async with inserted_team( + DUMMY_TEAM1.model_copy(update={"tournament_id": other_tournament.id}) + ) as other_team: + response = await send_auth_request( + HTTPMethod.PUT, + f"tournaments/{auth_context.tournament.id}/teams/{other_team.id}", + auth_context, + json={"name": "Hacked", "active": True, "player_ids": []}, + ) + assert response.get("detail") == f"Could not find team with id {other_team.id}"