Odoo · Seguridad · sudo()
sudo() ejecuta código como superusuario: ignora ACL y record rules. Es útil en crons y hooks internos; es peligroso en controladores web, APIs públicas y campos compute que el usuario puede influir. Esta entrada es una guía de auditoría, no un permiso para abusar de él.
Qué hace
Anatomía de sudo()
# Usuario portal (permisos limitados)
partner = request.env['res.partner'].browse(partner_id)
partner.read(['name']) # AccessError si no tiene ACL
# Mismo código con sudo — bypass total
partner = request.env['res.partner'].sudo().browse(partner_id)
partner.read(['name']) # Siempre funciona
partner.write({{'phone': '600000000'}}) # También escribe, sin restricciones
sudo() devuelve un nuevo entorno con uid=SUPERUSER_ID. No «eleva» solo la lectura: aplica a create, write y unlink.
Regla de oro: si el código corre en nombre de un usuario externo (HTTP, RPC, portal), no uses
sudo()salvo para operaciones acotadas y justificadas, con validación previa de identidad y datos.
Cuándo sí
Usos legítimos
| Contexto | Ejemplo | Por qué es aceptable |
|---|---|---|
| Cron / scheduled action | Enviar recordatorios masivos | No hay usuario interactivo; sistema automatizado |
post_init_hook | Crear datos de configuración inicial | Instalación con privilegios de módulo |
| Computed field técnico | Leer parámetro ir.config_parameter | Dato global, no expone registros ajenos |
| Mail / notificaciones | message_post cross-company | Patrón interno de Odoo, acotado |
| Tests | setUpClass crea datos base | Entorno aislado, no producción |
@api.model
def _cron_send_reminders(self):
# Cron corre como usuario del job; sudo para leer todos los pendientes
tests = self.sudo().search([
('state', '=', 'report_pending'),
('deadline', '<=', fields.Date.today()),
])
for test in tests:
test._send_reminder_email() # Vuelve a env con permisos del método
Cuándo nunca
Antipatrones peligrosos
| ❌ Patrón | Riesgo | ✅ Alternativa |
|---|---|---|
sudo().browse(id).write(data) en controller | Cualquier usuario modifica cualquier registro | Verificar permisos + write sin sudo |
sudo().search([]) en ruta auth='public' | Filtración masiva de datos | Record rules + dominio en búsqueda |
| Compute con sudo que expone campos sensibles | Usuario ve datos de terceros en vista | Calcular sin sudo o restringir groups= en campo |
sudo() «para que no dé error» | Máscara de ACL mal diseñados | Corregir CSV y record rules |
| Encadenar sudo en toda la call stack | Imposible auditar quién hizo qué | sudo mínimo, scope local |
# ❌ MAL — controller portal
@http.route('/my/diagnostic/<int:test_id>', auth='user')
def portal_diagnostic(self, test_id, **kw):
test = request.env['diagnostic.test'].sudo().browse(test_id)
test.write(kw) # Escribe cualquier campo que llegue por POST
# ✅ BIEN — permisos del usuario actual
@http.route('/my/diagnostic/<int:test_id>', auth='user')
def portal_diagnostic(self, test_id, **kw):
test = request.env['diagnostic.test'].browse(test_id)
if not test.exists():
raise NotFound()
allowed = {{k: v for k, v in kw.items() if k in ('note',)}}
test.write(allowed)
Alternativas
Herramientas más seguras que sudo()
with_user(user)
Ejecuta como otro usuario respetando sus reglas. Ideal para probar permisos y para server actions que deben actuar «en nombre de» alguien.
with_company(company)
Cambia contexto multi-compañía sin saltarse ACL. Combinable con usuario específico.
sudo(flag=False) / salir de sudo
Si heredas código con sudo, puedes volver al usuario original con env.sudo(False) para operaciones sensibles.
Acciones de servidor
Configurar cron con usuario dedicado (ej. «Bot automatización») con grupos mínimos, en lugar de sudo omnipotente.
Auditoría
Cómo revisar sudo en tu código
rg "\.sudo\(" odoo/addons/custom/— lista todos los usos- Clasifica cada uno: cron / hook / controller / compute / test
- En controladores: debe haber validación de identidad antes de cualquier write
- En computes: ¿el valor filtrado depende de reglas del usuario? → no sudo
- Documenta en comentario por qué es necesario (una línea basta)
# Ejemplo de sudo documentado y acotado
def _get_default_sequence(self):
# System parameter readable only by admin; safe sudo for default value
param = self.env['ir.config_parameter'].sudo().get_param(
'diagnostic.default_sequence', 'DIAG'
)
return param
sudo() que lee registros relacionados puede mostrar en la vista datos que las record rules ocultarían. El usuario no necesita permiso de lectura directa si el compute se lo regala.
Tests
Probar que la seguridad no se bypassa
def test_portal_user_cannot_read_other_diagnostic(self):
other = self.env['diagnostic.test'].create({{...}})
portal_user = self.env.ref('base.demo_user0')
with self.assertRaises(AccessError):
other.with_user(portal_user).read(['name'])
Si este test falla tras añadir sudo() en un compute o controller, has introducido una regresión de seguridad. Los tests de permisos son tan importantes como los de negocio.
Resumen
sudo() es para operaciones de sistema con contexto controlado: crons, hooks, parámetros globales. En rutas HTTP, portal y RPC es casi siempre una mala idea. Prefiere ACL bien diseñados, with_user() para pruebas y whitelist de campos en writes. Si necesitas sudo, que sea local, documentado y revisado en code review.