fix(core): el bootstrap de permisos colgaba el rol de un tenant inexistente

Al entrar por primera vez a una compañia, /permissions/me creaba el rol super_admin
con `tenant_id = self.db.info.get(RLS_TENANT_KEY) or 1`. La sesion de ese request no
trae contexto RLS, asi que caia en el respaldo: tenant_id = 1. En una instalacion
real ese tenant no existe -- aqui son 11 y 17 -- y el INSERT moria con
ForeignKeyViolation sobre company_roles_tenant_id_fkey.

El fallo era silencioso hacia afuera: el except del bootstrap lo registraba como
ERROR CRITICO y devolvia False, pero /permissions/me seguia respondiendo 200 con la
lista de permisos VACIA. En pantalla se leia "No tienes permisos para realizar esta
accion", que manda a revisar roles en vez de la base. Toda la API respondia 403.

Dos arreglos:

  - `_resolve_tenant_id_for_company` tenia el respaldo sin implementar (`pass` con un
    comentario de plantilla), asi que devolvia None siempre que faltara el contexto
    RLS. Con esa funcion se scopean las consultas de sus CINCO llamadores, o sea que
    la lectura de permisos tampoco resolvia. Ahora consulta a76.company, la tabla de
    companias del CRM, con SQL crudo igual que seed_crm.py.

  - bootstrap_super_admin toma el tenant de la COMPANIA, con consulta directa y no
    por el helper: el helper prefiere el contexto RLS, que es el tenant del REQUEST y
    puede no ser el de la compania. Para leer permisos esa preferencia esta bien y
    ahorra una consulta en el camino caliente; para escribir una fila atada por FK a
    a76.company y a core.tenants a la vez, no -- si difirieran, el rol naceria
    cruzado entre dos tenants. Si la compania no existe, aborta sin crear nada en vez
    de inventar un tenant.

Verificado contra la base: bootstrap devuelve True en las dos companias del usuario,
70 permisos en cada una, y las filas quedan consistentes (rol de company 1 -> tenant
17, company 2 -> tenant 11). Suite sin regresion: 151 pasan.

Ref: T2026-08-046

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 10:58:56 -06:00
parent 5c4df590d4
commit d1fcd236e4

View File

@@ -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(