The Access Rights Stack
Odoo security has four layers. When a user gets an AccessError, the problem is in one of these layers:
- ir.model.access (Model-level ACL): Can this group read/write/create/unlink this model?
- ir.rule (Record rules): Can this user access this specific record?
- Field-level access:
groupsattribute on field definitions - Menu/Action access: Can the user see the menu item or action?
Layer 1: Model Access (ir.model.access.csv)
Model access controls CRUD permissions per security group:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sale_order_salesman,sale.order.salesman,model_sale_order,sales_team.group_sale_salesman,1,1,1,0
access_sale_order_manager,sale.order.manager,model_sale_order,sales_team.group_sale_manager,1,1,1,1Debugging model access:
# Check what access a user has on a model
def check_model_access(self, model_name, user_id):
user = self.env['res.users'].browse(user_id)
groups = user.groups_id
accesses = self.env['ir.model.access'].search([
('model_id.model', '=', model_name),
'|',
('group_id', 'in', groups.ids),
('group_id', '=', False), # global access (no group)
])
for acc in accesses:
print(f'{acc.name}: R={acc.perm_read} W={acc.perm_write} '
f'C={acc.perm_create} D={acc.perm_unlink} '
f'Group={acc.group_id.name or "Global"}')Layer 2: Record Rules (ir.rule)
Record rules filter which records a user can access. They add domain conditions to every query:
<record id="sale_order_own_rule" model="ir.rule">
<field name="name">Own Sale Orders</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>Key record rule behaviors:
- Rules with a group: ORed within the same group, ANDed across different groups
- Global rules (no group): always ANDed
sudo()bypasses record rules
Debugging Record Rules
# Find all rules for a model
rules = self.env['ir.rule'].search([
('model_id.model', '=', 'sale.order')
])
for rule in rules:
groups = rule.groups.mapped('name') or ['Global']
print(f'{rule.name}: {rule.domain_force} -> {groups}')
# Test a specific rule for a user
user = self.env['res.users'].browse(user_id)
try:
record = self.env['sale.order'].with_user(user).browse(record_id)
record.check_access_rights('read')
record.check_access_rule('read')
print('Access granted')
except AccessError as e:
print(f'Access denied: {e}')Layer 3: Field-Level Access
Fields can restrict access to specific groups:
cost_price = fields.Float(groups='account.group_account_manager')
secret_notes = fields.Text(groups='base.group_system')If a user does not belong to the specified group, the field is invisible and inaccessible via API. Check field groups:
# List restricted fields on a model
for name, field in self.env['sale.order']._fields.items():
if field.groups:
print(f'{name}: groups={field.groups}')Systematic Debugging Workflow
When a user reports an AccessError, follow this sequence:
Step 1: Read the Error Message
Odoo's AccessError messages tell you exactly which operation was blocked:
# Model access error
"You are not allowed to access 'Sale Order' (sale.order) records."
"Allowed operations: Read"
# Record rule error
"Due to security restrictions, you are not allowed to access 'Sale Order' (sale.order) records."
"Records: SO001 (id=42)"Model access errors mention the model name. Record rule errors mention specific record IDs.
Step 2: Check User Groups
user = self.env['res.users'].browse(user_id)
print('Groups:', user.groups_id.mapped('full_name'))Step 3: Check Model Access
# Via shell
self.env['ir.model.access'].check('sale.order', 'write', raise_exception=False)
# Returns True/FalseStep 4: Check Record Rules
try:
record.with_user(user).check_access_rule('write')
except AccessError:
# Check which rules apply
rules = self.env['ir.rule']._compute_domain('sale.order', 'write')
print('Computed domain:', rules)Step 5: Test with sudo()
# If sudo() works, the issue is access rights (not a logic bug)
try:
record.with_user(user).write({'state': 'done'}) # fails?
record.sudo().write({'state': 'done'}) # works?
print('Issue is access rights, not logic')
except Exception as e:
print(f'Other error: {e}')Common AccessError Patterns
| Error Pattern | Likely Cause | Fix |
|---|---|---|
| Cannot read model | Missing ir.model.access for user group | Add CSV access line |
| Can read but not write | perm_write=0 in access CSV | Set perm_write=1 |
| Can access some records | Record rule domain too restrictive | Expand domain or add group exception |
| Lost access after module install | New record rule added by module | Check new rules from installed module |
| Error only in production | Demo user has admin rights | Test with a non-admin user |
Logging Access Checks
# Enable security logging
import logging
logging.getLogger('odoo.addons.base.models.ir_rule').setLevel(logging.DEBUG)
logging.getLogger('odoo.addons.base.models.ir_model_access').setLevel(logging.DEBUG)Best Practices
- Always test permissions with a non-admin user — admin bypasses most checks
- Use
check_access_rights()andcheck_access_rule()in your code when you need to verify access before operations - Document your security model — list all groups, access rules, and record rules
- Avoid global record rules unless absolutely necessary — they affect all users
- Use
sudo()sparingly and only when the business logic requires cross-user access - Test all CRUD operations, not just read — write and unlink are commonly missed