Odoo · Portal · Seguridad
El portal del cliente es la cara visible de tu ERP para usuarios externos. Aquí un error de permisos no es un bug: es una fuga de datos. La regla es simple y absoluta: cada consulta se filtra por el partner del usuario. Veamos cómo añadir listas, fichas y descargas al portal sin abrir agujeros.
Lista
Menú /my con contador
from odoo import http
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal
class DiagnosticPortal(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if 'diagnostic_count' in counters:
partner = request.env.user.partner_id
values['diagnostic_count'] = request.env[
'diagnostic.test'].search_count(
[('partner_id', '=', partner.id)])
return values
Heredar de CustomerPortal integra tu modelo en la página /my con su tarjeta y contador, igual que pedidos o facturas.
Listado
Página de listado filtrada
@http.route('/my/diagnostics', type='http',
auth='user', website=True)
def portal_diagnostics(self, **kw):
partner = request.env.user.partner_id
tests = request.env['diagnostic.test'].search(
[('partner_id', '=', partner.id)])
return request.render(
'mi_modulo.portal_my_diagnostics',
{'tests': tests, 'page_name': 'diagnostics'})
auth='user', request.env.user.partner_id es siempre el cliente real, y las record rules del grupo portal añaden una segunda capa de filtrado automático.
Ficha
Detalle con check_access y sudo
@http.route('/my/diagnostic/<int:test_id>',
type='http', auth='user', website=True)
def portal_diagnostic_page(self, test_id, access_token=None, **kw):
try:
test_sudo = self._document_check_access(
'diagnostic.test', test_id, access_token)
except (AccessError, MissingError):
return request.redirect('/my')
return request.render(
'mi_modulo.portal_diagnostic_page',
{'test': test_sudo})
_document_check_access (heredado de portal) valida que el partner tiene acceso o que el access_token es válido, y devuelve el registro en sudo de forma controlada. Es el patrón estándar: nunca hagas browse(test_id) directo desde una URL.
browse(test_id).sudo() sin comprobar el partner, cualquier cliente puede ver los datos de otro cambiando el ID en la URL. _document_check_access existe precisamente para cerrar esto.
Descarga
Servir un PDF protegido
@http.route('/my/diagnostic/<int:test_id>/pdf',
type='http', auth='user', website=True)
def portal_diagnostic_pdf(self, test_id, access_token=None, **kw):
test_sudo = self._document_check_access(
'diagnostic.test', test_id, access_token)
pdf, _ = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
'mi_modulo.action_report_diagnostic', [test_sudo.id])
headers = [('Content-Type', 'application/pdf'),
('Content-Length', len(pdf))]
return request.make_response(pdf, headers=headers)
Mismo guard antes de generar el PDF. Enlaza con la entrada de informes QWeb de esta serie: el portal solo añade la capa de control de acceso sobre el render ya existente.
Checklist
Antes de publicar al portal
| ✅ Comprobación | Por qué |
|---|---|
Toda ruta usa auth='user' | Identidad real del partner |
Filtrado por partner_id | No listar datos ajenos |
_document_check_access en detalle | Evita IDOR por URL |
| Record rule grupo portal | Segunda capa a nivel ORM |
| Probar con dos clientes | Confirmar aislamiento real |
Resumen
Hereda de CustomerPortal, usa auth='user' en todas las rutas y filtra siempre por partner_id. Para fichas y descargas, _document_check_access es obligatorio: cierra el IDOR clasico. Defiende en profundidad combinando filtrado en el controlador con record rules del grupo portal, y prueba el aislamiento con dos clientes distintos antes de publicar.