2. Aislamiento multi-tenant
El módulo más crítico de este plan. Verifica que el fix de seguridad de octubre 2026 (company_id resuelto exclusivamente desde el token Paseto, nunca del cliente) funciona en la práctica — no solo que el código compile, sino que un usuario autenticado de la Empresa A no pueda, bajo ninguna circunstancia, ver, listar, editar ni borrar datos de la Empresa B.
Historial de ejecución
| Ciclo | Fecha | Ejecutado por | Casos ejecutados | Resultado |
|---|---|---|---|---|
| 1 | — | — | — | Pendiente |
Preparación (una sola vez por ciclo)
- Registrar Empresa A (
admin.a@test.com) y verificar la cuenta. - Registrar Empresa B (
admin.b@test.com) y verificar la cuenta. - En cada empresa, crear: una tienda, una categoría, un impuesto, un producto, un cliente (contacto) y una venta simple con ese producto. Esto puebla
stores,categories,taxes,products,inventories,contacts,sales,sale_itemsypaymentsen ambas empresas. - Anotar los IDs numéricos exactos que devolvió el backend al crear cada recurso en la Empresa A (ej. “producto id=14”, “venta id=7”) — se necesitan para los casos de “acceso directo por ID” contra la Empresa B.
Caso base por módulo
Para cada módulo de la tabla, se ejecutan tres variantes con la sesión de la Empresa B:
- (a) Listado:
GETde la lista del recurso — no debe aparecer nada de la Empresa A. - (b) Lectura directa por ID:
GET /recurso/:idusando el ID real de un recurso de la Empresa A anotado en el paso 4 — debe responder 404 (nunca 403 con datos parciales, nunca 200 con el recurso). - (c) Escritura por ID:
PUT/DELETE /recurso/:idsobre ese mismo ID ajeno — debe fallar igual que (b), y los datos de la Empresa A no deben cambiar (verificar entrando con la sesión A después).
| Caso | Módulo | Recurso usado para (b)/(c) | Resultado esperado |
|---|---|---|---|
| MT-01 | sales | venta de A | 404 en (b) y (c); venta de A intacta |
| MT-02 | payments | pago de A (asociado a la venta de A) | 404 en (b)/(c) |
| MT-03 | products | producto de A | 404 en (b)/(c); además (a) no debe listar el producto de A aunque se use GET /products sin filtro de tienda |
| MT-04 | inventory (inventories) | registro de inventario de A | 404 en (b)/(c) |
| MT-05 | categories | categoría de A | 404 en (b)/(c) |
| MT-06 | taxes | impuesto de A | 404 en (b)/(c) |
| MT-07 | contacts (clientes/proveedores) | contacto de A | 404 en (b)/(c) — este endpoint no tenía ningún filtro antes del fix de hoy; es el caso más importante de confirmar |
| MT-08 | adjustments | ajuste de inventario de A | 404 en (b)/(c) |
| MT-09 | purchases | compra de A | 404 en (b)/(c) |
| MT-10 | expenses | gasto de A | 404 en (b)/(c) |
| MT-11 | return_orders | devolución de A | 404 en (b)/(c) |
| MT-12 | bookings / booking_services | reserva y servicio de A | 404 en (b)/(c) |
| MT-13 | gift_cards | gift card de A | 404 en (b)/(c); además probar canjear/consumir saldo de la gift card de A desde B (POST /gift-cards/:id/logs) — no debe poder restarle saldo a una gift card ajena |
| MT-14 | registers | caja de A | 404 en (b)/(c) |
| MT-15 | promotions | promoción de A | 404 en (b)/(c) |
| MT-16 | discount-policies | política de descuento de A | 404 en (b)/(c) |
| MT-17 | modifier-groups | grupo de modificadores de A | 404 en (b)/(c) |
| MT-18 | custom-fields | campo personalizado de A | 404 en (b)/(c) |
| MT-19 | groups / permissions (roles) | grupo/rol de A | 404 en (b)/(c) |
| MT-20 | stores | tienda de A | 404 en (b)/(c) |
Resultado obtenido de MT-01 a MT-20: Pendiente — llenar fila por fila al ejecutar, no en bloque.
Casos especiales (no siguen el patrón de la tabla)
CP MT-21: Dashboard sin filtro de tienda
- Pasos: sesión de Empresa B →
GET /api/v1/dashboardsin parámetrostore_id.- Resultado esperado: totales de ventas/gastos/compras/ventas recientes solo de la Empresa B — en cero si B no ha creado ventas ese mes, nunca sumando lo de A.
- Resultado obtenido: Pendiente.
CP MT-22: Reportes (los 14 endpoints de
/api/v1/reports/*) sin filtro de tienda
- Pasos: probar al menos
reports/sales,reports/customers(el que no tenía ningún filtro antes del fix, ni siquiera por tienda) yreports/registersdesde la sesión de B.- Resultado esperado: cero filas o solo las de B; nunca una fila de A.
- Resultado obtenido: Pendiente.
CP MT-23: Activity log (auditoría)
- Pasos: generar varias acciones registradas en la Empresa A (crear venta, crear usuario) → desde B,
GET /api/v1/activity-logs.- Resultado esperado: B no ve ninguna entrada de A.
- Resultado obtenido: Pendiente.
CP MT-24: Asistente IA —
get_inventoryycreate_customer
- Pasos: desde el chat de la Empresa B, pedirle al asistente “muéstrame el inventario” (sin mencionar tienda) y “crea un cliente llamado Juan Pérez”.
- Resultado esperado: el inventario mostrado es solo el de B; el cliente creado debe quedar asociado a la Empresa B (verificar en Ajustes → Clientes de B, y confirmar que no aparece en el listado de clientes de A).
- Resultado obtenido: Pendiente.
CP MT-25: Endpoint de cocina (KDS) — confirmar que el fix no rompió la función real
- Objetivo:
PATCH /api/v1/sales/:id/kitchen-statuses intencionalmente público (pantallas de cocina sin login). Verificar que, aun así, solo afecta la venta correcta y no permite cambiar el estado de cocina de una venta de otra empresa si se adivina el ID.- Pasos: crear una venta
helden la Empresa A, anotar su ID → desde cualquier sesión (o sin sesión), hacerPATCH /api/v1/sales/<ese_id>/kitchen-statuscon{"kitchen_status":"ready"}.- Resultado esperado: como este endpoint es deliberadamente sin autenticación, sí debe poder cambiar el estado (es el diseño conocido: “dispositivos de cocina confiables en red local”) — pero confirmar que al menos actualiza la venta correcta (por ID) y no una al azar, y que el valor realmente cambia en la base (antes del fix de hoy, esto fallaba silenciosamente para todas las ventas).
- Resultado obtenido: Pendiente.
- Nota de riesgo a discutir con el usuario: este endpoint sigue siendo de facto “cualquiera con el ID puede cambiar el estado de cocina de cualquier venta de cualquier empresa, sin login”. Es el trade-off aceptado por diseño, pero vale la pena confirmarlo explícitamente como riesgo conocido y no como descubrimiento nuevo.
CP MT-26: Carta online pública (
/public/menu/:slug)
- Objetivo: esta ruta es pública por diseño (clientes finales sin cuenta). Confirmar que el
slugde una tienda solo expone el menú de esa tienda, nunca datos de otra tienda/empresa aunque se manipule elstore_iden el body del pedido.- Pasos: tomar el
menu_slugde una tienda de la Empresa A → hacer un pedido (POST /public/menu/:slug/orders) enviando en el body unstore_idque en realidad pertenece a una tienda de la Empresa B.- Resultado esperado: el pedido debe quedar asociado a la tienda/empresa real del
slugde la URL, ignorando cualquierstore_idque venga en el body del cliente.- Resultado obtenido: Pendiente.
Notas de usabilidad y sugerencias
Se completa durante la ejecución. Aquí también se debe anotar si algún mensaje de error “filtra” información (ej. un 403 con detalle en vez de un 404 genérico, que le confirmaría a un atacante que el ID existe pero es de otra empresa).