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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user