# Auditoría de consistencia — labSoft / lab_saas

**Fecha:** 2026-07-28  
**Proyecto:** LIMS multi-tenant (CodeIgniter 4.6.5 / PHP 8.3 / MySQL)  
**Alcance:** flujos de negocio, controladores, servicios, modelos, migraciones, endpoints JSON (AJAX), roles Shield, aislamiento multi-tenant.  
**Método:** revisión estática del código + PHPUnit (tests de tenant en MySQL; suite completa con hallazgos en SQLite).  
**Naturaleza:** informe de hallazgos + bitácora de remediación Fase A.

## 0. Bitácora remediación Fase A (aplicada 2026-07-28)

| ID | Estado | Cambio |
|----|--------|--------|
| FLOW-05 | Hecho | Email paciente/médico solo POST; GET mutantes eliminados |
| FLOW-01 | Hecho | Envío al médico usa portal `/r/{token}` (no PDF con auth) |
| MT-01 | Hecho | `asegurarPublicToken` con filtro `tenant_id` |
| MT-02 | Hecho | Ticket `Venta::datosVenta` con `vm.tenant_id` y joins tenant |

---

## 1. Resumen ejecutivo

| Dimensión | Estado |
|-----------|--------|
| Multi-tenant (lectura/escritura de negocio) | **Sólido en el núcleo** (`BaseTenantModel` + `tenant_id()` fail-closed + `AuthFilter`) |
| Mezcla de datos entre laboratorios | **No se observa fuga obvia** en el flujo autenticado normal; defensa en profundidad incompleta en algunos SQL crudos |
| Roles / privilegios | **Correcto a nivel de rutas** (`group:`); **sin matriz de permisos** fina |
| Flujos POS / lab / resultados | Coherentes en general; **inconsistencias de producto** (PDF al médico; perfiles vs reportes) |
| API | **No hay API REST** de terceros; solo JSON autenticado con CSRF en POST |
| Tests | Tenant isolation **OK en MySQL**; suite global con **2 errores** en `ShieldAuthTest` (SQLite sin `tenant_id`) |

### Conclusión

El diseño multi-tenant es adecuado para **MT-0 + MT-1** y, en uso normal, **no permite cruzar datos de otro negocio** si el usuario tiene `tenant_id` de sesión correcto.

Riesgo residual a cerrar:

1. SQL crudo sin filtro `tenant_id` en caminos secundarios.  
2. Enlaces al médico que apuntan a URLs autenticadas.  
3. Acciones que mutan estado vía **GET** (correo).  
4. Ausencia de tests E2E de autorización / cross-tenant.

---

## 2. Arquitectura revisada

```
Auth (Shield)
  → AuthFilter (sesión + tenant_bloqueado)
  → Routes: superadmin | admin | admin+empleado | admin+laboratorista
  → Controllers
  → Models (BaseTenantModel) | SQL crudo (tenant manual)
  → VentaService (transacción + tenant_id en inserts)
```

### Roles (estado real del código)

| Rol | Home real | Capacidades principales |
|-----|-----------|-------------------------|
| `superadmin` | `/super` | Tenants, feedback de plataforma |
| `admin` | `/dashboard` | CRUDs, config, reportes, POS, lab, resultados |
| `empleado` | **`/inicio`** | POS, abonos, monedero, resultados (docs a veces citan `/cotizacion`) |
| `laboratorista` | `/pendientes` → `/lab` | Tablero, captura, capturas |

### Modelos

- **Negocio (tenant-scoped):** Cliente, Venta, Muestra, Analisis, Precio, Sucursal, Abono, Quimica/Item, Perfil/Item, Medico, Categoria, ResultadoDetalle, LabComentario, Empleado → `BaseTenantModel`.
- **Plataforma:** Tenant, Feedback → `BaseAuditModel` (sin scope de lab).
- **Especiales:** `UserModel` (Shield + `tenant_id` en `users`), `FeedbackAdjuntoModel` (`Model` plano).

---

## 3. Multi-tenant

### 3.1 Controles que funcionan

