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:
+119
-33
@@ -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=["*"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user