diff --git a/backend/api/v1/modules/core/permissions/service.py b/backend/api/v1/modules/core/permissions/service.py index b31c8d0..256aeb1 100644 --- a/backend/api/v1/modules/core/permissions/service.py +++ b/backend/api/v1/modules/core/permissions/service.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Set, Optional, List from sqlalchemy.orm import Session -from sqlalchemy import and_, or_ +from sqlalchemy import and_, or_, text from core.database import RLS_TENANT_KEY from .cache import PermissionCache @@ -51,8 +51,18 @@ class PermissionService: return None try: - # Sin modelo de compañía en la plantilla — implementa la consulta aquí. - pass + # La tabla de compañías del CRM es ``a76.company`` (esquema legado). No tiene modelo + # ORM en este proyecto, así que se consulta con SQL crudo, igual que ``seed_crm.py``. + # + # Sin este respaldo la función devolvía None siempre que la sesión no traía contexto + # RLS, y con ella se scopean las consultas de permisos de los cinco llamadores de + # abajo: el efecto neto era que nadie resolvía permisos. + row = self.db.execute( + text("SELECT tenant_id FROM a76.company WHERE id = :company_id"), + {"company_id": company_id}, + ).first() + if row is not None and row[0] is not None: + return int(row[0]) except Exception as exc: logger.warning( "resolve_tenant_id_for_company_failed", @@ -470,8 +480,35 @@ class PermissionService: sync_res = self.sync_permissions() logger.info(f"Bootstrap: Sincronización completa. {sync_res.get('synced', 0)} nuevos, {sync_res.get('total_registered', 0)} totales.") - # 1. Obtener el tenant_id (implementa con tu modelo de compañía) - tenant_id = self.db.info.get(RLS_TENANT_KEY) or 1 + # 1. tenant_id del rol: se toma de la COMPAÑÍA, que es su fuente autoritativa. + # ``company_roles`` referencia a la vez a ``a76.company`` y a ``core.tenants``, así + # que el tenant del rol tiene que ser el de su compañía o la fila queda cruzada + # entre dos tenants. + # + # Antes esto era ``self.db.info.get(RLS_TENANT_KEY) or 1``. Cuando la sesión no + # traía contexto RLS —justo el caso de ``/permissions/me`` en el primer acceso— el + # rol se creaba con ``tenant_id=1``; en una instalación real ese tenant no existe y + # el INSERT moría con ForeignKeyViolation. El bootstrap quedaba a medias, sin rol + # ni permisos, toda la API respondía 403 y ``/permissions/me`` seguía devolviendo + # 200 con la lista vacía: el fallo se leía en pantalla como "no tienes permisos" + # en lugar de como el error de configuración que era. + # Se consulta la compañía DIRECTAMENTE y no vía ``_resolve_tenant_id_for_company``: + # ese helper prefiere el contexto RLS, que es el tenant del REQUEST y puede no ser + # el de la compañía. Para leer permisos esa preferencia está bien y ahorra una + # consulta en el camino caliente; para escribir una fila atada por FK a las dos + # tablas, no: si difirieran, el rol nacería cruzado. + _row = self.db.execute( + text("SELECT tenant_id FROM a76.company WHERE id = :company_id"), + {"company_id": company_id}, + ).first() + if _row is None or _row[0] is None: + logger.error( + "Bootstrap: la compañía %s no existe en a76.company, no hay tenant al que " + "colgar el rol. Se aborta sin crear nada.", + company_id, + ) + return False + tenant_id = int(_row[0]) # 2. Buscar si ya existe el rol "super_admin" admin_role = self.db.query(CompanyRole).filter(