Access Security in Odoo — ir.model.access.csv and Record Rules (Odoo 16–19)
A complete developer reference for Odoo's two-layer security system: model-level CRUD permissions via ir.model.access.csv and record-level filtering via ir.rule. Verified for Odoo 18 & 19.
Odoo enforces access control at two independent layers. The first layer — model-level access — defines which user groups can perform create, read, write, and delete (CRUD) operations on a model as a whole. The second layer — record rules — further restricts which specific records within a model a user can see or modify. Both layers must be configured correctly for a secure module. Missing access control on a custom model is one of the most common causes of `AccessError` exceptions and unintended data exposure.
The Two Security Layers
| Layer | Mechanism | File / model | Controls | Evaluation |
|---|---|---|---|---|
| 1 — Model access | ir.model.access (ACL) | security/ir.model.access.csv | Can group X do CRUD on model Y at all? | Checked first. If denied here, record rules are never evaluated. |
| 2 — Record access | ir.rule (Record Rules) | security/*.xml (ir.rule records) | Which specific records can group X read/write/create/delete? | Checked second. Domain filter applied to the query. |
Part 1 — Model Access: ir.model.access.csv
Every custom model must have at least one `ir.model.access` record, or all access to it is denied by default. The standard approach is to define these records in a CSV file at `security/ir.model.access.csv` and declare it in `__manifest__.py` under the `data` key.
ir.model.access.csv — Column Reference
| Column | Required | Format / example | Notes |
|---|---|---|---|
| id | Yes | access_my_model_user | External ID for this ACL row. Must be unique across the module. Convention: access_<model>_<group_suffix>. |
| name | Yes | access_my_model_user | Human-readable label. Can match id. Shown in the Access Rights UI. |
| model_id:id | Yes | model_my_module_my_model | External ID of the model. Format: model_ + model name with dots → underscores. E.g. sale.order → model_sale_order. |
| group_id:id | No | base.group_user | External ID of the group. Leave empty to grant access to ALL users (no group restriction). Common values: base.group_user (internal users), base.group_system (admin). |
| perm_read | Yes | 0 or 1 | 1 = allow read. Required for any list or form view to open. |
| perm_write | Yes | 0 or 1 | 1 = allow write (editing existing records). |
| perm_create | Yes | 0 or 1 | 1 = allow create (new records). A user with write but not create can edit but not add records. |
| perm_unlink | Yes | 0 or 1 | 1 = allow delete. Set to 0 for regular users; reserve for managers. |
ir.model.access.csv — Full Example
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
# Internal users — read + write + create, no delete
access_project_task_user,access_project_task_user,model_project_task,base.group_user,1,1,1,0
# Managers — full CRUD
access_project_task_manager,access_project_task_manager,model_project_task,project.group_project_manager,1,1,1,1
# Portal users — read only
access_project_task_portal,access_project_task_portal,model_project_task,base.group_portal,1,0,0,0Declaring Security Files in __manifest__.py
{
'name': 'My Module',
'depends': ['base'],
'data': [
# Security files must come BEFORE views in the data list
# so that groups are defined before views reference them.
'security/groups.xml', # custom group definitions
'security/ir.model.access.csv', # model-level ACL
'security/record_rules.xml', # record rules (ir.rule)
# Views and other data come after:
'views/my_model_views.xml',
],
}Part 2 — Groups (res.groups)
Groups are the link between users and access rights. A user belongs to one or more groups; ACL rows and record rules are keyed to groups. You can define custom groups for your module or reuse Odoo's built-in groups. Groups can inherit from other groups — a user in a child group automatically has all the access of the parent group.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Define a module-specific group category -->
<record id="module_category_my_module" model="ir.module.category">
<field name="name">My Module</field>
<field name="sequence">100</field>
</record>
<!-- User group — basic access -->
<record id="group_my_module_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_my_module"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Manager group — inherits user group, adds extra rights -->
<record id="group_my_module_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_category_my_module"/>
<field name="implied_ids" eval="[(4, ref('group_my_module_user'))]"/>
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
</record>
</odoo>Part 3 — Record Rules (ir.rule)
Record rules add a domain filter on top of the model-level ACL. Even if a group has read permission on a model, a record rule can restrict them to only seeing records that match a domain — for example, records belonging to their own company or created by themselves. Record rules come in two types: global rules and group-specific rules.
| Rule type | groups field | Logic with other rules | Use case |
|---|---|---|---|
| Global rule | Empty (no groups) | AND — always applied, combined with all other rules using AND | Enforce a constraint on all users — e.g. company isolation in multi-company |
| Group-specific rule | One or more groups | OR — among rules for the same group; AND with global rules | Grant extra access to a specific group — e.g. managers see all records |
ir.rule XML Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
| name | Char | Yes | Human-readable label shown in the Record Rules UI. |
| model_id | Many2one → ir.model | Yes | The model this rule applies to. Reference with ref='<module>.<model_external_id>'. |
| domain_force | Char (domain string) | Yes | The domain filter. Records matching this domain are accessible; others are invisible to the affected group. |
| groups | Many2many → res.groups | No | Groups this rule applies to. Leave empty for a global rule. |
| perm_read | Boolean | No (default True) | Apply this rule to read operations. |
| perm_write | Boolean | No (default True) | Apply this rule to write operations. |
| perm_create | Boolean | No (default True) | Apply this rule to create operations. |
| perm_unlink | Boolean | No (default True) | Apply this rule to delete operations. |
Common Record Rule Patterns
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Pattern 1: Own-records rule
Users can only read/write records they created.
Global rule — applies to all users. -->
<record id="rule_my_model_own_records" model="ir.rule">
<field name="name">My Model: Own Records Only</field>
<field name="model_id" ref="model_my_module_my_model"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<!-- No groups → global rule, applies to everyone -->
</record>
<!-- Pattern 2: Manager bypass
Managers can see ALL records (overrides the own-records rule above
for members of the manager group via OR logic). -->
<record id="rule_my_model_manager_all" model="ir.rule">
<field name="name">My Model: Manager Sees All</field>
<field name="model_id" ref="model_my_module_my_model"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('my_module.group_my_module_manager'))]"/>
</record>
<!-- Pattern 3: Multi-company isolation
Global rule — users can only see records of their own company.
Combining allowed_company_ids ensures multi-company switching works. -->
<record id="rule_my_model_company" model="ir.rule">
<field name="name">My Model: Company Isolation</field>
<field name="model_id" ref="model_my_module_my_model"/>
<field name="domain_force">
['|',
('company_id', '=', False),
('company_id', 'in', company_ids)]
</field>
<!-- Global rule — no groups field -->
</record>
</odoo>Troubleshooting
| Error / symptom | Likely cause | Fix |
|---|---|---|
| AccessError: You don't have the right to access objects of type <model> | No ir.model.access row exists for this model and the user's group | Add a row to ir.model.access.csv for the model and the correct group, then upgrade the module. |
| Record list is empty or a specific record cannot be opened — no error shown | A record rule's domain_force is filtering out the records the user expects to see | In developer mode go to Settings → Technical → Security → Record Rules and check rules on the model. Test the domain in a Python shell: self.env['my.model'].search([]). |
| External ID not found: <module>.group_xyz during module install | groups.xml is listed after ir.model.access.csv in __manifest__.py data list | Move groups.xml above ir.model.access.csv in the data list — groups must load before ACL rows that reference them. |
| Admin user bypasses record rules — expected, not a bug | Odoo's superuser (admin, uid=1) bypasses all record rules by design | To test record rules, use a non-admin user. In unit tests, use self.env['my.model'].with_user(regular_user).search([]) to simulate the non-admin context. |
| [(1, '=', 1)] domain in manager rule still blocks some records | A global rule is applying AND logic on top of the manager's OR rule, narrowing results | Review all global rules (no groups set) on the model — these always apply AND. If the global rule is too restrictive, scope it to non-manager groups or remove it from the manager's effective domain. |
Version Notes
| Odoo version | Key changes affecting access security |
|---|---|
| Odoo 16 | No breaking changes to ACL CSV or ir.rule. company_ids variable available in domain_force for multi-company rules. `<list>` rename has no effect on security. |
| Odoo 17 | No breaking changes to ACL or record rule API. Odoo 17 tightens validation of domain_force strings — malformed domains that previously silently failed may now raise errors. |
| Odoo 18 | No breaking changes. ir.model.access.csv format unchanged. record rules API unchanged. New base groups added for the website/portal flows — check group inheritance if upgrading modules with portal rules. |
| Odoo 19 | No breaking changes to ACL or record rules. ORM performance improvements reduce overhead of domain_force evaluation on large tables. |
Need a Security Audit or Custom Access Control for Your Odoo Modules?
Our Odoo-certified developers design and audit access control for Saudi businesses — including ZATCA-sensitive financial data, PDPL-compliant HR records, and multi-company isolation across group structures.
Frequently Asked Questions
What happens if I install a module with no ir.model.access.csv file?
Can I grant access to all users without assigning a specific group?
What is the difference between perm_write and perm_create in the ACL?
How do global record rules interact with group-specific rules?
How do I write a record rule that lets users see only their own company's records in a multi-company setup?
Can record rules restrict which fields a user sees, not just which records?

iWesabe Editorial Team
Practitioner insights on Odoo ERP, ZATCA compliance, and Saudi enterprise digital operations — written by iWesabe's consulting, finance, and engineering teams.
Related Articles
Context and Domain in Odoo — Developer Reference (Odoo 16–19)
A practical guide to Odoo domains (record filtering) and context (key–value state passing) — how they work, where they appear, and the syntax rules for Odoo 16 through 19. Verified for Odoo 18 & 19.
How to Add Colors to List View Rows and Cells in Odoo
Use decoration-* attributes to colour-code rows and individual cells in Odoo list views based on field values — with expression syntax, all decoration types, and a migration note for the tree → list rename. Verified for Odoo 18 & 19.
Odoo attrs Deprecated: Migrating to invisible, readonly, and required in Odoo 16–19
The attrs dictionary was deprecated in Odoo 16 and removed in Odoo 17. This guide maps every attrs pattern to its direct-attribute replacement — with before/after code for the most common use cases. Verified for Odoo 18 & 19.
Explore Related Solutions
Financial Management
Automate your Saudi Arabia accounting — ZATCA Phase 2 e-invoicing, VAT returns, Zakat provisioning, multi-currency bank reconciliation, and real-time financial reporting — all inside one connected Odoo platform. Fully localised for KSA and the GCC by iWesabe.
ExploreCRM & Customer Relationship Management
Turn every lead into a closed deal. Odoo CRM gives your sales team a structured Kanban pipeline, automated follow-up sequences, and a 360° customer view — all connected natively to Accounting, Inventory, and Email Marketing.
ExploreSaudi HR & Payroll Management
Automate GOSI contributions, generate WPS SIF files, track IQAMA expiry, and calculate end-of-service gratuity — all inside one Saudi-compliant Odoo platform.
ExploreManufacturing Management ERP
Run lean, traceable production with Odoo Manufacturing — work orders, Bills of Materials, quality control, and real-time OEE dashboards built for KSA factory floors.
Explore