All posts
Developer10 min read

Odoo access rights and record rules: model-level permissions, ir.rule domains, and field-level security

Odoo security has two distinct layers: model-level access rights (who can read/write/create/unlink a model) and record-level rules (which specific records a user can see). Understanding both is essential for building multi-tenant features, restricting sensitive data, and debugging "you do not have access" errors.

Two Layers of Odoo Security#

Odoo enforces access control at two distinct levels:

  1. Model-level access rights (ir.model.access): can this user/group read, write, create, or delete records on this model at all?
  2. Record-level rules (ir.rule): of the records this user can theoretically access, which specific ones are visible?

Both must pass for a user to access a record. Passing model-level access but failing a record rule results in a "record not found" error (an empty recordset), not a 403 - this is intentional; the record's existence is not revealed.


Model-Level Access Rights#

Defining Access in CSV#

Access rights are defined in a security/ir.model.access.csv file in your module:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_my_model_user,access_my_model_user,model_my_model,base.group_user,1,1,1,0
access_my_model_manager,access_my_model_manager,model_my_model,my_module.group_my_manager,1,1,1,1

Columns:

  • model_id:id: the External ID of the model - always model_ followed by the model name with dots replaced by underscores
  • group_id:id: the security group; leave blank to apply to all users
  • perm_read, perm_write, perm_create, perm_unlink: 1 to grant, 0 to deny

Reference the CSV in __manifest__.py:

python
'data': [
    'security/ir.model.access.csv',
    'views/my_views.xml',
],

Checking Access Programmatically#

python
# Returns True/False without raising
self.env['my.model'].check_access_rights('read', raise_exception=False)

# Checks record-level rules (raises if any record is inaccessible)
records.check_access_rule('write')

Security Groups#

Groups are defined in an XML data file:

xml
<record id="group_my_manager" model="res.groups">
  <field name="name">My Module / Manager</field>
  <field name="category_id" ref="base.module_category_hidden"/>
  <field name="implied_ids" eval="[(4, ref('group_my_user'))]"/>
</record>

implied_ids means "a Manager automatically has all rights of a User." Use this to build a hierarchy: User → Manager → Administrator.

Reference groups in views to show/hide buttons and fields:

xml
<button name="action_approve" string="Approve"
        groups="my_module.group_my_manager"/>
<field name="sensitive_cost" groups="account.group_account_manager"/>

Record Rules (ir.rule)#

Record rules filter which records a user can see using an Odoo domain expression evaluated at runtime.

Defining a Record Rule#

xml
<record id="rule_my_model_company" model="ir.rule">
  <field name="name">My Model: company-scoped</field>
  <field name="model_id" ref="model_my_model"/>
  <field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
  <field name="groups" eval="[(4, ref('base.group_user'))]"/>
  <field name="perm_read" eval="True"/>
  <field name="perm_write" eval="True"/>
  <field name="perm_create" eval="True"/>
  <field name="perm_unlink" eval="True"/>
</record>

The domain_force is evaluated with user (the current user's browse record) and time available as variables. The ORM appends this domain to every query on the model for users in the listed groups.

Global Rules vs Group Rules#

  • Global rules (no groups set): apply to all users; multiple global rules are ANDed
  • Group rules (with groups): apply only to users in those groups; multiple group rules for the same user are ORed

The final effective domain is: (all global rules ANDed) AND (any group rule ORed)

This means: if a user belongs to two groups, each with a rule, they see the union of what each group allows.

Bypassing Record Rules#

python
# sudo() bypasses all record rules and runs as admin
records_with_admin_rights = self.env['my.model'].sudo().search([])

# Specific user context (bypasses only if that user has no rules)
records_as_user = self.env['my.model'].with_user(specific_user).search([])

Use sudo() carefully - it removes all record-level filtering. In controllers, always validate input before using sudo().


Field-Level Access (field groups)#

Fields can be restricted to specific groups directly in the model definition:

python
class MyModel(models.Model):
    _name = 'my.model'

    standard_field = fields.Char()
    sensitive_field = fields.Float(groups='my_module.group_my_manager')

Users not in the group see the field as False (read) and get an access error if they try to write it. In views, the field is hidden automatically for unauthorized users.


Debugging Access Errors#

"You do not have access to this document"#

This is a model-level access error. Check:

  1. Does the user's group have an ir.model.access entry for this model?
  2. Is the model name correct in the CSV? (dots → underscores with model_ prefix)
  3. Did you add the CSV to __manifest__.py data and reinstall the module?

"Record not found" when you know it exists#

This is a record rule filtering it out. In developer mode, go to Settings → Technical → Security → Record Rules and look for rules on the model. Test the domain manually with the user's company/context.

Access works for admin but not for a regular user#

Almost always a missing ir.model.access entry for the user's group. Admin bypasses all access checks. Add a row in the CSV for the correct group.

Record rule domain raises an error#

If the domain references a field that does not exist on the model, or uses incorrect syntax, the domain evaluation fails silently and Odoo treats it as "deny all." Validate domains in Settings → Technical → Security → Record Rules using the Test Rule button.


Checklist for a Secure Custom Module#

  • ir.model.access.csv covers every model with appropriate read/write/create/unlink for each group
  • Record rules enforce company or user scoping for multi-tenant data
  • Sensitive fields have groups= attribute restricting access
  • Views hide buttons and fields based on groups (not just controller checks)
  • Controllers using sudo() validate caller identity before elevating

ERPeek can list all ir.model.access entries, ir.rule records, and field-level groups attributes across your custom modules - useful for a security review before go-live. See the contact page for details.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free