## Context `website_sale_aplicoop` expone el portal de compra colaborativa (Eskaera) a usuarios de portal, y la API externa de Odoo (XML-RPC/JSON-RPC) es accesible aunque el puerto de la BD esté firewallado. La recon del código confirmó cinco hallazgos (H1–H5) y varios puntos de config de infra. Producción corre tras nginx/traefik con TLS; el `docker-compose.yml` del repo es lab/dev y no refleja prod. Los controladores de eskaera ya usan `sudo()` para sus escrituras y ya disponen de gatekeepers de pertenencia (`_validate_user_group_access`, `_get_consumer_group_for_user` en `controllers/website_sale_validators.py`); el problema es que las ACL/record rules son demasiado permisivas y que algunos endpoints no invocan esos gatekeepers. ## Goals / Non-Goals **Goals:** - Cerrar H1–H5 con mínimo cambio de comportamiento observable para el usuario legítimo, respaldado por tests que impidan regresión. - Entregar un proceso de auditoría repetible (checklist, runbook, scripts, findings register). - Documentar y aplicar hardening de infra (odoo.conf, proxy, fail2ban, secretos/BD). **Non-Goals:** - No se corrigen hallazgos nuevos de la auditoría activa en este change (se registran). - No se toca `ocb/` ni addons OCA originales. - No se rediseña la UX del portal; los cambios de endpoint son de transporte/seguridad. ## Decisions **D1 — ACL de `group.order`/`group.order.slot` (H1): reemplazar la fila de `group_id` vacío por ACLs explícitas de mínimo privilegio.** La fila vacía concede write+create a todos. Se sustituye por: lectura interna (`base.group_user`, `1,0,0,0`, que empareja con la record rule interna ya existente), lectura de portal explícita donde haga falta, y escritura solo para `group_group_order_manager`. Alternativa descartada: añadir record rules que bloqueen write/create a portal — más frágil que quitar el permiso en la ACL, porque una ACL permisiva sin regla restrictiva ya abre el acceso. Verificar si el portal lee `group.order.slot` por ACL o por `sudo()` en el controlador; si es por `sudo()`, no añadir ACL de portal para el slot. **D2 — `product.supplierinfo` (H2): acotar el acceso, no eliminarlo.** El portal SÍ usa `supplierinfo`: la web pinta el **origen** del producto y el **proveedor principal** a partir de él (vía `product.seller_ids`), además de que el pricing se computa server-side en `controllers/website_sale_pricing.py` vía `sudo()`. Por tanto no se puede quitar la ACL de portal sin romper ese render. Dos opciones, en orden de preferencia: - **(preferida) Preparar origen + proveedor principal en el controlador** vía `sudo()` y pasarlos ya resueltos a la plantilla (alineado con la regla del repo "sin lógica en QWeb"). Entonces el portal deja de necesitar ACL directa sobre `product.supplierinfo` → se elimina la ACL de portal (fila 6) y la record rule `rule_product_supplierinfo_portal_read`. Es lo más seguro: no expone ningún coste. - **(fallback) Acotar la record rule** para que el portal solo lea `supplierinfo` de los productos de los grupos a los que pertenece, sustituyendo `domain=[(1,'=',1)]` por un dominio filtrado, y sin exponer campos de coste/precio (restringir con `groups=` en el campo o exponer campo derivado). El punto clave: hay que verificar en las plantillas/controladores qué campos de `supplierinfo` se leen realmente (origen, nombre del proveedor) antes de elegir; el objetivo es no exponer `price`/coste de todos los proveedores a cualquier usuario de portal. **D3 — CSRF (H3): convertir a `type="json"`.** Los endpoints de estado (`save-order`, `confirm`, `clear-cart`, `save-cart`) pasan de `type="http"` + `csrf=False` a `type="json"`. Un `type="json"` exige `Content-Type: application/json`, que un formulario HTML cross-site no puede fijar, de modo que neutraliza el CSRF por formulario sin gestionar tokens manualmente. Plantilla de referencia: `confirm_order_from_portal` (ya `type="json"`). Implica ajustar el JS (solo transporte: fetch con envelope JSON-RPC y lectura de `result`) y el Python (devolver `dict` en vez de `Response`). Alternativa descartada: mantener `type="http"` y validar token CSRF — más código y más fácil de olvidar en endpoints futuros. **D4 — Autorización horizontal (H4): invocar los gatekeepers existentes.** En cada endpoint que hace `group.order.sudo().browse(order_id)` y solo comprueba `exists()`/`state`, añadir la comprobación de pertenencia tras ese check. Usar `_get_consumer_group_for_user` (devuelve `False`) en los que deban retornar vacío/redirect silencioso (`load_eskaera_page`, `load_products_ajax`) y `_validate_user_group_access` (lanza) en los que deban fallar duro. Mantener el bypass para usuarios internos (`current_user.share == False`) igual que `eskaera_shop`. **D5 — Plantilla (H5): salida JSON segura.** Sustituir `t-raw` en `