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
+34 -7
View File
@@ -29,14 +29,13 @@ def get_auth_context(
In development / testing mode, allows test tokens while enforcing identity isolation.
"""
token = None
if api_key:
token = api_key
elif credentials:
if credentials and credentials.credentials:
token = credentials.credentials
elif api_key:
token = api_key
# If no token is provided
if not token:
# Check if environment allows default dev token or requires strict 401
raise HTTPException(
status_code=401,
detail="Authentication credentials required. Please provide X-API-Key or Bearer token.",
@@ -51,9 +50,37 @@ def get_auth_context(
headers={"WWW-Authenticate": "Bearer"}
)
# Derive patient/user identity context from token or header
patient_id = x_patient_id or ("PAT-DEFAULT" if token == API_SECRET_KEY else token.replace("test_token_", ""))
return AuthContext(user_id=patient_id, role="patient", patient_id=patient_id)
# Master API Secret Key has admin/service privileges
if token == API_SECRET_KEY:
patient_id = x_patient_id or "PAT-DEFAULT"
return AuthContext(user_id=patient_id, role="admin", patient_id=patient_id)
else:
patient_id = x_patient_id or token.replace("test_token_", "")
return AuthContext(user_id=patient_id, role="patient", patient_id=patient_id)
def verify_session_ownership(session_patient_id: Optional[str], auth: AuthContext, detail: str = "Forbidden: You do not have authorization to access or modify this onboarding session."):
"""
Verifies session ownership by comparing normalized patient IDs.
Master API key callers (role='admin') are always authorized.
"""
if auth.role == "admin":
return
s_id = (session_patient_id or "").strip()
a_id = (auth.patient_id or "").strip()
if s_id == a_id:
return
# Normalize "PAT-" prefixes for matching (e.g. "PAT-417" vs "417")
s_norm = s_id[4:] if s_id.startswith("PAT-") else s_id
a_norm = a_id[4:] if a_id.startswith("PAT-") else a_id
if s_norm == a_norm:
return
raise HTTPException(status_code=403, detail=detail)
# Simple In-Memory Token Bucket Rate Limiter