Merge branch 'development' into feature/api-sittar-actualizacion

This commit is contained in:
2026-05-08 14:39:24 -06:00
6 changed files with 43 additions and 17 deletions

View File

@@ -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.general_catalogs.company.models import Company
from api.v1.modules.a76.invoices.models import InvoiceHeader 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 .services.service import AuditService
from .utils.serialization import serialize_for_json 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: if company_id is not None:
resolution_source = "company_self_id" resolution_source = "company_self_id"
if company_id is None: 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: if company_id is not None:
resolution_source = "rls_context" resolution_source = "rls_context"
tenant_id = getattr(target, "tenant_id", None) tenant_id = getattr(target, "tenant_id", None)
if tenant_id is 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": if tenant_id is not None and resolution_source == "target":
resolution_source = "rls_context" resolution_source = "rls_context"

View File

@@ -3,6 +3,7 @@ from typing import Dict, Any, Optional
from core.config import settings from core.config import settings
from core.database import get_core_db 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 core.security import collect_user_role_names, get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query, Path from fastapi import APIRouter, Depends, HTTPException, Query, Path
from sqlalchemy import func, or_, and_ from sqlalchemy import func, or_, and_
@@ -69,6 +70,8 @@ def get_creation_data(
return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id) return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id)
except HTTPException: except HTTPException:
raise raise
except BaseAPIException:
raise
except Exception as e: except Exception as e:
logger.exception("get_creation_data failed: %s", 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)}") 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 return data
except HTTPException: except HTTPException:
raise raise
except BaseAPIException:
raise
except Exception as e: except Exception as e:
logger.exception("get_edition_data failed: %s", 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)}") 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) return services.InvoiceService.create(db, data, tenant_id, company_id)
except HTTPException: except HTTPException:
raise raise
except BaseAPIException:
raise
except Exception as e: except Exception as e:
logger.exception("create_invoice failed: %s", e) logger.exception("create_invoice failed: %s", e)
raise HTTPException(status_code=500, detail=f"Error al guardar factura: {str(e)}") raise HTTPException(status_code=500, detail=f"Error al guardar factura: {str(e)}")
@@ -337,6 +344,8 @@ def list_invoices(
"page": page, "page": page,
"page_size": page_size "page_size": page_size
} }
except BaseAPIException:
raise
except Exception as e: except Exception as e:
logger.exception("list_invoices failed: %s", e) logger.exception("list_invoices failed: %s", e)
raise HTTPException(status_code=500, detail=f"Internal server error in invoices list: {str(e)}") raise HTTPException(status_code=500, detail=f"Internal server error in invoices list: {str(e)}")

View File

@@ -3,7 +3,7 @@ from typing import Any
from celery import Task from celery import Task
from sqlalchemy.orm import Session 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 from .service import TaskTrackerService
@@ -29,8 +29,10 @@ def track_and_dispatch(
if company_id is not None: if company_id is not None:
headers["rls_company_id"] = str(int(company_id)) headers["rls_company_id"] = str(int(company_id))
token_t = rls_tenant_var.set(int(tenant_id)) prev_tenant = rls_tenant_var.get()
token_c = rls_company_var.set(int(company_id) if company_id is not None else None) 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: try:
celery_task = task.apply_async( celery_task = task.apply_async(
args=args or [], args=args or [],
@@ -39,7 +41,8 @@ def track_and_dispatch(
headers=headers, headers=headers,
) )
finally: 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 = TaskTrackerService(db)
tracker.register_dispatch( tracker.register_dispatch(

View File

@@ -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 escriben ``db: Session = Depends(get_core_db)`` siguen funcionando sin
cambios porque ``Request`` se resuelve en la capa de dependencia. cambios porque ``Request`` se resuelve en la capa de dependencia.
Replica el mismo ``(tenant_id, company_id)`` en ContextVars para código que No se escriben las ContextVars de RLS aquí: las dependencias síncronas con
comparte la transacción sin la misma instancia de sesión (p. ej. listeners). ``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) 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 = CoreSessionLocal()
db.info[RLS_TENANT_KEY] = tenant_id db.info[RLS_TENANT_KEY] = tenant_id
db.info[RLS_COMPANY_KEY] = company_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 yield db
finally: finally:
db.close() db.close()
reset_rls_context_tokens(token_t, token_c)
async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSession, None]: async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSession, None]:
"""Dependency async para obtener sesión con contexto RLS.""" """Dependency async para obtener sesión con contexto RLS."""
tenant_id, company_id = _extract_rls_context(request) tenant_id, company_id = _extract_rls_context(request)
token_t = rls_tenant_var.set(tenant_id) prev_tenant = rls_tenant_var.get()
token_c = rls_company_var.set(company_id) prev_company = rls_company_var.get()
rls_tenant_var.set(tenant_id)
rls_company_var.set(company_id)
try: try:
async with AsyncCoreSessionLocal() as session: async with AsyncCoreSessionLocal() as session:
session.info[RLS_TENANT_KEY] = tenant_id session.info[RLS_TENANT_KEY] = tenant_id
@@ -184,7 +186,8 @@ async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSess
finally: finally:
await session.close() await session.close()
finally: finally:
reset_rls_context_tokens(token_t, token_c) rls_tenant_var.set(prev_tenant)
rls_company_var.set(prev_company)
@contextmanager @contextmanager

View File

@@ -178,12 +178,12 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
} }
) )
except Exception as e: 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( return JSONResponse(
status_code=500, status_code=500,
content={ content={
"error": "VALIDATION_ERROR", "error": "VALIDATION_ERROR",
"message": "Error interno al validar licencia con el Hub.", "message": "Error interno al contactar el servicio de licencias.",
"status_code": 500, "status_code": 500,
} }
) )

View File

@@ -346,6 +346,8 @@ services:
- NODE_ENV=${NODE_ENV:-production} - NODE_ENV=${NODE_ENV:-production}
- VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api} - VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api}
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/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_URL=${VITE_KEYCLOAK_URL:-https://anexo76-dev.aduanasoft.com/kcauth/}
- VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master}
- VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend}