movidas openspec a /docs

This commit is contained in:
GitHub Copilot 2026-08-07 16:46:33 +02:00
parent 0ef0c9951c
commit aae6b6096e
9 changed files with 486 additions and 0 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-15

View file

@ -0,0 +1,112 @@
## 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 (H1H5) 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 H1H5 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 `<script>` de
`load_from_history_templates.xml` por `<script type="application/json">` leído por el JS, evitando
inyección raw en contexto de script.
**D6 — Proceso de auditoría: docs + scripts parametrizados.** Checklist, runbook y findings register
en `docs/`; scripts en `scripts/security/` que toman URL y credenciales por parámetro/env (sin
secretos versionados) y son no destructivos. El harness de fuerza bruta y el fuzzing corren contra
staging o en ventana, nunca contra cuentas reales.
**D7 — Hardening de infra: requisitos documentados + cambio versionable puntual.** La config de
`odoo.conf`, nginx/traefik y fail2ban vive en el servidor (no versionable aquí); se documenta como
requisitos verificables en el checklist. El único cambio versionable de este bloque es añadir
`groups_id` a la server action de mandatos SEPA en `account_banking_mandate_batch`.
## Risks / Trade-offs
- [Convertir endpoints a `type="json"` rompe el carrito/checkout] → Tests de endpoint + verificación
end-to-end del flujo de portal antes de mergear; cambios de JS limitados a transporte.
- [Recortar `supplierinfo` rompe la visualización de precios] → Confirmar que el pricing se computa
vía `sudo()`; test que verifica que el shop sigue mostrando precios.
- [Endurecer ACL bloquea a un usuario interno legítimo que dependía del permiso global] → La record
rule interna de lectura ya existe; el test cubre lectura interna y escritura de manager.
- [Auditoría activa afecta a producción] → Snapshot previo, staging preferente, ventana de
mantenimiento, cuentas de test dedicadas, monitorización en vivo.
- [`proxy_mode=True` mal configurado falsea la IP de origen] → Verificar cabeceras `X-Forwarded-For`
en nginx y comprobar en la Fase 1 que fail2ban ve la IP real de Kali.
## Migration Plan
1. Rama aparte para los fixes de código; la auditoría activa se ejecuta contra staging.
2. Aplicar D1D5 en `website_sale_aplicoop` + tests; bump de `__manifest__.py`.
3. Actualizar el addon: `docker-compose run odoo odoo -d odoo --stop-after-init -u website_sale_aplicoop`.
Rollback: revertir la rama y re-actualizar el addon (las ACL/record rules se recargan al -u).
4. Aplicar D7 (config de servidor) en ventana; rollback = restaurar los ficheros de config previos y
recargar servicios.
## Open Questions
- ¿El portal lee `group.order.slot` y `product.supplierinfo` directamente en algún punto, o todo va
por `sudo()`? Resolver leyendo los controladores antes de eliminar ACLs (afecta a D1/D2).
- ¿Se usa traefik o nginx en prod? Ajusta la sintaxis de rate-limit/cabeceras del runbook (no el
requisito).
- ¿Externalizar secretos a `.env`/secrets del orquestador en este change o en uno posterior de infra?

View file