1. **`BaseTenantModel`:** `find` / `findAll` / `first` / `paginate` / `count` / `update` / `delete` filtran por `tenant_id`; insert **fail-closed** sin tenant.
2. **`AuthFilter`:** usuario de lab sin `tenant_id` o tenant inactivo → logout y bloqueo.
3. **`poblar_sesion_usuario`:** fija `tenant_id` desde `users.tenant_id` y valida fila `tenant` activa.
4. **`VentaService`:** consecutivo con `FOR UPDATE` por `idsucursal` **y** `tenant_id`; inserts con `tenant_id`.
5. **Listados SQL bien acotados:** p. ej. `listarConResultadosDisponibles`, `getConSaldo`, `ReporteService`, `MuestraModel` pendientes/captura.
6. **Tests de aislamiento** (`BaseTenantModelTest`, `TenantHelperTest`, `TenantProvisionerTest`): **OK** en MySQL de desarrollo.

### 3.2 Hallazgos multi-tenant

| ID | Severidad | Hallazgo | Impacto |
|----|-----------|----------|---------|
| **MT-01** | **P1** | `VentaModel::asegurarPublicToken()` actualiza `ventas` por `idventas` **sin** `tenant_id` | Defensa en profundidad débil |
| **MT-02** | **P2** | `Empleado\Venta::datosVenta()` SQL crudo a `venta_muestra`/`muestra`/`analisis` **sin** `tenant_id` en joins | Mitigado por `find()` scoped de la venta; incompleto |
| **MT-03** | **P2** | `EnsureMultiTenantSchema` **no** conoce `perfil`, `perfil_item`, `medico`, `ventas.idmedico`, `venta_muestra.idperfil` | Drift en installs catch-up |
| **MT-04** | **P2** | Dualidad legacy `doctores`/`correodoctor` **y** nuevo `medico`/`idmedico` | Dos fuentes de verdad |
| **MT-05** | **P3** | `FeedbackAdjuntoModel` sin `tenant_id` | Bajo: solo vía feedback superadmin |
| **MT-06** | **P3** | Carrito en sesión sin clave por tenant | Bajo hoy |
| **MT-07** | **P3** | Join empleado–sucursal al poblar sesión incompleto en sucursal | Bajo si FKs OK |

### 3.3 Veredicto multi-tenant

| Pregunta | Respuesta |
|----------|-----------|
| ¿Se mezclan datos de dos labs en flujo normal autenticado? | **No, con confianza alta** |
| ¿Hay superficie residual IDOR / SQL sin tenant? | **Sí, acotada** (MT-01, MT-02) |
| ¿Hay API pública que ignore tenant? | **No** (portal usa token de venta) |

---

## 4. Roles y privilegios

### 4.1 Matriz por rutas

| Capacidad | superadmin | admin | empleado | laboratorista |
|-----------|:----------:|:-----:|:--------:|:-------------:|
| Plataforma `/super` | ✓ | | | |
| Config, análisis, personal, sucursales, reportes | | ✓ | | |
| POS / carrito / ticket | | ✓ | ✓ | |
| Alta cliente / médico en POS | | ✓ | ✓ | |
| Resultados (entrega paciente / médico) | | ✓ | ✓ | |
| Abonos / monedero | | ✓ | ✓ | |
| Lab kanban / captura | | ✓ | | ✓ |
| Feedback bandeja | ✓ | | | |
| Enviar feedback (widget) | cualquier autenticado | | | |

### 4.2 Hallazgos RBAC

| ID | Severidad | Hallazgo |
|----|-----------|----------|
| **RBAC-01** | **P2** | `permissions` / `$matrix` vacíos: solo `group:` en rutas |
| **RBAC-02** | **P3** | Empleado crea médicos en POS pero no gestiona `/medicos` |
| **RBAC-03** | **P3** | Docs home empleado `/cotizacion` vs código `/inicio` |
| **RBAC-04** | **P2** | Sin tests de 403 al cruzar roles |

---

## 5. Flujos y consistencia funcional

### 5.1 POS

```
Paciente (+ alta) → Médico opcional (+ alta) → Estudios (análisis | química | perfil)
  → Carrito (sesión) → POST /venta → VentaService → Ticket
```

| ID | Severidad | Hallazgo |
|----|-----------|----------|
| **FLOW-01** | **P1** | Envío al médico usa `resultados/pdf/{id}` (**requiere login**). Paciente usa `/r/{token}`. Doctor externo **no abre** el PDF |
| **FLOW-02** | **P2** | Perfiles en venta; reportes solo `idquimica` |
| **FLOW-03** | **P2** | Precio POS siempre columna **General** |
| **FLOW-04** | **P3** | Carrito solo sesión |

