Harden media and gameday access control
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user