fix: enforce run ownership on all mutation endpoints

Add require_run_owner helper in auth.py that enforces ownership on
mutation endpoints. Unowned (legacy) runs are now read-only.

Applied ownership checks to:
- All 4 encounter mutation endpoints
- Both boss result mutation endpoints
- Run update/delete endpoints
- All 5 genlocke mutation endpoints (via first leg's run owner)

Also sets owner_id on run creation in genlockes.py (create_genlocke,
advance_leg) and adds 22 comprehensive ownership enforcement tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:28:32 +01:00
parent a12958ae32
commit eeb1609452
8 changed files with 660 additions and 39 deletions

View File

@@ -5,7 +5,7 @@ from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.core.auth import AuthUser, require_admin, require_auth
from app.core.auth import AuthUser, require_admin, require_auth, require_run_owner
from app.core.database import get_session
from app.models.boss_battle import BossBattle
from app.models.boss_pokemon import BossPokemon
@@ -344,12 +344,14 @@ async def create_boss_result(
run_id: int,
data: BossResultCreate,
session: AsyncSession = Depends(get_session),
_user: AuthUser = Depends(require_auth),
user: AuthUser = Depends(require_auth),
):
run = await session.get(NuzlockeRun, run_id)
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
require_run_owner(run, user)
boss = await session.get(BossBattle, data.boss_battle_id)
if boss is None:
raise HTTPException(status_code=404, detail="Boss battle not found")
@@ -425,8 +427,14 @@ async def delete_boss_result(
run_id: int,
result_id: int,
session: AsyncSession = Depends(get_session),
_user: AuthUser = Depends(require_auth),
user: AuthUser = Depends(require_auth),
):
run = await session.get(NuzlockeRun, run_id)
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
require_run_owner(run, user)
result = await session.execute(
select(BossResult).where(
BossResult.id == result_id, BossResult.run_id == run_id