Complete Section 1 security/auth hardening

This commit is contained in:
dwindown
2026-04-30 11:35:56 +07:00
parent 432ffbcdb9
commit 12d2d9458f
15 changed files with 863 additions and 232 deletions

View File

@@ -7,14 +7,15 @@ Endpoints:
- GET /tryout: List tryouts for a website
"""
from typing import List, Optional
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Header, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import Integer, cast, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.core.auth import AuthContext, get_auth_context, require_website_auth
from app.models.item import Item
from app.models.tryout import Tryout
from app.models.tryout_stats import TryoutStats
@@ -29,35 +30,6 @@ from app.schemas.tryout import (
router = APIRouter(prefix="/tryout", tags=["tryouts"])
def get_website_id_from_header(
x_website_id: Optional[str] = Header(None, alias="X-Website-ID"),
) -> int:
"""
Extract and validate website_id from request header.
Args:
x_website_id: Website ID from header
Returns:
Validated website ID as integer
Raises:
HTTPException: If header is missing or invalid
"""
if x_website_id is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="X-Website-ID header is required",
)
try:
return int(x_website_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="X-Website-ID must be a valid integer",
)
@router.get(
"/{tryout_id}/config",
response_model=TryoutConfigResponse,
@@ -67,7 +39,7 @@ def get_website_id_from_header(
async def get_tryout_config(
tryout_id: str,
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
) -> TryoutConfigResponse:
"""
Get tryout configuration.
@@ -78,6 +50,8 @@ async def get_tryout_config(
Raises:
HTTPException: If tryout not found
"""
website_id = require_website_auth(auth, allowed_roles={"student", "admin", "system_admin"})
# Get tryout with stats
result = await db.execute(
select(Tryout)
@@ -140,7 +114,7 @@ async def update_normalization(
tryout_id: str,
request: NormalizationUpdateRequest,
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
) -> NormalizationUpdateResponse:
"""
Update normalization settings for a tryout.
@@ -157,6 +131,8 @@ async def update_normalization(
Raises:
HTTPException: If tryout not found or validation fails
"""
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
# Get tryout
result = await db.execute(
select(Tryout).where(
@@ -214,7 +190,7 @@ async def update_normalization(
)
async def list_tryouts(
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
) -> List[TryoutConfigBrief]:
"""
List all tryouts for a website.
@@ -226,6 +202,8 @@ async def list_tryouts(
Returns:
List of TryoutConfigBrief
"""
website_id = require_website_auth(auth, allowed_roles={"student", "admin", "system_admin"})
# Get tryouts with stats
result = await db.execute(
select(Tryout)
@@ -255,7 +233,7 @@ async def list_tryouts(
async def get_calibration_status(
tryout_id: str,
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
):
"""
Get calibration status for items in a tryout.
@@ -273,6 +251,8 @@ async def get_calibration_status(
Raises:
HTTPException: If tryout not found
"""
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
# Verify tryout exists
tryout_result = await db.execute(
select(Tryout).where(
@@ -324,7 +304,7 @@ async def get_calibration_status(
async def trigger_calibration(
tryout_id: str,
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
):
"""
Trigger IRT calibration for all items in a tryout.
@@ -343,6 +323,8 @@ async def trigger_calibration(
Raises:
HTTPException: If tryout not found or calibration fails
"""
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
from app.services.irt_calibration import (
calibrate_all,
CALIBRATION_SAMPLE_THRESHOLD,
@@ -391,7 +373,7 @@ async def trigger_item_calibration(
tryout_id: str,
item_id: int,
db: AsyncSession = Depends(get_db),
website_id: int = Depends(get_website_id_from_header),
auth: AuthContext = Depends(get_auth_context),
):
"""
Trigger IRT calibration for a single item.
@@ -408,6 +390,8 @@ async def trigger_item_calibration(
Raises:
HTTPException: If tryout or item not found
"""
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
from app.services.irt_calibration import calibrate_item, CALIBRATION_SAMPLE_THRESHOLD
# Verify tryout exists