fix(security): resolve 403 Forbidden on voice turn - normalize patient ID matching with PAT-prefix support - Bearer token now prioritized over X-API-Key for identity resolution - Master API key gets admin role for service-to-service bypass - Centralized verify_session_ownership helper across all IDOR checks - Files: security.py, voice_onboarding.py, onboarding.py, documents.py

This commit is contained in:
2026-08-31 15:09:25 +05:30
parent 0cd932dbce
commit 1d794ca425
4 changed files with 42 additions and 35 deletions
+2 -6
View File
@@ -8,7 +8,7 @@ from app.schemas.documents import DocumentExtractionResponse
from app.services.onboarding_service import OnboardingService
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from app.core.security import get_auth_context, AuthContext, check_rate_limit
from app.core.security import get_auth_context, AuthContext, check_rate_limit, verify_session_ownership
router = APIRouter(
prefix="/v1/documents",
@@ -38,11 +38,7 @@ async def upload_medical_document(
raise HTTPException(status_code=404, detail="Onboarding session not found")
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to upload documents to this session.",
)
verify_session_ownership(session.patient_id, auth, detail="Forbidden: You do not have authorization to upload documents to this session.")
# Enforce MIME type and File Size Limits (10MB MAX)
allowed_mimes = ["image/jpeg", "image/png", "image/webp", "application/pdf"]
+3 -11
View File
@@ -39,7 +39,7 @@ from app.services.voice_service import VoiceService
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from app.core.security import get_auth_context, AuthContext, check_rate_limit
from app.core.security import get_auth_context, AuthContext, check_rate_limit, verify_session_ownership
router = APIRouter(
prefix="/v1/onboarding",
@@ -147,11 +147,7 @@ async def send_message(
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to access or modify this patient onboarding session.",
)
verify_session_ownership(session.patient_id, auth, detail="Forbidden: You do not have authorization to access or modify this patient onboarding session.")
# 2. Check session
if session.status != "in_progress":
@@ -298,11 +294,7 @@ async def complete_onboarding(
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to complete this onboarding session.",
)
verify_session_ownership(session.patient_id, auth, detail="Forbidden: You do not have authorization to complete this onboarding session.")
# 2. Load state
state = onboarding_service.get_state(
@@ -32,7 +32,7 @@ from app.services.patient_submission_service import PatientSubmissionService
from app.services.result_extraction_service import ResultExtractionService
from app.services.voice_service import VoiceService
from app.core.security import get_auth_context, AuthContext, check_rate_limit
from app.core.security import get_auth_context, AuthContext, check_rate_limit, verify_session_ownership
router = APIRouter(
prefix="/v1/voice-onboarding",
@@ -159,11 +159,7 @@ async def voice_turn(
)
# 1b. IDOR Session Ownership Check
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to access or modify this voice session.",
)
verify_session_ownership(session.patient_id, auth, detail="Forbidden: You do not have authorization to access or modify this voice session.")
if session.status == "completed":
session.status = "in_progress"
@@ -393,11 +389,7 @@ async def complete_voice_onboarding(
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to complete this voice session.",
)
verify_session_ownership(session.patient_id, auth, detail="Forbidden: You do not have authorization to complete this voice session.")
state = onboarding_service.get_state(db, session_id)