What Is sudo()?
The sudo() method creates a new recordset that bypasses access rights and record rules. It is Odoo's equivalent of Unix sudo: it elevates permissions for a specific operation. Used correctly, it enables legitimate cross-security-boundary operations. Used carelessly, it creates security vulnerabilities.
# Normal access (respects ACL + record rules)
partner = self.env['res.partner'].browse(partner_id)
# Elevated access (bypasses ACL + record rules)
partner = self.env['res.partner'].sudo().browse(partner_id)
# sudo() returns a new recordset, does not modify self
elevated = self.sudo()
self.env.su # False
elevated.env.su # True
When to Use sudo()
1. Cross-Model Access in Business Logic
When a user action in one model legitimately needs to modify another model they do not have direct access to:
def action_confirm(self):
# User confirms their own order
self.write({'state': 'confirmed'})
# Create accounting entries (user has no accounting access)
self.sudo()._create_account_move()
# Update inventory (user has no warehouse access)
self.sudo()._update_stock_move()
2. Portal/Public User Operations
Portal users have minimal ORM access. sudo() is needed for any database operation beyond basic reads:
@http.route('/portal/submit', type='http', auth='public')
def portal_submit(self, **kwargs):
# Portal user cannot create records via ORM
ticket = request.env['support.ticket'].sudo().create({
'name': kwargs.get('subject'),
'partner_id': request.env.user.partner_id.id,
'description': kwargs.get('description'),
})
return request.redirect(f'/portal/ticket/{ticket.id}')
3. System Operations (Cron, Automation)
Cron jobs often need unrestricted access to process all records:
def _cron_send_reminders(self):
# Cron runs as OdooBot, needs access to all records
overdue = self.sudo().search([
('date_deadline', '<', fields.Date.today()),
('state', '!=', 'done'),
])
for record in overdue:
record._send_reminder_email()
4. Reading Configuration
System parameters and settings often require elevated access:
def _get_config_value(self):
return self.env['ir.config_parameter'].sudo().get_param(
'my_module.api_url', default='https://api.example.com')
When NOT to Use sudo()
1. Bypassing Access Errors You Do Not Understand
# BAD: hiding a real permissions problem
def action_view_invoices(self):
invoices = self.sudo().mapped('invoice_ids') # WHY sudo?
# The user should have invoice access!
# Fix: grant proper access rights instead
2. In Compute Methods
# BAD: sudo in compute bypasses security for all users
@api.depends('partner_id')
def _compute_partner_info(self):
for record in self:
# This exposes data from ANY partner to ANY user
partner = record.partner_id.sudo()
record.partner_phone = partner.phone # security bypass!
3. Returning Elevated Recordsets to the UI
# BAD: returning sudo recordset to web client
def action_view_records(self):
records = self.env['secret.model'].sudo().search([])
return {
'type': 'ir.actions.act_window',
'res_model': 'secret.model',
'domain': [('id', 'in', records.ids)],
# The user still cannot read these in the UI!
# sudo does NOT persist across requests
}
SUPERUSER_ID
SUPERUSER_ID (value: 1) is the special user ID that bypasses all access checks. It is used differently from sudo():
from odoo import SUPERUSER_ID
# Using SUPERUSER_ID for environment creation
env = api.Environment(cr, SUPERUSER_ID, {})
record = env['model'].create(vals)
# sudo() vs SUPERUSER_ID
self.sudo() # same user, elevated privileges
self.with_user(SUPERUSER_ID) # switch to superuser
The practical difference: sudo() keeps the current user context (for tracking, defaults), while with_user(SUPERUSER_ID) switches the user entirely.
sudo() Scope Best Practices
Minimize the Scope
# BAD: entire method runs elevated
def action_process(self):
records = self.sudo()
for record in records:
record.compute_something() # unnecessary sudo
record.validate() # unnecessary sudo
record.partner_id.write({...}) # unnecessary sudo
# GOOD: elevate only the specific operation
def action_process(self):
for record in self:
record.compute_something() # normal access
record.validate() # normal access
# Only this specific write needs elevation
record.sudo()._create_system_log()
Validate Before Elevating
def action_approve(self):
# Validate with normal permissions first
for record in self:
if record.state != 'submitted':
raise UserError('Cannot approve.')
if not self.env.user.has_group('module.group_approver'):
raise UserError('Not authorized.')
# Then elevate for the system operation
self.sudo().write({
'state': 'approved',
'approved_by': self.env.uid,
})
Drop sudo() as Soon as Possible
def _get_partner_email(self):
# Read with sudo, but return plain value (not recordset)
partner = self.partner_id.sudo()
email = partner.email # plain string
# email is not elevated, safe to return
return email
Alternatives to sudo()
- Proper access rights: grant users the correct groups and ACLs
- Record rules: use domain-based restrictions instead of blanket deny + sudo
- Server actions: run automated code as admin without exposing sudo in module code
- with_user(): switch to a specific user instead of superuser
- with_company(): switch company context for multi-company access
Security Audit Checklist
- Search your codebase for all
.sudo()calls - For each call, document WHY sudo is needed
- Verify that input validation happens BEFORE sudo
- Ensure sudo recordsets are not returned to the UI
- Check that sudo is not used in compute methods that expose data
- Confirm that the minimal scope of sudo is used