@ -0,0 +1,106 @@
# Proposal — Auditoría de seguridad y hardening (Odoo 18.0)
## Why
Detectamos ataques automatizados cada vez más sofisticados que la heurística de fail2ban no frena,
y somos responsables de la seguridad de nuestra infra y la de nuestros clientes. Necesitamos (a) un
proceso de auditoría **reproducible** tipo "checklist de prevuelo" para verificar la resiliencia
ante fuerza bruta, acceso indebido a datos vía API y vectores inadvertidos; y (b) corregir cinco
hallazgos ya confirmados leyendo el propio código de `website_sale_aplicoop`.
## Objetivo
Dejar la infra Odoo con una postura de seguridad auditable y repetible, y cerrar los hallazgos
H1H5 con tests que impidan su regresión.
## Alcance
- **Solo nuestra infra** (autorización directa, sin terceros). Producción está tras nginx/traefik
con TLS.
- Auditoría **activa autorizada** (fuerza bruta controlada contra cuenta de test y fuzzing de
endpoints) desde la máquina Kali de la red y desde `localhost` vía SSH, preferentemente contra un
clon de staging o en ventana de mantenimiento con snapshot previo.
- Remediación de los hallazgos confirmados en `website_sale_aplicoop` (+ `groups_id` en la server
action de mandatos SEPA de `account_banking_mandate_batch`).
## No-objetivos
- No se audita ni escanea infra de terceros ni hosting ajeno.
- No se corrige aquí ningún hallazgo **nuevo** que surja durante la auditoría activa: se registra en
el findings register y se aborda en un change posterior (salvo severidad crítica, que se escalará).
- No se modifica `ocb/` ni los addons OCA originales.
## What Changes
- **Proceso de auditoría**: se añaden entregables de documentación (`docs/SECURITY_AUDIT_CHECKLIST.md`,
`docs/SECURITY_AUDIT_RUNBOOK.md`, `docs/SECURITY_FINDINGS.md`) y scripts no destructivos en
`scripts/security/` (probe de ACL vía API, probe de IDOR, harness de fuerza bruta, PoC de CSRF,
recon web) parametrizados con cuentas de test.
- **Fix H1 (Alta)** — ACL con `group_id` vacío que concede write+create global sobre `group.order` y
`group.order.slot`: se reemplaza por ACLs de mínimo privilegio (lectura interna/portal explícitas;
escritura solo manager). Los writes de los controladores usan `sudo()` y no dependen de estas ACLs.
- **Fix H2 (Alta)** — record rule de `product.supplierinfo` con `domain=[(1,'=',1)]` que expone todos
los precios de proveedor al portal. El portal usa `supplierinfo` para pintar el **origen** del
producto y el **proveedor principal** en la web, así que no se elimina sin más: o se preparan esos
valores server-side (vía `sudo()` en el controlador, según la regla "sin lógica en QWeb") o se acota
el dominio a los productos accesibles por el usuario, sin exponer campos de coste/precio.
- **Fix H3 (Media)** — endpoints de cambio de estado con `csrf=False`: se convierten a `type="json"`
(bloquea el POST de formulario cross-site), tomando como plantilla `confirm_order_from_portal`.
- **Fix H4 (Media)** — endpoints de lectura/ajax que solo comprueban `exists()`+`state`: se añade
comprobación de pertenencia al grupo reutilizando `_validate_user_group_access` /
`_get_consumer_group_for_user`.
- **Fix H5 (Baja)**`t-raw` inyectado en `<script>`: se sustituye por un patrón de salida seguro.
- **Hardening de config**: `admin_passwd`, `list_db=False`, `proxy_mode=True`, password de BD fuerte,
rate-limit/cabeceras en nginx, jails de fail2ban (web + API). Documentado como requisitos; los
cambios de infra no versionable se ejecutan en el servidor, los versionables (`groups_id` en la
server action) en el repo.
## Capabilities
### New Capabilities
- `eskaera-access-control`: cómo `website_sale_aplicoop` restringe acceso a datos y acciones —
ACL de mínimo privilegio, alcance de `product.supplierinfo`, autorización por pertenencia al grupo
de consumo en todos los endpoints, protección CSRF en endpoints de estado, y escapado de salida en
plantillas. Cubre H1H5.
- `security-audit-process`: proceso repetible de auditoría — reglas de compromiso/autorización,
checklist de prevuelo, runbook de auditoría activa, scripts de prueba y registro de hallazgos.
- `infra-hardening`: requisitos de endurecimiento de producción — `odoo.conf`, reverse proxy
(nginx/traefik) con TLS/rate-limit/cabeceras, fail2ban (web + API), y gestión de secretos/BD.
### Modified Capabilities
- (ninguna — no existen specs previas en `openspec/specs/`)
## Impact
- **Addons afectados**: `website_sale_aplicoop` (security CSV + record rules, controladores,
plantilla, tests, bump de `__manifest__.py`); `account_banking_mandate_batch` (`groups_id` en la
server action).
- **Addons NO afectados**: `ocb/`, los addons OCA originales, y el resto de custom (solo se auditan;
cualquier hallazgo se registra, no se modifica en este change).
- **Nuevas rutas en el repo**: `docs/SECURITY_*`, `scripts/security/`.
- **Impacto UI/website/QWeb**: al convertir endpoints a `type="json"` hay que ajustar el transporte
en el JS de `website_sale_aplicoop` (solo transporte, sin lógica). La plantilla
`load_from_history_templates.xml` cambia su forma de inyectar JSON. Sin cambios visibles de UX.
- **Infra (no versionable)**: `odoo.conf`, config de nginx/traefik y fail2ban en el servidor de prod.
## Riesgos
- La auditoría activa (fuerza bruta/fuzzing) puede degradar o afectar a producción: mitigado con
clon de staging o ventana de mantenimiento, snapshot previo y cuentas de test dedicadas (nunca
contra cuentas reales, por riesgo de lockout/DoS de usuarios legítimos).
- Convertir endpoints a `type="json"` cambia el contrato de request/response: riesgo de regresión en
el carrito/checkout del portal; mitigado con tests y verificación end-to-end.
- Recortar el acceso a `product.supplierinfo` podría romper la visualización de precios si algún
punto del portal dependía de la ACL abierta: verificar que el pricing se computa vía `sudo()`.
## Criterios de aceptación
- Los probes `odoo_acl_probe.py` e `idor_probe.py` deniegan tras los fixes lo que permitían antes
(write/create de `group.order`, lectura de `supplierinfo` ajeno, datos de grupos ajenos).
- El PoC de CSRF funciona antes del fix y falla después.
- El harness de fuerza bruta acaba con la IP de Kali baneada por fail2ban en `/web/login` y en la
ruta API.
- Tests de `website_sale_aplicoop` en verde:
`docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u website_sale_aplicoop`.
- El checklist de prevuelo cubre los tres frentes + infra, con criterio pass/fail por ítem, y el
findings register queda con H1H5 en estado resuelto/verificado.

