AuctionRouter / backend /app /security.py
dakshtaneja's picture
deploy hardening: access gate, rate limiting, spend guard, env CORS
98d4633
Raw
History Blame Contribute Delete
2.51 kB
"""Abuse protection: access-code gate, per-IP rate limiting, spend guard.
The API key is a server-side secret and is never sent to the browser, so it
cannot be stolen — the threat is abuse of the endpoints that *spend* it. These
layers bound the worst case; the true backstop is a credit-capped OpenRouter
key (provider-enforced, outside this code).
"""
import hmac
from datetime import datetime, timezone
from fastapi import Header, HTTPException, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from .config import settings
def _client_ip(request: Request) -> str:
"""Real client IP behind HF Spaces' / Vercel's proxy."""
fwd = request.headers.get("x-forwarded-for")
if fwd:
return fwd.split(",")[0].strip()
return get_remote_address(request)
limiter = Limiter(key_func=_client_ip)
# Rate strings shared by the query endpoints' decorators
RATE_LIMITS = f"{settings.rate_limit_per_min}/minute;{settings.rate_limit_per_day}/day"
async def require_access(x_access_code: str = Header(default="")) -> None:
"""Dependency: gate every /api/* route behind the shared access code.
Empty ACCESS_CODE disables the gate (local dev). Constant-time compare so
the check can't be timing-probed.
"""
expected = settings.access_code
if not expected:
return
if not hmac.compare_digest(x_access_code, expected):
raise HTTPException(status_code=401, detail="Invalid or missing access code")
class SpendGuard:
"""In-memory daily spend ceiling (UTC day). Resets on restart, which is
fine: the credit-capped OpenRouter key is the real backstop, this is just
the graceful early stop."""
def __init__(self) -> None:
self._day = ""
self._spent = 0.0
def _roll(self) -> None:
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if today != self._day:
self._day, self._spent = today, 0.0
def check(self) -> None:
"""Raise 503 if today's spend is already over the limit."""
self._roll()
if self._spent >= settings.daily_spend_limit_usd:
raise HTTPException(
status_code=503,
detail="Daily budget reached — try again tomorrow.",
)
def add(self, cost_usd: float) -> None:
self._roll()
self._spent += max(cost_usd, 0.0)
@property
def spent_today(self) -> float:
self._roll()
return self._spent
spend_guard = SpendGuard()