security: fix auth, CORS, file upload, endpoint hardening + test fixes

- Add INTERNAL_SECRET shared-secret auth to /internal/nc-upload endpoint
- Add JWT token validation to WebSocket /ws/versions/{version_id}
- Fix NameError: band_slug → band.slug in internal.py
- Move inline imports to top of internal.py; add missing Member/NextcloudClient imports
- Remove ~15 debug print() statements from auth.py
- Replace Content-Type-only avatar check with extension whitelist + Pillow Image.verify()
- Sanitize exception details in versions.py (no more str(e) in 4xx/5xx responses)
- Restrict CORS allow_methods/allow_headers from "*" to explicit lists
- Add security headers middleware: X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Reduce JWT expiry from 7 days to 1 hour
- Add Pillow>=10.0 dependency; document INTERNAL_SECRET in .env.example
- Implement missing RedisJobQueue.dequeue() method (required by protocol)
- Fix 5 pre-existing unit test failures: settings env vars conftest, deferred Redis push,
  dequeue method, AsyncMock→MagicMock for sync scalar_one_or_none

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-03-30 21:02:56 +02:00
parent efef818612
commit 68da26588a
12 changed files with 161 additions and 98 deletions

View File

@@ -3,11 +3,14 @@
import logging
from pathlib import Path
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.config import get_settings
from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import BandMember, Member
from rehearsalhub.repositories.audio_version import AudioVersionRepository
from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository
@@ -15,11 +18,19 @@ from rehearsalhub.repositories.song import SongRepository
from rehearsalhub.schemas.audio_version import AudioVersionCreate
from rehearsalhub.services.session import extract_session_folder, parse_rehearsal_date
from rehearsalhub.services.song import SongService
from rehearsalhub.storage.nextcloud import NextcloudClient
log = logging.getLogger(__name__)
router = APIRouter(prefix="/internal", tags=["internal"])
async def _verify_internal_secret(x_internal_token: str | None = Header(None)) -> None:
"""Verify the shared secret sent by internal services."""
settings = get_settings()
if x_internal_token != settings.internal_secret:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
AUDIO_EXTENSIONS = {".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".opus"}
@@ -32,6 +43,7 @@ class NcUploadEvent(BaseModel):
async def nc_upload(
event: NcUploadEvent,
session: AsyncSession = Depends(get_session),
_: None = Depends(_verify_internal_secret),
):
"""
Called by nc-watcher when a new audio file is detected in Nextcloud.
@@ -105,13 +117,11 @@ async def nc_upload(
nc_folder_path=nc_folder,
created_by=None,
)
log.info("nc-upload: created song '%s' for band '%s'", title, band_slug)
log.info("nc-upload: created song '%s' for band '%s'", title, band.slug)
elif rehearsal_session_id and song.session_id is None:
song = await song_repo.update(song, session_id=rehearsal_session_id)
# Use first member of the band as uploader (best-effort for watcher uploads)
from sqlalchemy import select
from rehearsalhub.db.models import BandMember
result = await session.execute(
select(BandMember.member_id).where(BandMember.band_id == band.id).limit(1)
)
@@ -121,7 +131,7 @@ async def nc_upload(
storage = None
if uploader_id:
uploader_result = await session.execute(
select(Member).where(Member.id == uploader_id).limit(1)
select(Member).where(Member.id == uploader_id).limit(1) # type: ignore[arg-type]
)
uploader = uploader_result.scalar_one_or_none()
storage = NextcloudClient.for_member(uploader) if uploader else None