Harden media and gameday access control

This commit is contained in:
Codex
2026-04-24 08:09:31 -05:00
parent cc241d4ae7
commit 22d4f8c017
4 changed files with 333 additions and 70 deletions

View File

@@ -11,7 +11,7 @@ from fastapi.testclient import TestClient
from app.config import settings
from app.database import Base, SessionLocal, engine
from app.main import app
from app.models import AudioAsset, AudioClip, UserSession
from app.models import AudioAsset, AudioClip, GameAssignment, UserSession
from app.routes.teamsnap import rewrite_teamsnap_urls
@@ -344,6 +344,63 @@ def test_player_can_pin_a_clip_to_multiple_games_independently() -> None:
assert [item["clip_id"] for item in pins.json()] == [first_clip.id]
def test_player_cannot_pin_another_players_clip() -> None:
db = SessionLocal()
owner_session = UserSession(
session_token="owner-session",
provider="teamsnap",
external_team_id="team-1",
external_player_id="player-1",
)
attacker_session = UserSession(
session_token="attacker-session",
provider="teamsnap",
external_team_id="team-1",
external_player_id="player-2",
)
db.add_all([owner_session, attacker_session])
db.flush()
asset = AudioAsset(
external_team_id="team-1",
owner_external_player_id="player-1",
uploaded_by_session_id=owner_session.id,
title="Song",
original_filename="song.mp3",
mime_type="audio/mpeg",
size_bytes=123,
storage_path="uploads/song.mp3",
)
db.add(asset)
db.flush()
clip = AudioClip(
asset_id=asset.id,
label="Intro",
start_ms=0,
end_ms=10000,
normalization_status="ready",
normalized_path="clips/intro.mp3",
)
db.add(clip)
db.commit()
db.refresh(clip)
db.close()
client.cookies.set(settings.session_cookie_name, "attacker-session")
response = client.post(
"/games/game-1/assignments",
json={
"external_team_id": "team-1",
"external_player_id": "player-1",
"clip_id": clip.id,
"batting_slot": 1,
"status": "ready",
},
)
assert response.status_code == 403
def test_upload_creates_default_clip_and_clip_ranges_can_be_updated() -> None:
login = client.post("/auth/admin/login", json={"username": "admin", "password": "admin"})
assert login.status_code == 200
@@ -552,7 +609,12 @@ def test_hidden_clips_are_removed_from_gameday_views_but_remain_pinnable() -> No
def test_clip_updates_can_use_player_scoped_authorization() -> None:
uploader_session = UserSession(session_token="uploader-session", provider="teamsnap")
editor_session = UserSession(session_token="editor-session", provider="teamsnap")
editor_session = UserSession(
session_token="editor-session",
provider="teamsnap",
external_team_id="team-3",
external_player_id="player-3",
)
db = SessionLocal()
db.add_all([uploader_session, editor_session])
@@ -597,6 +659,59 @@ def test_clip_updates_can_use_player_scoped_authorization() -> None:
assert updated_clip["label"] == "Player clip"
def test_clip_updates_reject_cross_player_scopes() -> None:
uploader_session = UserSession(
session_token="uploader-session",
provider="teamsnap",
external_team_id="team-3",
external_player_id="player-3",
)
editor_session = UserSession(
session_token="editor-session",
provider="teamsnap",
external_team_id="team-3",
external_player_id="player-4",
)
db = SessionLocal()
db.add_all([uploader_session, editor_session])
db.flush()
asset = AudioAsset(
external_team_id="team-3",
owner_external_player_id="player-3",
uploaded_by_session_id=uploader_session.id,
title="Player track",
original_filename="player-track.mp3",
mime_type="audio/mpeg",
size_bytes=123,
storage_path="uploads/player-track.mp3",
)
db.add(asset)
db.flush()
clip = AudioClip(
asset_id=asset.id,
label="Player clip",
start_ms=0,
end_ms=30000,
normalization_status="ready",
normalized_path="clips/player-clip.mp3",
)
db.add(clip)
db.commit()
db.refresh(clip)
db.close()
client.cookies.set(settings.session_cookie_name, "editor-session")
update = client.patch(
f"/media/clips/{clip.id}",
params={"owner_external_player_id": "player-3"},
json={"start_ms": 1500, "end_ms": 9000},
)
assert update.status_code == 403
def test_create_clip_uses_team_and_player_scope() -> None:
login = client.post("/auth/admin/login", json={"username": "admin", "password": "admin"})
assert login.status_code == 200
@@ -634,6 +749,57 @@ def test_create_clip_uses_team_and_player_scope() -> None:
assert clip["end_ms"] == 6000
def test_create_asset_with_default_clip_cleans_up_files_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
from app.routes import media as media_routes
db = SessionLocal()
session = UserSession(
session_token="cleanup-session",
provider="teamsnap",
external_team_id="team-clean",
external_player_id="player-clean",
)
db.add(session)
db.commit()
db.refresh(session)
source_relative_path = "uploads/cleanup-source.wav"
source_path = settings.media_root / source_relative_path
source_path.parent.mkdir(parents=True, exist_ok=True)
source_path.write_bytes(make_test_wav_bytes())
normalized_path = settings.media_root / "normalized" / "cleanup-copy.wav"
def fake_normalize_clip(source_relative_path_arg: str, clip_name: str) -> str:
normalized_path.parent.mkdir(parents=True, exist_ok=True)
normalized_path.write_bytes((settings.media_root / source_relative_path_arg).read_bytes())
return "normalized/cleanup-copy.wav"
def fake_generate_waveform(source_relative_path_arg: str, bins: int = media_routes.WAVEFORM_PEAK_COUNT) -> dict[str, int | list[int]]:
raise RuntimeError("waveform failed")
monkeypatch.setattr(media_routes.storage, "normalize_clip", fake_normalize_clip)
monkeypatch.setattr(media_routes.storage, "generate_waveform", fake_generate_waveform)
with pytest.raises(RuntimeError):
media_routes.create_asset_with_default_clip(
db=db,
session=session,
external_team_id="team-clean",
owner_external_player_id="player-clean",
title="Cleanup track",
original_filename="cleanup-source.wav",
mime_type="audio/wav",
size_bytes=source_path.stat().st_size,
storage_path=source_relative_path,
)
assert not source_path.exists()
assert not normalized_path.exists()
assert db.query(AudioAsset).count() == 0
assert db.query(AudioClip).count() == 0
db.close()
def test_asset_title_can_be_edited() -> None:
login = client.post("/auth/admin/login", json={"username": "admin", "password": "admin"})
assert login.status_code == 200