diff --git a/hasura/functions/tournaments/team/can_manage_team.sql b/hasura/functions/tournaments/team/can_manage_team.sql index 9aa0d662f..6b2a598dc 100644 --- a/hasura/functions/tournaments/team/can_manage_team.sql +++ b/hasura/functions/tournaments/team/can_manage_team.sql @@ -1,32 +1,51 @@ CREATE OR REPLACE FUNCTION public.can_manage_tournament_team(tournament_team public.tournament_teams, hasura_session json) RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$ +DECLARE + _user_steam_id bigint; BEGIN IF hasura_session ->> 'x-hasura-role' = 'admin' OR hasura_session ->> 'x-hasura-role' = 'administrator' OR hasura_session ->> 'x-hasura-role' = 'tournament_organizer' THEN RETURN true; END IF; - IF tournament_team.team_id IS NOT NULL THEN - RETURN EXISTS ( - SELECT 1 FROM tournament_team_roster - WHERE - tournament_id = tournament_team.tournament_id - AND player_steam_id = (hasura_session ->> 'x-hasura-user-id')::bigint - AND role IN ('Admin') - ); + _user_steam_id := (hasura_session ->> 'x-hasura-user-id')::bigint; + + IF _user_steam_id IS NULL THEN + RETURN false; END IF; - IF tournament_team.owner_steam_id = (hasura_session ->> 'x-hasura-user-id')::bigint THEN + IF tournament_team.owner_steam_id = _user_steam_id THEN RETURN true; END IF; - RETURN EXISTS ( - SELECT 1 FROM tournament_team_roster - WHERE - tournament_id = tournament_team.tournament_id - AND player_steam_id = (hasura_session ->> 'x-hasura-user-id')::bigint + -- Scoped to this tournament_team, never the tournament: a roster row only + -- makes you an Admin of the team it belongs to. + IF EXISTS ( + SELECT 1 FROM tournament_team_roster + WHERE + tournament_team_id = tournament_team.id + AND player_steam_id = _user_steam_id AND role IN ('Admin') - ); + ) THEN + RETURN true; + END IF; + + IF tournament_team.team_id IS NOT NULL THEN + RETURN EXISTS ( + SELECT 1 FROM teams + WHERE + id = tournament_team.team_id + AND (owner_steam_id = _user_steam_id OR captain_steam_id = _user_steam_id) + ) OR EXISTS ( + SELECT 1 FROM team_roster + WHERE + team_id = tournament_team.team_id + AND player_steam_id = _user_steam_id + AND role IN ('Admin') + ); + END IF; + + RETURN false; END; -$$; \ No newline at end of file +$$; diff --git a/hasura/metadata/databases/default/tables/public_tournament_teams.yaml b/hasura/metadata/databases/default/tables/public_tournament_teams.yaml index d1ce4c5bb..2fef3e888 100644 --- a/hasura/metadata/databases/default/tables/public_tournament_teams.yaml +++ b/hasura/metadata/databases/default/tables/public_tournament_teams.yaml @@ -191,26 +191,33 @@ delete_permissions: - role: user permission: filter: - _or: - - team: - roster: - _and: - - role: - _eq: Admin - - player_steam_id: + _and: + - _or: + - team: + roster: + _and: + - role: + _eq: Admin + - player_steam_id: + _eq: X-Hasura-User-Id + - team: + owner_steam_id: + _eq: X-Hasura-User-Id + - team: + captain_steam_id: + _eq: X-Hasura-User-Id + - _and: + - team_id: + _is_null: true + - owner_steam_id: _eq: X-Hasura-User-Id - - _and: - - team_id: - _is_null: true - - owner_steam_id: - _eq: X-Hasura-User-Id - - tournament: - is_organizer: - _eq: true + - tournament: + is_organizer: + _eq: true - tournament: status: - _in: - - Setup - - RegistrationOpen - - RegistrationClosed + _nin: + - Cancelled + - CancelledMinTeams + - Finished comment: "" diff --git a/test/permissions.spec.ts b/test/permissions.spec.ts index f9957b2da..5341c0263 100644 --- a/test/permissions.spec.ts +++ b/test/permissions.spec.ts @@ -2,9 +2,11 @@ import { PostgresService } from "./../src/postgres/postgres.service"; import { Fixtures } from "./utils/fixtures"; import { bootMigratedDb, + runAsUser, seedRegionWithServer, SqlTestDb, } from "./utils/sql-test-db"; +import { TournamentFixtures } from "./utils/tournament-fixtures"; // Exercises the Hasura permission functions — the layer that decides what a // session may do. These run as plain SELECTs with an explicit session JSON, @@ -330,4 +332,58 @@ describe("permission functions (SQL-driven)", () => { ).toBe(true); }); }); + + describe("can_manage_tournament_team", () => { + const canManage = (tournamentTeamId: string, steamId: string) => + boolFn( + "can_manage_tournament_team", + "tournament_teams", + tournamentTeamId, + "id", + session(steamId), + ); + + // A free-agent entry: no team_id, so the joiner's own roster row is + // promoted to Admin by tbi_tournament_team_roster. + const joinAsFreeAgent = async (tournamentId: string) => { + const joiner = await fx.player(); + const id = await runAsUser(postgres, joiner, "user", async (query) => { + const [row] = (await query( + `INSERT INTO tournament_teams (tournament_id, name, owner_steam_id) + VALUES ($1, $2, $3) RETURNING id`, + [tournamentId, fx.nextName("solo"), joiner], + )) as Array<{ id: string }>; + await query( + `INSERT INTO tournament_team_roster (tournament_team_id, tournament_id, player_steam_id) + VALUES ($1, $2, $3)`, + [row.id, tournamentId, joiner], + ); + return row.id; + }); + return { id, joiner }; + }; + + it("scopes management to the joiner's own team", async () => { + const tf = new TournamentFixtures(postgres, fx); + const tournament = await tf.createTournament([ + { type: "SingleElimination", order: 1, minTeams: 4, maxTeams: 8 }, + ]); + await tf.setStatus(tournament.id, tournament.organizer, "RegistrationOpen"); + + const team = await fx.team(1); + const registered = await tf.registerTeam(tournament.id, team); + const solo = await joinAsFreeAgent(tournament.id); + + expect(await canManage(solo.id, solo.joiner)).toBe(true); + expect(await canManage(registered, team.owner)).toBe(true); + + // Being Admin of one entry in the tournament must not carry over. + expect(await canManage(registered, solo.joiner)).toBe(false); + expect(await canManage(solo.id, team.owner)).toBe(false); + + const outsider = await fx.player(); + expect(await canManage(registered, outsider)).toBe(false); + expect(await canManage(solo.id, outsider)).toBe(false); + }); + }); });