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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user