### 5.2 Resultados / portal

| ID | Severidad | Hallazgo |
|----|-----------|----------|
| **FLOW-05** | **P1** | GET de email (paciente y médico) **puede enviar correo** → sin CSRF, mutación en GET |
| **FLOW-06** | **P2** | Portal: token + reto + rate limit — razonable |
| **FLOW-07** | — | DataTables empty rowspan: ya corregido en código reciente |

### 5.3 Lab

| ID | Severidad | Hallazgo |
|----|-----------|----------|
| **FLOW-08** | OK | Tablero/captura con `tenantIdForQuery` |
| **FLOW-09** | **P3** | Validar transiciones LAB en todos los entry points |

---

## 6. Servicios y migraciones

| Servicio | Evaluación |
|----------|------------|
| `VentaService` | Crítico y acotado a tenant; paquetes + médico opcional |
| `ReporteService` | Filtra tenant; sin perfiles en ranking |
| `TenantProvisioner` | Alta tenant OK; sin seed perfiles/médicos |

**Migraciones:** ~25; perfil/medico/idmedico presentes. EnsureMT desactualizado (MT-03). Legacy `doctores` sigue (MT-04).

---

## 7. Endpoints JSON (“API”)

No hay API REST de terceros. Superficie autenticada:

| Endpoint | Roles | Notas |
|----------|-------|-------|
| `GET catalogo/buscar` | admin, empleado | Select2 |
| `POST carrito/*` | admin, empleado | CSRF |
| `GET medicos/buscar`, `POST medicos/nuevo` | admin, empleado | POS |
| `GET/POST lab/*` | admin, lab | |
| `GET/POST resultados/*` | admin, empleado | **GET email mutante** |
| `POST feedback` | auth | CSRF |

Sin suite de integración HTTP en el repo.

---

## 8. Tests

| Suite | Resultado |
|-------|-----------|
| Tenant (MySQL) | 8 OK |
| Suite completa | Errors en `ShieldAuthTest` (SQLite sin `tenant_id` en users) |
| E2E cross-tenant / roles | Ausente |

---

## 9. Remediación priorizada (solo plan; no ejecutada)

### Fase A — P0/P1 (recomendada primero)

1. **FLOW-05:** email solo POST.  
2. **FLOW-01:** médico → portal `/r/{token}` (no PDF con auth).  
3. **MT-01:** `asegurarPublicToken` con `tenant_id`.  
4. **MT-02:** ticket SQL con `tenant_id` en joins.

### Fase B — P2

5. Fuente de verdad `medico` (deprecar `doctores`).  
6. Reportes de perfiles/paquetes.  
7. EnsureMT / checklist deploy.  
8. Tests cross-tenant + roles.  
9. Fix `ShieldAuthTest`.

### Fase C — P3

10. Docs home `/inicio`.  
11. Carrito keyed por tenant.  
12. Matriz permisos si hace falta.  
13. Smoke HTTP JSON.

---

## 10. Checklist manual (2 tenants A/B)

1. Login A: cliente, venta, médico → IDs.  
2. Login B: ver/ticket/captura de IDs de A → sin datos.  
3. Empleado A: no `/config`, `/super`.  
4. Lab A: no POS cobro.  
5. Superadmin: tenants; sin POS de lab.  
6. Link médico sin sesión lab → usable.  
7. POST sin CSRF → rechazo.

---

## 11. Fuentes

- `app/Config/Routes.php`, `AuthGroups.php`, `Filters.php`, `AuthFilter.php`
- `app/Models/BaseTenantModel.php` + modelos de negocio
- `app/Services/VentaService.php`, `ReporteService.php`, `TenantProvisioner.php`
- `app/Helpers/lab_helper.php`
- `app/Controllers/**`
- `app/Database/Migrations/*`
- `docs/ANALISIS_CONTEXTO.md`
- PHPUnit tenant OK; ShieldAuthTest roto en SQLite

---

## 12. Historial

| Fecha | Nota |
|-------|------|
| 2026-07-28 | Auditoría estática completa; **solo documento**; sin remediaciones aplicadas |

---

*Fin del informe.*
