Odoo · Seguridad · HTTP
Los controladores Odoo son la frontera entre Internet y tu ORM. Una ruta mal configurada con auth='public', un write sin whitelist o un JSON sin validar puede anular meses de trabajo en ACL y record rules. Esta entrada recoge el patrón que uso en APIs REST reales.
Fundamentos
Anatomía de un controlador Odoo
from odoo import http
from odoo.http import request
class MyController(http.Controller):
@http.route('/api/v1/items', type='jsonrpc', auth='user', methods=['POST'])
def list_items(self, **kwargs):
return request.env['my.item'].search_read([], ['name', 'state'])
| Parámetro | Valores típicos | Implicación de seguridad |
|---|---|---|
auth | user, public, none | ¿Hay sesión? ¿Usuario portal o anónimo? |
type | http, jsonrpc | CSRF en formularios HTTP vs JSON API |
methods | GET, POST, … | Restringe verbos; evita GET con side-effects |
csrf | True (default http) | Desactivar solo con autenticación alternativa |
Regla práctica:
auth='user'para portal y backend autenticado;auth='public'solo para landing pages sin datos sensibles;auth='none'casi nunca en módulos custom.
Autenticación
Sesión, API key y entorno del usuario
Patrón híbrido (sesión web + API key para clientes móviles):
def get_authenticated_user():
api_key = request.httprequest.headers.get('X-API-Key')
if api_key:
user_id = request.env['res.users.apikeys'].sudo()._check_credentials(
scope='rpc', key=api_key
)
if user_id:
return request.env['res.users'].sudo().browse(user_id)
user = request.env.user
if user and not user._is_public():
return user
return None
def _with_authenticated_env(self, user):
return request.env(user=user) # Respeta ACL del usuario
El sudo() queda acotado a validar la API key (sin contexto de usuario). Las operaciones de negocio corren con request.env(user=user).
request.env['model'].sudo().write(data) tras autenticar. Anulas record rules y abres IDOR (Insecure Direct Object Reference).
Autorización
Grupos, dominios y comprobación explícita
READER_GROUP = 'my_module.group_reader'
def _require_reader(self, user):
if not user.has_group(READER_GROUP):
return self._json_error('Read access required.', status=403)
return None
@http.route('/api/v1/items/<int:item_id>', auth='user', type='jsonrpc')
def get_item(self, item_id):
user, err = self._authenticate()
if err:
return err
if deny := self._require_reader(user):
return deny
env = self._with_authenticated_env(user)
item = env['my.item'].browse(item_id)
if not item.exists():
return self._json_error('Not found.', status=404)
return item.read(['name', 'state'])
ACL + record rules filtran en el ORM; has_group() añade capa de rol cuando la API expone operaciones distintas (lectura vs gestión).
Entrada
Validar y sanitizar el payload
ALLOWED_PATCH_FIELDS = frozenset({'name', 'qty_on_hand'})
def _parse_json_body(self):
raw = request.httprequest.get_data(as_text=True) or ''
if not raw.strip():
return {}
payload = json.loads(raw)
if not isinstance(payload, dict):
raise ValidationError('Body must be a JSON object.')
return payload
def patch_item(self, item_id):
data = self._parse_json_body()
safe = {k: v for k, v in data.items() if k in ALLOWED_PATCH_FIELDS}
if not safe:
return self._json_error('No allowed fields.', status=400)
item.write(safe)
int, str, rangos) antes del write. Odoo no lo hace por ti en rutas custom.
Errores
Respuestas HTTP coherentes
| Código | Cuándo | Qué devolver |
|---|---|---|
| 401 | Sin autenticación | Mensaje genérico, sin filtrar si el ID existe |
| 403 | Autenticado pero sin permiso | Grupo o ACL insuficiente |
| 404 | Registro inexistente o invisible | Mismo mensaje en ambos casos (anti-enumeración) |
| 400 | JSON inválido o campos no permitidos | Detalle del campo rechazado |
def _json_error(self, message, status=400):
return request.make_json_response(
{'ok': False, 'error': message}, status=status
)
Checklist
Auditoría rápida de controladores
rg "@http.route" odoo/addons/custom/— inventario de rutas- ¿Cada ruta tiene el
authmínimo necesario? - ¿Hay
sudo()en el handler? ¿Está justificado? - ¿Los
write/createusan whitelist de campos? - ¿Se prueba con usuario portal sin permisos de admin?
Resumen
Autentica con sesión o API key acotada, opera con request.env(user=…), comprueba grupos cuando haga falta y filtra campos en cada write. Los controladores enrutan; no sustituyen ACL ni record rules. Si necesitas sudo(), que sea local y documentado — como vimos en la entrada anterior de la serie.