View file

@ -0,0 +1,87 @@
## ADDED Requirements
### Requirement: Least-privilege ACLs for group order models
`group.order` and `group.order.slot` SHALL NOT grant write, create or unlink to portal, public
or unassigned internal users. Write/create/unlink SHALL be limited to the group-order manager
group (`website_sale_aplicoop.group_group_order_manager`) and ERP admins. Read MAY be granted to
internal users and portal users through explicit, group-scoped ACLs (no empty `group_id` rows).
Controller mutations continue to run via `sudo()` and MUST NOT depend on broad ACLs.
#### Scenario: Portal user cannot mutate group orders
- **WHEN** a portal user calls the external API to `create`, `write` or `unlink` on `group.order`
or `group.order.slot`
- **THEN** the operation is denied with an `AccessError`
#### Scenario: Manager retains full access
- **WHEN** a user in `group_group_order_manager` creates or edits a `group.order` of their company
- **THEN** the operation succeeds
#### Scenario: Controllers still operate
- **WHEN** an authenticated portal member confirms a cart through the eskaera controller (which
uses `sudo()`)
- **THEN** the underlying `sale.order`/`group.order` writes succeed despite the tighter ACLs
### Requirement: Supplier cost not broadly exposed to portal
Portal users SHALL NOT read `product.supplierinfo` cost/price rows for products or vendors
unrelated to them. The unrestricted record rule (`domain=[(1,'=',1)]`) granting portal read on
`product.supplierinfo` SHALL NOT exist. The product **origin** and **main seller** shown in the
eskaera shop — currently painted from `supplierinfo` — MUST keep working, either by preparing those
values server-side (via `sudo()` in the controller, per the repo's "no logic in QWeb" rule) or via
a record rule scoped to the products the user can access, exposing no supplier cost/price fields.
#### Scenario: Portal user cannot list unrelated supplier cost
- **WHEN** a portal user calls `search_read` on `product.supplierinfo` via the external API
- **THEN** the result does not include supplier cost/price rows of products/vendors unrelated to
the user
#### Scenario: Origin and main seller still render
- **WHEN** a portal member opens the eskaera shop
- **THEN** each product's origin and main seller display correctly, without exposing supplier
cost/price across all vendors
### Requirement: CSRF protection on state-changing eskaera endpoints
State-changing eskaera endpoints SHALL be protected against cross-site request forgery. The
endpoints `/eskaera/save-order`, `/eskaera/confirm`, `/eskaera/clear-cart` and `/eskaera/save-cart`
(which create, confirm, merge or cancel `sale.order` records) SHALL NOT be `type="http"` with
`csrf=False`; they SHALL be `type="json"` (or otherwise validate a CSRF token) so that a cross-site
HTML form POST cannot trigger them.
#### Scenario: Cross-site form POST is rejected
- **WHEN** a logged-in user's browser is induced to submit a cross-site form POST to
`/eskaera/clear-cart` or `/eskaera/confirm`
- **THEN** the request is rejected and no order is cancelled/confirmed
#### Scenario: Legitimate same-origin call works
- **WHEN** the eskaera frontend submits the JSON-RPC request from the same origin
- **THEN** the order is created/confirmed as before
### Requirement: Consumer-group membership enforced on group-order endpoints
Every eskaera endpoint that resolves a `group.order` from a request id SHALL verify that the
requesting user belongs to a consumer group of that order before returning data. This applies to
`load_eskaera_page`, `load_products_ajax`, `add_to_eskaera_cart`, `eskaera_checkout` and
`check_group_order_status`, reusing `_validate_user_group_access` / `_get_consumer_group_for_user`.
Internal users MAY be exempt (matching `eskaera_shop`).
#### Scenario: Non-member gets no cross-group data
- **WHEN** a portal user who is not a member of a group order requests
`/eskaera/<order_id>/load-products-ajax` for that order
- **THEN** the endpoint returns empty/redirect and discloses no catalog or pricing of that order
#### Scenario: Member accesses own group order
- **WHEN** a portal member of the order's consumer group requests the same endpoint
- **THEN** the products and pricing are returned normally
### Requirement: Safe template output in inline scripts
QWeb templates SHALL NOT inject data into inline `<script>` blocks via `t-raw`. JSON payloads for
the client SHALL be emitted through a safe pattern (e.g. `<script type="application/json">` read by
the JS layer).
#### Scenario: Rendered history payload is not raw-injected
- **WHEN** the load-from-history template renders its JSON payload
- **THEN** the payload is emitted through a safe (escaped/typed) mechanism, not `t-raw` inside a
`<script>` tag

View file

@ -0,0 +1,61 @@
## ADDED Requirements
### Requirement: Hardened Odoo configuration
Production `odoo.conf` SHALL set a strong `admin_passwd` (not the default `admin`), `list_db =
False`, a `dbfilter` scoping databases per host, `proxy_mode = True`, `workers > 0`, request/time
limits, `without_demo = True`, and a strong database password (not the literal `odoo`). The
database manager SHALL NOT be reachable publicly.
#### Scenario: Database manager not exposed
- **WHEN** an anonymous client requests `/web/database/manager` on production
- **THEN** it is not served (blocked at the proxy and/or `list_db=False`)
#### Scenario: Real client IP reaches Odoo logs
- **WHEN** a login fails behind the reverse proxy
- **THEN** with `proxy_mode=True` the Odoo log records the real client IP (from `X-Forwarded-For`),
not the proxy IP
### Requirement: Reverse proxy enforces TLS, rate limiting and security headers
The nginx/traefik layer SHALL terminate TLS with strong protocols/ciphers, apply `limit_req` to
`/web/login`, `/web/session/authenticate`, `/jsonrpc` and `/xmlrpc`, and set security headers
(HSTS, X-Frame-Options/CSP, X-Content-Type-Options, Referrer-Policy). The `session_id` cookie
SHALL be `Secure`, `HttpOnly` and `SameSite`.
#### Scenario: Login rate limited
- **WHEN** a client exceeds the configured request rate on `/web/login`
- **THEN** the proxy returns 429 and further attempts are throttled
#### Scenario: Security headers present
- **WHEN** a response is returned from production
- **THEN** it includes HSTS, anti-clickjacking and content-type-options headers, and the session
cookie carries Secure/HttpOnly/SameSite
### Requirement: Brute-force sources banned on web and API
fail2ban SHALL ban source IPs that exceed a failed-authentication threshold, covering both the web
login route and the API authentication routes (`/web/session/authenticate`, `/xmlrpc/2/common`),
using the real client IP.
#### Scenario: Repeated web login failures banned
- **WHEN** a source exceeds the failed-login threshold on `/web/login`
- **THEN** fail2ban bans that IP for the configured duration
#### Scenario: API auth brute force banned
- **WHEN** a source brute-forces `/web/session/authenticate` or `/xmlrpc/2/common`
- **THEN** the same fail2ban jail bans it (attacks via API do not bypass protection)
### Requirement: Secrets and database access hardened
Database and master secrets SHALL be strong and SHALL NOT be committed in plaintext in versioned
files. The PostgreSQL port SHALL NOT be reachable from outside the host/network (firewalled), and
the database user SHALL have least privilege.
#### Scenario: DB port not externally reachable
- **WHEN** an external client attempts to connect to PostgreSQL (5432) on production
- **THEN** the connection is refused/filtered by the firewall
#### Scenario: No plaintext secrets in repo
- **WHEN** the repository is inspected
- **THEN** production database/master passwords are not present in plaintext in versioned files

View file

@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: Authorization precedes active testing
Active security testing (brute force, fuzzing) SHALL NOT begin until a written authorization
recording scope, source IPs, time window and emergency contact exists, a tested backup/snapshot is
taken, and dedicated test accounts are provisioned. Active tests SHALL run against a staging clone
or within a maintenance window, and SHALL NOT target real user accounts.
#### Scenario: Test blocked without authorization
- **WHEN** an operator attempts an active test without the recorded authorization and snapshot
- **THEN** the runbook procedure halts at the pre-flight gate and the test does not run
#### Scenario: Test uses dedicated accounts on staging
- **WHEN** a brute-force test runs
- **THEN** it targets a dedicated test account on staging (or in a maintenance window), never a
real user
### Requirement: Pre-flight security checklist
The repo SHALL ship a pre-flight checklist (`docs/SECURITY_AUDIT_CHECKLIST.md`) covering the three
fronts — brute force, API/data access, inadvertent vectors — plus infrastructure. Each item SHALL
have an explicit pass/fail criterion.
#### Scenario: Checklist covers all fronts with criteria
- **WHEN** an operator opens the checklist
- **THEN** it contains markable items for each of the three fronts and infra, each with a defined
pass/fail criterion
### Requirement: Non-destructive audit scripts
The repo SHALL provide audit scripts under `scripts/security/` (ACL probe, IDOR probe, brute-force
harness, CSRF PoC, web recon) that are non-destructive and parameterized with test-account
credentials rather than hard-coded production secrets.
#### Scenario: ACL probe reports access matrix
- **WHEN** the ACL probe runs with a portal test account
- **THEN** it reports, per model, whether read/write/create/unlink is allowed or denied, without
modifying production data
#### Scenario: Scripts take credentials as parameters
- **WHEN** a script is invoked
- **THEN** target URL and credentials are supplied as parameters/env, with no secrets committed to
the repo
### Requirement: Findings register tracks resolution
Findings SHALL be recorded in `docs/SECURITY_FINDINGS.md` with severity, evidence and status, and
each SHALL be tracked until resolved and verified.
#### Scenario: Confirmed findings tracked to closure
- **WHEN** the audit completes
- **THEN** the register lists H1H5 (and any new findings) with severity and a status of
resolved/verified or an owner and plan

View file

@ -0,0 +1,63 @@
## 1. Pre-flight y preparación (reglas de compromiso)
- [ ] 1.1 Redactar y firmar la autorización (alcance, IPs origen Kali+localhost, ventana, contacto) y guardarla como cabecera de `docs/SECURITY_FINDINGS.md`.
- [ ] 1.2 Tomar snapshot/backup de BD y volumen; probar la restauración. Preparar clon de staging.
- [ ] 1.3 Crear cuentas de test dedicadas (`sec_test_portal`, `sec_test_user`, opcional manager); anotar grupos y partners.
- [ ] 1.4 Confirmar topología de prod vía SSH (`ss -tlnp`, `ps aux | grep odoo`, config nginx/traefik, `odoo.conf`, fail2ban, firewall, versión exacta de Odoo).
## 2. Entregables de documentación (`docs/`)
- [ ] 2.1 Crear `docs/SECURITY_AUDIT_CHECKLIST.md`: checklist de prevuelo con ítems markables por los 3 frentes + infra, cada uno con criterio pass/fail.
- [ ] 2.2 Crear `docs/SECURITY_AUDIT_RUNBOOK.md`: comandos paso a paso de la auditoría activa (nmap, hydra/patator, testssl.sh, nikto, ffuf, invocación de scripts) con las salvaguardas de la sección 1.
- [ ] 2.3 Crear `docs/SECURITY_FINDINGS.md`: registro con severidad/evidencia/estado, sembrado con H1H5 + hallazgos de infra.
## 3. Scripts de auditoría (`scripts/security/`, no destructivos, credenciales por parámetro/env)
- [ ] 3.1 `odoo_acl_probe.py`: autentica como portal/usuario de test vía API externa y prueba matriz read/write/create/unlink sobre `group.order`, `group.order.slot`, `product.supplierinfo`, `res.partner`, `res.users`, `account.move`, `sale.order` (de otro partner), `ir.config_parameter`, `ir.attachment`, `account.banking.mandate`, `res.partner.bank`.
- [ ] 3.2 `idor_probe.py`: como portal de test, itera `order_id`/`group_order_id` en `load_eskaera_page`, `load_products_ajax`, `add_to_eskaera_cart`, `eskaera_checkout`, `check_group_order_status` y detecta fuga de catálogo/precios de grupos ajenos; incluir IDOR vertical sobre `/my/orders/<id>` y `/my/events/<id>`.
- [ ] 3.3 `bruteforce_test.sh`: hydra/patator controlado contra cuenta de test en `/web/login` y contra la ruta API (`/web/session/authenticate`, `/xmlrpc/2/common`); verifica baneo de la IP de Kali por fail2ban.
- [ ] 3.4 `csrf_poc.html`: formulario cross-site que fuerza `POST /eskaera/clear-cart` y `/eskaera/confirm` desde sesión logueada (para demostrar H3 antes del fix).
- [ ] 3.5 `web_recon.sh`: nmap + testssl.sh + nikto + ffuf sobre rutas Odoo conocidas (`/web/database/manager`, `/web/database/selector`, `?debug=1`, `/web/webclient/version_info`, `.git/`) y chequeo de cabeceras/cookies.
## 4. Ejecución de la auditoría activa (registrar todo en el findings register)
- [ ] 4.1 Frente 1 (fuerza bruta): ejecutar 3.3; medir umbral/tiempo de baneo web y API; revisar política de contraseñas (`auth_password_policy`), 2FA (`auth_totp`), `auth_signup` no invitado, gestor de BD.
- [ ] 4.2 Frente 2 (acceso a datos vía API): ejecutar 3.1 y 3.2 (baseline pre-fix); documentar accesos indebidos (valida H1, H2, H4).
- [ ] 4.3 Frente 3 (vectores inadvertidos): ejecutar 3.4 (valida H3), 3.5 (TLS, cabeceras, superficie web, versión/CVEs) y revisar `t-raw`/XSS (valida H5).
## 5. Fixes de código — `website_sale_aplicoop`
- [ ] 5.1 (H1) En `security/ir.model.access.csv`: sustituir la fila `access_group_order_base` (grupo vacío) por lectura interna `base.group_user` `1,0,0,0`; idem `access_group_order_slot_base`; mantener filas de manager para escritura; añadir ACL de portal de solo lectura para el slot solo si el controlador lo lee sin `sudo()`.
- [ ] 5.2 (H2) Identificar en plantillas/controladores qué campos de `product.supplierinfo` lee el portal (origen del producto, proveedor principal vía `product.seller_ids`) y confirmar que el pricing va por `sudo()` en `controllers/website_sale_pricing.py`.
- [ ] 5.3 (H2, preferido) Preparar origen + proveedor principal en el controlador vía `sudo()` y pasarlos resueltos a la plantilla (sin lógica en QWeb); luego eliminar la ACL de portal de `product.supplierinfo` (fila 6 del CSV) y la record rule `rule_product_supplierinfo_portal_read`.
- [ ] 5.4 (H2, fallback si el render no se puede mover al controlador) Sustituir `domain=[(1,'=',1)]` de `rule_product_supplierinfo_portal_read` por un dominio acotado a los productos de los grupos del usuario, sin exponer campos de coste/precio.
- [ ] 5.5 (H3) En `controllers/website_sale.py`: convertir `save-order`, `confirm`, `clear-cart`, `save-cart` de `type="http"`+`csrf=False` a `type="json"` (plantilla: `confirm_order_from_portal`); devolver `dict`.
- [ ] 5.6 (H3) Ajustar el JS de `static/src/js/` (solo transporte: fetch con envelope JSON-RPC y lectura de `result`), sin lógica de negocio en JS.
- [ ] 5.7 (H4) En `controllers/website_sale.py`: añadir comprobación de pertenencia tras `exists()`/`state` en `load_eskaera_page`, `load_products_ajax`, `add_to_eskaera_cart`, `eskaera_checkout`, `check_group_order_status`, reutilizando `_get_consumer_group_for_user` (retorno vacío/redirect) o `_validate_user_group_access` (fallo duro); mantener bypass de usuario interno (`share == False`).
- [ ] 5.8 (H5) En `views/load_from_history_templates.xml`: sustituir `t-raw` en `<script>` por `<script type="application/json">` leído por el JS.
- [ ] 5.9 Bump de versión en `website_sale_aplicoop/__manifest__.py` (`18.0.X.Y.Z`).
## 6. Fix versionable de infra — `account_banking_mandate_batch`
- [ ] 6.1 Añadir `groups_id` a la server action de mandatos SEPA en `account_banking_mandate_batch/data/server_action.xml` para restringir quién puede lanzarla.
## 7. Tests (extender los existentes de `website_sale_aplicoop/tests/`)
- [ ] 7.1 `test_record_rules.py` / `test_multi_company.py`: aserciones de que el portal NO puede write/create `group.order` ni `group.order.slot` (H1) ni leer `product.supplierinfo` ajeno (H2).
- [ ] 7.2 `test_group_order_status_endpoint.py`: un portal fuera del grupo NO obtiene datos de los endpoints de lectura/ajax (H4); un miembro sí.
- [ ] 7.3 Test de que los endpoints de estado convertidos a `type="json"` funcionan same-origin y que el flujo de checkout/carrito sigue operando (H3).
## 8. Hardening de infra (servidor de prod; no versionable — verificar contra el checklist)
- [ ] 8.1 `odoo.conf`: `admin_passwd` fuerte, `list_db=False`, `dbfilter` por host, `proxy_mode=True`, `workers>0`, `limit_*`, `without_demo=True`, `db_password` fuerte; log parseable por fail2ban.
- [ ] 8.2 nginx/traefik: `limit_req` en `/web/login`, `/web/session/authenticate`, `/jsonrpc`, `/xmlrpc`; bloquear/restringir `/web/database/*`; HSTS + cabeceras; TLS fuerte; cookie `session_id` Secure/HttpOnly/SameSite; upgrade de `/websocket`.
- [ ] 8.3 fail2ban: jails para login web y API (IP real vía `proxy_mode`), jail de 429 de nginx y `recidive`; verificar baneo con 3.3.
- [ ] 8.4 BD/secretos: confirmar 5432 no expuesto por firewall; usuario de BD con mínimos privilegios; externalizar secretos fuera de ficheros versionados.
## 9. Verificación y calidad
- [ ] 9.1 Re-ejecutar 3.1/3.2 (deben denegar lo que antes permitían) y 3.4 (debe fallar tras el fix); actualizar el findings register a resuelto/verificado.
- [ ] 9.2 Tests del addon: `docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u website_sale_aplicoop` (usar `run`, no `exec`).
- [ ] 9.3 Verificación end-to-end del portal (carrito, save-order, confirm, clear-cart, checkout) tras la conversión a `type="json"`.
- [ ] 9.4 Calidad: `make format` / `make lint` (black línea 88 + isort + flake8 + pylint-odoo); sin `_()` en definiciones de campo; `pre-commit run --all-files`.
- [ ] 9.5 Cerrar cada ítem del checklist de prevuelo con evidencia.

54
docs/openspec/config.yaml Normal file
View file

@ -0,0 +1,54 @@
schema: spec-driven
context: |
Repo: addons-cm (Odoo 18.0 / OCB). Stack: Python 3.10+, Odoo ORM, XML/QWeb, JS.
Restricciones:
- NO modificar el core de Odoo/OCB en `ocb/`.
- NO modificar addons OCA originales (ver `.github/copilot-instructions.md`).
- Los cambios deben hacerse en addons custom del repo.
Convenciones clave:
- Seguir guías OCA (black/isort/flake8/pylint-odoo).
- Tests: `docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u <addon>`.
- Evitar `_()` en definiciones de campos; usar traducciones en `i18n/*.po`.
- No meter lógica compleja en QWeb; preparar datos en Python (controller/model).
rules:
proposal:
- "Incluir siempre: Objetivo, Alcance, No-objetivos, Riesgos, Criterios de aceptación."
- "Listar explícitamente los addons afectados (y los NO afectados)."
- "Reafirmar restricciones: no tocar `ocb/` ni addons OCA originales; cambios solo en custom."
- "Si afecta a UI/website/QWeb, indicar qué páginas/vistas se tocan y el impacto para usuario."
design:
- "Referenciar modelos/campos/vistas por nombre técnico (ej: `group.order`, `sale.order`)."
- "Incluir consideraciones de seguridad (grupos/reglas) y multi-compañía si aplica."
- "Evitar lógica compleja en QWeb: documentar qué datos se preparan en Python."
- "Si hay impactos de datos (migración/recomputes), describir estrategia (cron, compute store, scripts)."
tasks:
- "Descomponer en tareas pequeñas (ideal: <= 2h cada una) y ordenadas."
- "Cada tarea debe mencionar archivos/carpetas a tocar (rutas relativas) y el addon."
- "Incluir al menos una tarea de verificación: tests con `docker-compose run ... --test-enable` (no `exec`)."
- "Incluir notas de calidad cuando aplique: lint/format (black/isort) y evitar `_()` en fields."
- "Prohibido proponer cambios en `ocb/` o addons OCA originales; si se necesita, crear un addon custom heredando."
specs:
- "Especificar comportamiento observable (inputs/outputs), edge cases y criterios de aceptación verificables."
- "Evitar detalles de implementación; esos van en design.md."
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours