Skip to content

Commit c9390fe

Browse files
committed
Fix S1-5 and S1-6: Implement real auth resolution and JWT verification
- Add field validator for api_keys to parse comma-separated strings - Update verify_auth to return User object instead of dict - Implement actual JWT verification using python-jose - Add current_user parameter to all route functions in cases.py and jobs.py - Replace hardcoded user_id=1 with current_user.id in all routes and audit logs - Remove router-level dependency and use per-route Depends(verify_auth)
1 parent 09dadda commit c9390fe

4 files changed

Lines changed: 124 additions & 27 deletions

File tree

pybrain/api/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ class Settings(BaseSettings):
8686
description="List of valid API keys for research mode authentication",
8787
)
8888

89+
@field_validator("api_keys", mode="before")
90+
@classmethod
91+
def parse_api_keys(cls, v):
92+
"""Parse comma-separated API keys string."""
93+
if isinstance(v, str):
94+
return [k.strip() for k in v.split(",") if k.strip()]
95+
return v
96+
8997
# CORS
9098
allowed_origins: List[str] = Field(
9199
default_factory=lambda: ["http://localhost:3000", "http://localhost:8080"],

pybrain/api/main.py

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
"""
44

55
from contextlib import asynccontextmanager
6+
from datetime import datetime, timedelta
67
from fastapi import FastAPI, HTTPException, Depends, status, Request
78
from fastapi.middleware.cors import CORSMiddleware
89
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
910
from fastapi.responses import JSONResponse
1011
import redis.asyncio as redis
11-
from sqlalchemy import text
12+
from sqlalchemy import text, select
1213
from sqlalchemy.ext.asyncio import AsyncSession
1314
import logging
1415

1516
from pybrain.api.config import settings
16-
from pybrain.api.db.base import engine, init_db
17+
from pybrain.api.db.base import engine, init_db, get_db
18+
from pybrain.api.db.models import User
1719
from pybrain.api.routes import cases, jobs
1820

1921
logger = logging.getLogger(__name__)
@@ -104,9 +106,10 @@ async def readiness_check():
104106
# Auth middleware
105107
async def verify_auth(
106108
credentials: HTTPAuthorizationCredentials = Depends(security),
107-
) -> dict:
109+
db: AsyncSession = Depends(get_db),
110+
) -> User:
108111
"""
109-
Verify JWT or API key authentication.
112+
Verify JWT or API key authentication and return User object.
110113
In research mode, API keys are accepted.
111114
"""
112115
if credentials is None:
@@ -120,12 +123,84 @@ async def verify_auth(
120123

121124
# Check if it's an API key (research mode)
122125
if settings.api_keys and token in settings.api_keys:
123-
return {"type": "api_key", "key": token}
126+
# Look up user by api_key
127+
result = await db.execute(
128+
select(User).where(User.api_key == token, User.is_active == True)
129+
)
130+
user = result.scalar_one_or_none()
131+
if user is None:
132+
# Create a default user for research mode
133+
user = User(
134+
username="research_user",
135+
api_key=token,
136+
is_active=True,
137+
created_at=datetime.utcnow(),
138+
updated_at=datetime.utcnow(),
139+
)
140+
db.add(user)
141+
await db.commit()
142+
await db.refresh(user)
143+
return user
144+
145+
# Try JWT verification
146+
try:
147+
from jose import jwt, JWTError
148+
149+
payload = jwt.decode(
150+
token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]
151+
)
152+
user_id = payload.get("sub")
153+
if user_id is None:
154+
raise JWTError("Missing sub claim")
155+
156+
user = await db.get(User, int(user_id))
157+
if user is None or not user.is_active:
158+
raise HTTPException(
159+
status_code=status.HTTP_401_UNAUTHORIZED,
160+
detail="User not found or inactive",
161+
)
162+
return user
163+
except ImportError:
164+
# python-jose not installed, fall back to development mode
165+
pass
166+
except JWTError as e:
167+
if settings.environment == "development":
168+
# Dev fallback: create default user
169+
result = await db.execute(select(User).where(User.username == "dev"))
170+
user = result.scalar_one_or_none()
171+
if user is None:
172+
user = User(
173+
username="dev",
174+
api_key="dev",
175+
is_active=True,
176+
created_at=datetime.utcnow(),
177+
updated_at=datetime.utcnow(),
178+
)
179+
db.add(user)
180+
await db.commit()
181+
await db.refresh(user)
182+
return user
183+
raise HTTPException(
184+
status_code=status.HTTP_401_UNAUTHORIZED,
185+
detail=f"Invalid token: {e}",
186+
)
124187

125-
# TODO: Verify JWT token
126-
# For now, accept any token in development mode
188+
# Development mode fallback
127189
if settings.environment == "development":
128-
return {"type": "jwt", "token": token}
190+
result = await db.execute(select(User).where(User.username == "dev"))
191+
user = result.scalar_one_or_none()
192+
if user is None:
193+
user = User(
194+
username="dev",
195+
api_key="dev",
196+
is_active=True,
197+
created_at=datetime.utcnow(),
198+
updated_at=datetime.utcnow(),
199+
)
200+
db.add(user)
201+
await db.commit()
202+
await db.refresh(user)
203+
return user
129204

130205
raise HTTPException(
131206
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -135,8 +210,8 @@ async def verify_auth(
135210

136211

137212
# Include routers
138-
app.include_router(cases.router, dependencies=[Depends(verify_auth)])
139-
app.include_router(jobs.router, dependencies=[Depends(verify_auth)])
213+
app.include_router(cases.router)
214+
app.include_router(jobs.router)
140215

141216

142217
# Root endpoint

pybrain/api/routes/cases.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
import tempfile
1616

1717
from pybrain.api.db.base import get_db
18-
from pybrain.api.db.models import Case, Job, LongitudinalLink
18+
from pybrain.api.db.models import Case, Job, LongitudinalLink, User
1919
from pybrain.api.storage import storage
2020
from pybrain.api.audit import log_patient_data_access, log_patient_data_modification, log_api_call
2121
from pybrain.api.routes.jobs import create_segmentation_job
22+
from pybrain.api.main import verify_auth
2223
import logging
2324

2425
logger = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ async def create_case(
3435
patient_sex: Optional[str] = Form(None),
3536
analysis_mode: str = Form("auto"),
3637
db: AsyncSession = Depends(get_db),
38+
current_user: User = Depends(verify_auth),
3739
) -> Dict[str, Any]:
3840
"""
3941
Upload DICOM zip or NIfTI files and create a case.
@@ -61,7 +63,7 @@ async def create_case(
6163
shutil.copyfileobj(files.file, f)
6264

6365
# Extract if zip file
64-
if files.filename.endswith(".zip"):
66+
if files.filename and files.filename.endswith(".zip"):
6567
extract_path = Path(tempfile.mkdtemp())
6668
with zipfile.ZipFile(temp_path, "r") as zip_ref:
6769
zip_ref.extractall(extract_path)
@@ -86,7 +88,7 @@ async def create_case(
8688
# Create case record
8789
case = Case(
8890
id=case_id,
89-
user_id=1, # TODO: Get from auth context
91+
user_id=current_user.id,
9092
patient_name=patient_name,
9193
patient_age=patient_age,
9294
patient_sex=patient_sex,
@@ -100,7 +102,7 @@ async def create_case(
100102
# Log audit
101103
await log_patient_data_modification(
102104
db=db,
103-
user_id=1, # TODO: Get from auth context
105+
user_id=current_user.id,
104106
case_id=case_id,
105107
action="create",
106108
new_values={
@@ -122,6 +124,7 @@ async def create_case(
122124
async def get_case(
123125
case_id: str,
124126
db: AsyncSession = Depends(get_db),
127+
current_user: User = Depends(verify_auth),
125128
) -> Dict[str, Any]:
126129
"""
127130
Get case status and results.
@@ -141,7 +144,7 @@ async def get_case(
141144
# Log audit
142145
await log_patient_data_access(
143146
db=db,
144-
user_id=1, # TODO: Get from auth context
147+
user_id=current_user.id,
145148
case_id=case_id,
146149
)
147150

@@ -165,6 +168,7 @@ async def get_case(
165168
async def get_segmentation(
166169
case_id: str,
167170
db: AsyncSession = Depends(get_db),
171+
current_user: User = Depends(verify_auth),
168172
) -> FileResponse:
169173
"""
170174
Download segmentation as NIfTI.
@@ -187,7 +191,7 @@ async def get_segmentation(
187191
# Log audit
188192
await log_patient_data_access(
189193
db=db,
190-
user_id=1, # TODO: Get from auth context
194+
user_id=current_user.id,
191195
case_id=case_id,
192196
)
193197

@@ -210,6 +214,7 @@ async def get_segmentation(
210214
async def get_report(
211215
case_id: str,
212216
db: AsyncSession = Depends(get_db),
217+
current_user: User = Depends(verify_auth),
213218
) -> FileResponse:
214219
"""
215220
Download PDF report.
@@ -232,7 +237,7 @@ async def get_report(
232237
# Log audit
233238
await log_patient_data_access(
234239
db=db,
235-
user_id=1, # TODO: Get from auth context
240+
user_id=current_user.id,
236241
case_id=case_id,
237242
)
238243

@@ -255,6 +260,7 @@ async def get_report(
255260
async def get_dicom_seg(
256261
case_id: str,
257262
db: AsyncSession = Depends(get_db),
263+
current_user: User = Depends(verify_auth),
258264
) -> FileResponse:
259265
"""
260266
Stream DICOM-SEG file.
@@ -277,7 +283,7 @@ async def get_dicom_seg(
277283
# Log audit
278284
await log_patient_data_access(
279285
db=db,
280-
user_id=1, # TODO: Get from auth context
286+
user_id=current_user.id,
281287
case_id=case_id,
282288
)
283289

@@ -300,6 +306,7 @@ async def get_dicom_seg(
300306
async def trigger_segmentation(
301307
case_id: str,
302308
db: AsyncSession = Depends(get_db),
309+
current_user: User = Depends(verify_auth),
303310
) -> Dict[str, Any]:
304311
"""
305312
Trigger segmentation for a case.
@@ -327,6 +334,7 @@ async def trigger_longitudinal(
327334
case_id: str,
328335
prior_id: str,
329336
db: AsyncSession = Depends(get_db),
337+
current_user: User = Depends(verify_auth),
330338
) -> Dict[str, Any]:
331339
"""
332340
Trigger longitudinal comparison between current and prior case.
@@ -357,6 +365,7 @@ async def trigger_longitudinal(
357365
async def delete_case(
358366
case_id: str,
359367
db: AsyncSession = Depends(get_db),
368+
current_user: User = Depends(verify_auth),
360369
) -> Dict[str, Any]:
361370
"""
362371
Soft delete a case (audit trail preserved).
@@ -380,7 +389,7 @@ async def delete_case(
380389
# Log audit
381390
await log_patient_data_modification(
382391
db=db,
383-
user_id=1, # TODO: Get from auth context
392+
user_id=current_user.id,
384393
case_id=case_id,
385394
action="delete",
386395
old_values={"status": case.status},

0 commit comments

Comments
 (0)