Files
kite/backend/api/cloud/cache.py
T
curo1305 299d4b523d feat(14-03): cache settings/status API and security tests
- api/cloud/cache.py: GET /analysis/cache, PATCH /analysis/cache/settings
- api/cloud/analysis.py: analysis router aggregator (Phase 14 parent)
- schemas.py: CacheStatusOut, CacheSettingsUpdateRequest (explicit allowlists)
- __init__.py: register analysis_router under /api/cloud
- 7 new security tests: admin block, unauthenticated block, object_key exclusion,
  response structure, enum validation, tier cap enforcement (T-14-01..02, T-14-08)
- All 47 plan target tests pass (test_cloud_cache.py + test_cloud_security.py)
2026-06-23 15:32:12 +02:00

138 lines
4.8 KiB
Python

"""
Cache settings and status endpoint — Phase 14.
Owner-scoped endpoints for the byte cache. Admin users are blocked by
get_regular_user (T-14-01, T-14-06).
Route family:
GET /analysis/cache — current cache usage and user settings
PATCH /analysis/cache/settings — update analysis preferences and cache limit
Security invariants (T-14-02, T-14-08):
- object_key, credentials_enc, and raw provider URLs must never appear in any
response body.
- All queries are scoped to current_user.id — no cross-user access is possible.
"""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from api.cloud.schemas import CacheStatusOut, CacheSettingsUpdateRequest
from db.models import CloudByteCacheEntry, User
from deps.auth import get_regular_user
from deps.db import get_db
from services.cloud_analysis_settings import (
DEFAULT_MAX_CACHE_LIMIT_BYTES,
get_or_create_user_analysis_settings,
update_user_analysis_settings,
)
from services.rate_limiting import account_limiter
router = APIRouter()
@router.get("/analysis/cache", response_model=CacheStatusOut)
@account_limiter.limit("120/minute")
async def get_cache_status(
request: Request,
session: AsyncSession = Depends(get_db),
current_user: User = Depends(get_regular_user),
) -> CacheStatusOut:
"""Return cache usage totals and analysis settings for the current user.
Returns aggregate byte/count totals (no object_key or credentials).
Blocked for admin accounts (T-14-01).
"""
request.state.current_user = current_user
# Aggregate active (non-evicted) cache entries for this user
result = await session.execute(
select(
func.count(CloudByteCacheEntry.id).label("entry_count"),
func.coalesce(func.sum(CloudByteCacheEntry.size_bytes), 0).label("total_bytes"),
).where(
CloudByteCacheEntry.user_id == current_user.id,
CloudByteCacheEntry.evicted_at.is_(None),
)
)
row = result.one()
entry_count = int(row.entry_count)
total_bytes = int(row.total_bytes)
# Fetch or create settings (never fails — creates defaults on first access)
settings = await get_or_create_user_analysis_settings(
session, user_id=current_user.id
)
return CacheStatusOut(
entry_count=entry_count,
total_bytes=total_bytes,
cache_limit_bytes=settings.cloud_cache_limit_bytes,
tier_cap_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES,
analysis_progress_detail=settings.analysis_progress_detail,
analysis_failure_behavior=settings.analysis_failure_behavior,
)
@router.patch("/analysis/cache/settings", response_model=CacheStatusOut)
@account_limiter.limit("30/minute")
async def update_cache_settings(
request: Request,
body: CacheSettingsUpdateRequest,
session: AsyncSession = Depends(get_db),
current_user: User = Depends(get_regular_user),
) -> CacheStatusOut:
"""Update analysis preferences and/or the user's cache byte limit.
Only provided (non-None) fields are updated. Enum and bounds validation
is performed by the service layer (raises ValueError; we re-raise as 422).
Blocked for admin accounts (T-14-01).
"""
request.state.current_user = current_user
try:
await update_user_analysis_settings(
session,
user_id=current_user.id,
analysis_progress_detail=body.analysis_progress_detail,
analysis_failure_behavior=body.analysis_failure_behavior,
cloud_cache_limit_bytes=body.cloud_cache_limit_bytes,
max_cache_limit_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES,
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(exc),
) from exc
await session.commit()
# Return fresh status after update
result = await session.execute(
select(
func.count(CloudByteCacheEntry.id).label("entry_count"),
func.coalesce(func.sum(CloudByteCacheEntry.size_bytes), 0).label("total_bytes"),
).where(
CloudByteCacheEntry.user_id == current_user.id,
CloudByteCacheEntry.evicted_at.is_(None),
)
)
row = result.one()
settings = await get_or_create_user_analysis_settings(
session, user_id=current_user.id
)
return CacheStatusOut(
entry_count=int(row.entry_count),
total_bytes=int(row.total_bytes),
cache_limit_bytes=settings.cloud_cache_limit_bytes,
tier_cap_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES,
analysis_progress_detail=settings.analysis_progress_detail,
analysis_failure_behavior=settings.analysis_failure_behavior,
)