apk-scanner / app /security.py
Aniket2006
feat: add copilot (RAG chat) endpoint alongside apk-scan and smishing
2dca8fb
Raw
History Blame Contribute Delete
2.38 kB
"""
security.py — Shared-secret API key auth and Redis-backed rate limiting.
ShieldYONO has no user accounts yet (the Flutter app is anonymous — see
AppConstants.guardianCallUserId), so this is a single shared-secret gate
between "anyone on the internet" and the API, not per-user auth. It exists
to stop the backend being a completely open APK-upload proxy; it is NOT a
substitute for real auth once accounts exist.
settings.API_KEY ships with a placeholder default so the app keeps working
out of the box — rotate it via the API_KEY env var before any real
deployment, and keep the Flutter build's --dart-define=API_KEY in sync.
"""
from __future__ import annotations
import logging
from fastapi import Header, HTTPException, Request, status
from app.config import get_settings
logger = logging.getLogger(__name__)
async def require_api_key(x_api_key: str | None = Header(default=None, alias="X-API-Key")) -> None:
settings = get_settings()
if not settings.API_KEY:
# Explicitly disabled by the operator (empty env var) — no gate.
return
if x_api_key != settings.API_KEY:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing API key.")
def rate_limit(limit: int = 30, window_seconds: int = 60):
"""Dependency factory: per-IP fixed-window rate limit backed by Redis.
Fails open (no limiting) if Redis is unavailable, matching this
codebase's existing "Redis is a best-effort cache, not a hard
dependency" pattern (see main.py lifespan).
"""
async def _dependency(request: Request) -> None:
redis = getattr(request.app.state, "redis", None)
if redis is None:
return
client_ip = request.client.host if request.client else "unknown"
key = f"ratelimit:{request.url.path}:{client_ip}"
try:
count = await redis.incr(key)
if count == 1:
await redis.expire(key, window_seconds)
except Exception as exc:
logger.warning("Rate limiter unavailable, failing open: %s", exc)
return
if count > limit:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Rate limit exceeded ({limit} requests / {window_seconds}s). Try again shortly.",
)
return _dependency