feat(backend): durcit la surface exposée
En-têtes de sécurité, CORS resserré, caviardage des journaux, `/metrics` derrière un jeton facultatif, documentation fermée en préproduction, et la sonde de disponibilité cesse de publier la version de TimescaleDB. HSTS et CSP sont volontairement absents : l'application ignore si TLS termine devant elle, et une CSP sur une API JSON ne protège presque rien. Les deux appartiennent au terminateur TLS, celle qui compte protège la page Angular. `/metrics` est gardé par un jeton statique et non par un rôle : coupler la supervision au modèle d'utilisateurs casserait la collecte à chaque panne d'authentification, c'est-à-dire quand on en a le plus besoin. Le contrôle principal reste le réseau. Le caviardage est la troisième ligne de défense, pas la première. On ne passe aucun secret au logger et aucun jeton dans une URL ; le filtre rattrape ce que personne n'a relu, à commencer par l'écho SQL qui publiait les empreintes Argon2 quand `debug` est actif. Corrige un défaut que le test a révélé : `create_app(settings)` ne pilotait que la construction, les dépendances continuaient de lire `get_settings()` depuis l'environnement. Un test « en production » ne testait donc pas la production, et `TESTING.md` promet le contraire.
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
|
||||
from app.api.errors import register_error_handlers
|
||||
from app.api.middleware import SecurityHeadersMiddleware
|
||||
from app.api.security import require_metrics_token
|
||||
from app.api.v1.router import api_router
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.logging import configure_logging, get_logger
|
||||
@@ -13,6 +15,9 @@ from app.db.session import get_engine
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
METHODES_AUTORISEES = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
|
||||
EN_TETES_AUTORISES = ["Authorization", "Content-Type"]
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
@@ -28,30 +33,46 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
resolved = settings or get_settings()
|
||||
configure_logging(resolved)
|
||||
|
||||
documentee = resolved.api_docs_are_exposed
|
||||
application = FastAPI(
|
||||
title=resolved.name,
|
||||
version=resolved.version,
|
||||
debug=resolved.debug,
|
||||
lifespan=lifespan,
|
||||
docs_url=None if resolved.is_production else "/docs",
|
||||
redoc_url=None if resolved.is_production else "/redoc",
|
||||
openapi_url=None if resolved.is_production else "/openapi.json",
|
||||
docs_url="/docs" if documentee else None,
|
||||
redoc_url="/redoc" if documentee else None,
|
||||
openapi_url="/openapi.json" if documentee else None,
|
||||
)
|
||||
|
||||
application.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
if resolved.allowed_origins:
|
||||
# Méthodes et en-têtes listés plutôt que joker : avec `allow_credentials`, la liste
|
||||
# d'origines devient l'unique contrôle, autant documenter le contrat exact.
|
||||
application.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=resolved.allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_methods=METHODES_AUTORISEES,
|
||||
allow_headers=EN_TETES_AUTORISES,
|
||||
expose_headers=["Retry-After"],
|
||||
max_age=600,
|
||||
)
|
||||
|
||||
register_error_handlers(application)
|
||||
|
||||
Instrumentator().instrument(application).expose(
|
||||
application, endpoint="/metrics", include_in_schema=False
|
||||
application,
|
||||
endpoint="/metrics",
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(require_metrics_token)],
|
||||
)
|
||||
application.include_router(api_router, prefix=resolved.api_prefix)
|
||||
|
||||
# Piège : sans cette surcharge, une configuration passée à `create_app()` ne piloterait
|
||||
# que la construction, et les dépendances continueraient de lire `get_settings()` depuis
|
||||
# l'environnement. Un test « en production » ne testerait alors pas la production.
|
||||
if settings is not None:
|
||||
application.dependency_overrides[get_settings] = lambda: resolved
|
||||
|
||||
return application
|
||||
|
||||
Reference in New Issue
Block a user