feat: enhance backend security and configuration

- Updated Dockerfile to improve security with a non-root user and added health checks.
- Modified docker-compose.yml to set containers as read-only, restrict ports to localhost, and implement health checks.
- Enhanced .env.example with additional environment variables for security and configuration.
- Improved FastAPI application with middleware for security headers, CORS, and body size limits.
- Refactored authentication flow in auth.py to include state validation and improved error handling.
- Added rate limiting to various endpoints to prevent abuse.
- Updated researcher and publication handling to ensure better validation and error management.
This commit is contained in:
Mireya Cueto Garrido
2026-05-08 11:19:52 +02:00
parent 96e58dbd16
commit af1b8e9956
37 changed files with 1375 additions and 282 deletions
+119 -33
View File
@@ -1,68 +1,154 @@
from fastapi import Depends, FastAPI
"""
Entry point del backend FastAPI.
Aplica un perfil de seguridad por defecto:
- Configuración tipada (Pydantic Settings) que falla rápido en producción.
- TrustedHostMiddleware (anti Host-header injection).
- CORS con lista blanca estricta (sin `*`).
- Body size limit (anti DoS por payload).
- Cabeceras de seguridad HTTP.
- Rate limiting (slowapi) con backend Redis si está configurado.
- Error handlers que NO filtran trazas ni internals.
"""
from __future__ import annotations
import logging
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from slowapi.errors import RateLimitExceeded
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from app.db.session import init_db
from app.db.session import get_db
from app.api.researchers import router as researchers_router
from app.api.auth import complete_oauth_login_response, router as auth_router
from app.api.export import router as export_router
from app.api.auth import router as auth_router
from app.api.auth import _complete_oauth_login
from app.schema.auth import OrcidLoginResponseSchema
from app.api.researchers import router as researchers_router
from app.core.body_size import BodySizeLimitMiddleware
from app.core.config import settings
from app.core.error_handlers import (
http_exception_handler,
sqlalchemy_exception_handler,
unhandled_exception_handler,
validation_exception_handler,
)
from app.core.logging_config import configure_logging
from app.core.rate_limit import limiter, rate_limit_exceeded_handler
from app.core.security_headers import SecurityHeadersMiddleware
from app.db.session import get_db, init_db
from app.scheduler.sync_scheduler import start_scheduler
from app.schema.auth import OrcidLoginResponseSchema
configure_logging()
logger = logging.getLogger("app.main")
# ---------------------------------------------------------
# Crear instancia principal de FastAPI
# ---------------------------------------------------------
app = FastAPI(
title="ORCID SWORD Backend",
description="Backend para sincronización ORCID y exportación SWORD",
version="1.0.0"
version="1.0.0",
docs_url=settings.docs_url,
redoc_url=settings.redoc_url,
openapi_url=settings.openapi_url,
)
# ---------------------------------------------------------
# Crear tablas al iniciar la aplicación
# Middlewares (orden importa: el último añadido es el más externo)
# ---------------------------------------------------------
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)
app.add_middleware(SecurityHeadersMiddleware, settings=settings)
app.add_middleware(
BodySizeLimitMiddleware,
max_bytes=settings.MAX_REQUEST_BODY_BYTES,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=[
"Authorization",
"Content-Type",
"Accept",
"Origin",
"X-Requested-With",
settings.API_KEY_NAME,
],
expose_headers=["Content-Disposition", "X-RateLimit-Remaining", "X-RateLimit-Reset"],
max_age=600,
)
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=settings.TRUSTED_HOSTS,
)
# ---------------------------------------------------------
# Exception handlers
# ---------------------------------------------------------
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
app.add_exception_handler(Exception, unhandled_exception_handler)
# ---------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------
@app.on_event("startup")
def startup_event():
init_db() # 🔥 CREA TABLAS
start_scheduler() # 🔥 INICIA SCHEDULER
def on_startup() -> None:
init_db()
start_scheduler()
logger.info(
"Backend ready (env=%s, docs=%s)",
settings.ENVIRONMENT,
bool(settings.DOCS_ENABLED),
)
# ---------------------------------------------------------
# Healthcheck
# ---------------------------------------------------------
@app.get("/health")
def health():
def health() -> dict[str, str]:
return {"status": "ok"}
# ---------------------------------------------------------
# Alias del callback OAuth (mismo flujo, mismo endurecimiento)
# ---------------------------------------------------------
@app.get("/callback", response_model=OrcidLoginResponseSchema)
def oauth_callback_root(code: str, db: Session = Depends(get_db)):
def oauth_callback_root(
request: Request,
code: str,
state: str | None = None,
db: Session = Depends(get_db),
):
"""
Alias para probar redirect URIs como `https://127.0.0.1/callback` en local.
Intercambia el code con ORCID y emite el JWT.
Alias para integraciones que registran un redirect_uri tipo
`https://<host>/callback` en ORCID.
"""
return _complete_oauth_login(code=code, db=db)
return complete_oauth_login_response(request=request, code=code, state=state, db=db)
# ---------------------------------------------------------
# Registrar routers
# Routers
# ---------------------------------------------------------
app.include_router(researchers_router, prefix="/api")
app.include_router(export_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
# ---------------------------------------------------------
# CORS
# ---------------------------------------------------------
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # en producción limitar
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)