diff --git a/backend/api/v1/modules/a76/audit_log/events.py b/backend/api/v1/modules/a76/audit_log/events.py index 3a790310..28f55422 100644 --- a/backend/api/v1/modules/a76/audit_log/events.py +++ b/backend/api/v1/modules/a76/audit_log/events.py @@ -9,7 +9,12 @@ from sqlalchemy.orm import Session from api.v1.modules.a76.general_catalogs.company.models import Company from api.v1.modules.a76.invoices.models import InvoiceHeader -from core.database import rls_company_var, rls_tenant_var +from core.database import ( + RLS_COMPANY_KEY, + RLS_TENANT_KEY, + rls_company_var, + rls_tenant_var, +) from .services.service import AuditService from .utils.serialization import serialize_for_json @@ -56,13 +61,17 @@ def _resolve_audit_company_tenant(session: Session, target) -> tuple: if company_id is not None: resolution_source = "company_self_id" if company_id is None: - company_id = rls_company_var.get() + company_id = session.info.get(RLS_COMPANY_KEY) + if company_id is None: + company_id = rls_company_var.get() if company_id is not None: resolution_source = "rls_context" tenant_id = getattr(target, "tenant_id", None) if tenant_id is None: - tenant_id = rls_tenant_var.get() + tenant_id = session.info.get(RLS_TENANT_KEY) + if tenant_id is None: + tenant_id = rls_tenant_var.get() if tenant_id is not None and resolution_source == "target": resolution_source = "rls_context" diff --git a/backend/api/v1/modules/a76/invoices/routes.py b/backend/api/v1/modules/a76/invoices/routes.py index f5428197..54fb72d0 100644 --- a/backend/api/v1/modules/a76/invoices/routes.py +++ b/backend/api/v1/modules/a76/invoices/routes.py @@ -3,6 +3,7 @@ from typing import Dict, Any, Optional from core.config import settings from core.database import get_core_db +from core.exceptions import BaseAPIException from core.security import collect_user_role_names, get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query, Path from sqlalchemy import func, or_, and_ @@ -69,6 +70,8 @@ def get_creation_data( return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id) except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("get_creation_data failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al cargar datos de creación: {str(e)}") @@ -97,6 +100,8 @@ def get_edition_data( return data except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("get_edition_data failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al cargar datos de edición: {str(e)}") @@ -191,6 +196,8 @@ def create_invoice( return services.InvoiceService.create(db, data, tenant_id, company_id) except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("create_invoice failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al guardar factura: {str(e)}") @@ -337,6 +344,8 @@ def list_invoices( "page": page, "page_size": page_size } + except BaseAPIException: + raise except Exception as e: logger.exception("list_invoices failed: %s", e) raise HTTPException(status_code=500, detail=f"Internal server error in invoices list: {str(e)}") diff --git a/backend/api/v1/modules/core/tasks_tracking/dispatch.py b/backend/api/v1/modules/core/tasks_tracking/dispatch.py index 99ea371f..ac25b8e7 100644 --- a/backend/api/v1/modules/core/tasks_tracking/dispatch.py +++ b/backend/api/v1/modules/core/tasks_tracking/dispatch.py @@ -3,7 +3,7 @@ from typing import Any from celery import Task from sqlalchemy.orm import Session -from core.database import reset_rls_context_tokens, rls_company_var, rls_tenant_var +from core.database import rls_company_var, rls_tenant_var from .service import TaskTrackerService @@ -29,8 +29,10 @@ def track_and_dispatch( if company_id is not None: headers["rls_company_id"] = str(int(company_id)) - token_t = rls_tenant_var.set(int(tenant_id)) - token_c = rls_company_var.set(int(company_id) if company_id is not None else None) + prev_tenant = rls_tenant_var.get() + prev_company = rls_company_var.get() + rls_tenant_var.set(int(tenant_id)) + rls_company_var.set(int(company_id) if company_id is not None else None) try: celery_task = task.apply_async( args=args or [], @@ -39,7 +41,8 @@ def track_and_dispatch( headers=headers, ) finally: - reset_rls_context_tokens(token_t, token_c) + rls_tenant_var.set(prev_tenant) + rls_company_var.set(prev_company) tracker = TaskTrackerService(db) tracker.register_dispatch( diff --git a/backend/core/database.py b/backend/core/database.py index f794f11a..f88412a8 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -154,12 +154,13 @@ def get_core_db(request: Request = None) -> Generator[Session, None, None]: escriben ``db: Session = Depends(get_core_db)`` siguen funcionando sin cambios porque ``Request`` se resuelve en la capa de dependencia. - Replica el mismo ``(tenant_id, company_id)`` en ContextVars para código que - comparte la transacción sin la misma instancia de sesión (p. ej. listeners). + No se escriben las ContextVars de RLS aquí: las dependencias síncronas con + ``yield`` se ejecutan vía ``contextmanager_in_threadpool`` (hilo worker) y + mezclar ``ContextVar.set`` / ``reset`` entre ese hilo y el bucle asyncio + provoca ``ValueError: ... was created in a different Context``. El aislamiento + RLS se aplica con ``session.info`` (véase ``after_begin`` y audit listeners). """ tenant_id, company_id = _extract_rls_context(request) - token_t = rls_tenant_var.set(tenant_id) - token_c = rls_company_var.set(company_id) db = CoreSessionLocal() db.info[RLS_TENANT_KEY] = tenant_id db.info[RLS_COMPANY_KEY] = company_id @@ -167,14 +168,15 @@ def get_core_db(request: Request = None) -> Generator[Session, None, None]: yield db finally: db.close() - reset_rls_context_tokens(token_t, token_c) async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSession, None]: """Dependency async para obtener sesión con contexto RLS.""" tenant_id, company_id = _extract_rls_context(request) - token_t = rls_tenant_var.set(tenant_id) - token_c = rls_company_var.set(company_id) + prev_tenant = rls_tenant_var.get() + prev_company = rls_company_var.get() + rls_tenant_var.set(tenant_id) + rls_company_var.set(company_id) try: async with AsyncCoreSessionLocal() as session: session.info[RLS_TENANT_KEY] = tenant_id @@ -184,7 +186,8 @@ async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSess finally: await session.close() finally: - reset_rls_context_tokens(token_t, token_c) + rls_tenant_var.set(prev_tenant) + rls_company_var.set(prev_company) @contextmanager diff --git a/backend/core/middleware.py b/backend/core/middleware.py index f6150a23..1bb5836d 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -178,12 +178,12 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): } ) except Exception as e: - logger.error(f"Hub verify-license request failed: {str(e)}") + logger.exception("Hub verify-license request failed: %s", e) return JSONResponse( status_code=500, content={ "error": "VALIDATION_ERROR", - "message": "Error interno al validar licencia con el Hub.", + "message": "Error interno al contactar el servicio de licencias.", "status_code": 500, } ) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0edc6fd1..45064db3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -346,6 +346,8 @@ services: - NODE_ENV=${NODE_ENV:-production} - VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api} - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} + - INTERNAL_HUB_URL=https://workspace.aduanasoft.com + - HUB_URL=https://workspace.aduanasoft.com - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-https://anexo76-dev.aduanasoft.com/kcauth/} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend}