A user clicks a button, opens a record, or runs a custom report and Odoo throws this on the screen:
odoo.exceptions.AccessError: You are not allowed to access "Users" (res.users) records.
- Operation: read
- User: 12 (Mark T)
- Required model access groups for "res.users":
- base.group_erp_manager
This shows up in Odoo 17.0, 18.0, and 19.0. It is almost never the user's fault — your code (or a third-party module) is reading res.users from a context where the current user does not have permission. The fix is rarely "give the user admin"; that is the wrong answer 95 percent of the time.
Quick Fix
In your custom code, replace direct reads of res.users with a sudo() read on the specific field you need, then drop the elevated context:
# WRONG — fails for non-admins
manager_email = order.user_id.email
# RIGHT — read one field with sudo, no privilege escalation beyond that
manager_email = order.sudo().user_id.email
# or, if you need the user record itself:
manager = order.user_id.sudo()
If the error happens in a third-party module you cannot edit, add the user to the base.group_user group (Internal User) — res.users is readable by any internal user by default. The error usually means the affected user is a Portal or Public user.
Why This Happens
res.users has a strict ACL. Reads require base.group_user (Internal User) or higher. Writes require base.group_erp_manager (Administration: Settings). Record rules further restrict which users a given user can see. The error fires when:
- A portal/public user triggers a flow that reads a user field. Typical example: a website controller renders a sale order with
order.user_id.partner_id.image_1024and the visitor is not logged in as an internal user. - A controller method runs with
request.env(which uses the calling user) instead ofrequest.env.with_user(SUPERUSER_ID)for an explicitly public action. - A computed field reads
user_id.somethingwithoutsudo()and is rendered in a list view that any portal user can see. - A model inherits something that reads
res.usersin_compute_*. The compute runs in the requesting user's context. - A cron or automated action runs as a user that does not have
base.group_user. (Crons should run as admin or as a dedicated bot user.)
Step-by-Step Diagnosis
1. Identify the user and the model. The traceback always names the user id and the model. Note both.
2. Check the user's groups. Open Settings > Users & Companies > Users, select the user, and look at the "Access Rights" tab. Does the user have User types > Internal User? If they are Portal or Public, that is your problem.
3. Reproduce in a shell. Open odoo-bin shell -c odoo.conf -d <db>:
user = env['res.users'].browse(12)
env['res.users'].with_user(user).check_access_rights('read', raise_exception=False)
# False means access is forbidden
If check_access_rights returns False, ACL is the issue. If it returns True, the problem is a record rule.
4. Inspect record rules.
rules = env['ir.rule'].sudo().search([('model_id.model', '=', 'res.users')])
for rule in rules:
print(rule.name, rule.domain_force, rule.groups.mapped('name'))
Look for rules that exclude the affected user. Multi-company rules are a frequent culprit — the user might not have the relevant company in company_ids.
5. Find the offending read in your code. Use git grep for \.user_id\. and any env['res.users']. Each one is a candidate. The traceback's last user-code frame is usually the answer.
Permanent Fix
The right fix depends on intent.
If the read is intentional public data (the sales rep's name on a public quotation page), use sudo() on the smallest scope:
class SaleOrder(models.Model):
_inherit = 'sale.order'
salesperson_name = fields.Char(compute='_compute_salesperson_name')
def _compute_salesperson_name(self):
for order in self:
order.salesperson_name = order.sudo().user_id.name or ''
If the read is admin-only, add an explicit guard so the error message is meaningful:
def action_reassign(self):
if not self.env.user.has_group('sales_team.group_sale_manager'):
raise UserError(_("Only Sales Managers can reassign orders."))
...
If a website controller is the source, decorate the route or add auth='public' only when you genuinely intend public access, and guard your env reads:
@http.route('/my/orders', auth='user', website=True)
def my_orders(self, **kw):
orders = request.env['sale.order'].search([('partner_id', '=', request.env.user.partner_id.id)])
# safe — user is logged in and reading their own orders
If a cron is failing, set the cron's user to a dedicated bot account with the right groups, or admin if the action genuinely requires it.
How to Prevent It
- Make
sudo()audit-grep-able. Establish a team rule: everysudo()call gets a one-line comment explaining why. This makes security reviews cheap and surfaces accidental privilege escalation immediately. - Lint for
request.env['res.users']. Any controller-side read ofres.userswithoutsudo()is suspect. Catch it pre-merge. - Test as portal user. Add a Playwright or Hoot test that signs in as a portal account and exercises every public route. Most
AccessErrorregressions ship because nobody tested as a non-admin. - Use computed fields with
compute_sudo=Truefor fields that aggregate user data:
salesperson_name = fields.Char(compute='_compute_salesperson_name', compute_sudo=True)
compute_sudo=True runs the compute in superuser context, sidestepping ACL while keeping the read explicit and auditable.
- Multi-company sanity check. New users must be added to all companies they need access to (
company_ids), not just one (company_id). The Odoo UI hides this distinction.
Related Errors
- MissingError: Record does not exist — record rules can surface as MissingError instead of AccessError.
- ValidationError: Required field not set — different error class, same wave of post-deploy fallout.
- Expected singleton — common companion error in the same code path.
Frequently Asked Questions
Why does sudo() fix this — am I bypassing security?
sudo() runs that one ORM call as superuser. It bypasses ACL and record rules for that call only. It is not a global escalation. Use it for reads where the user genuinely needs the value (a salesperson's name on a public page), and never for writes that the user should not be allowed to make.
Should I add the user to base.group_erp_manager?
Almost never. That group grants Administration: Settings, which is a superpower. The right answer is base.group_user (Internal User) for staff, base.group_portal for customers, and sudo() in code for the specific reads that cross those boundaries.
How do I see exactly which record rule is blocking the user?
Enable Developer Mode, open Technical > Security > Record Rules, filter by model res.users. Each rule shows its domain and the groups it applies to. To debug live, run env['ir.rule']._compute_domain('res.users', 'read') in a shell as the affected user — that returns the combined domain Odoo applies.
My cron worked yesterday and started failing today. What changed?
Check ir.cron.user_id on the failing cron. A migration or restore can reset the cron's user to a default that lacks the right groups. Set it back to admin (or a dedicated bot) and the error goes away. ECOSIRE's Odoo migration team hits this on every major version upgrade.
How do I audit every sudo() in my codebase?
grep -rn "\.sudo()" /opt/odoo/custom/ | wc -l
Anything over a few dozen warrants review. Pair the count with grep -B1 ".sudo()" to read the line above each — if it does not have a # why-sudo: comment, add one or refactor.
What is the difference between sudo() and with_user(SUPERUSER_ID)?
sudo() is a shortcut for with_user(SUPERUSER_ID) plus an internal flag that propagates through related-record reads. with_user(uid) switches to a specific user. Use sudo() for "I need superuser for this read", with_user(uid) for "I need to act as a specific other user" (rare, mostly tests).
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transform Your Business with Odoo ERP
Expert Odoo implementation, customization, and support to streamline your operations.
Related Articles
How to Add a Custom Button to an Odoo Form View (2026)
Add custom action buttons to Odoo 19 form views: Python action method, view inheritance, conditional visibility, confirmation dialogs. Production-tested.
How to Add a Custom Field in Odoo Without Studio (2026)
Add custom fields via custom module in Odoo 19: model inheritance, view extension, computed fields, store/non-store decisions. Code-first, version-controlled.
How to Add a Custom Report in Odoo Using External Layout
Build a branded PDF report in Odoo 19 using web.external_layout: QWeb template, paperformat, action binding. With print logo + footer overrides.