In Odoo 20, the entire security architecture is consolidated into a single model; ir.access consolidates both ir.model.access (Access Rights) and ir.rule (Record Rules). Instead of using multiple CSV files and XML record rules for security, it is now consolidated into one file, namely, security/ir.access.csv.
Importance of the Change Beyond Its Appearance
This is one of the most disruptive architectural changes for Odoo 20, which is surprising, in a way, because this isn't a change in the Python API but a data loading change. Two existing security models, namely, ir.model.access (Access Rights) and ir.rule (Record Rules) have been consolidated into one single model: ir.access. Either model no longer exists as such, not even in alias form - any module that provides a security/ir.model.access.csv file or an XML record with model="ir.rule" will fail during installation regardless of whether the module uses the corresponding model in its code or not.
Field Mapping Changes
| Odoo 19 & Older | Odoo 20 (ir.access) | Purpose / Format |
| ir.model.access | ir.access | Access control system integrated. |
| ir.rule | ir.access | Record-level access rules moved into the same model. |
| model_id:id / model_id | model_id (no suffix) | Important! Strictly refers to the name of the model (e.g., project.task), NOT the XML ID of the model (e.g., model_project_task). |
| group_id:id | group_id/id | Refers to the security group of users (XML ID). |
| perm_read, perm_write, perm_create, perm_unlink | operation | String formed from c, r, u, d (crud or r, for example). |
| domain_force (on ir.rule) | domain | Python filter for domain, now optional on the same row. |
One thing that needs special mention is the way the CSV header uses model_id with no suffix but group_id/id with a suffix. This may appear to be a mistake but is actually how the field expects its data. Furthermore, model_id now requires the actual name of the model (project.task), not the ir.model id (model_project_task).
Step-by-Step Migration Procedure
1. Rename and move your security file
Rename the security/ir.model.access.csv to security/ir.access.csv in the security/ directory.
2. Update the header
id,name,model_id,group_id/id,operation,domain
3. Migrate rows defining old access rights
The older version used four boolean columns (1/0) for CRUD permissions. In the new version you have a single column called 'operation', which consists of the letters c,r,u,d in any combination – meaning that you don’t have to break a row into several letters unless you want to.
Old ir.model.access.csv row using project.task as an example:
access_project_task,project.task,model_project_task,project.group_project_user,1,1,1,0
New ir.access.csv row (the same permissions – read, write, create, without delete – in a single row):
id,name,model_id,group_id/id,operation,domain
access_project_task,project.task,project.task,project.group_project_user,cru,
Do not specify anything in the domain if you are defining model-level permission without record filtering.
4. Migrate XML record rules
Delete your XML files containing <record model="ir.rule">. Replace them with the corresponding rows in ir.access.csv and place the rule’s domain filter in the domain field.
Old XML record rule:
<record id="project_task_personal_rule" model="ir.rule">
<field name="name">Personal Tasks Only</field>
<field name="model_id" ref="model_project_task"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('project.group_project_user'))]"/>
<field name="perm_read" eval="1"/>
</record>
New ir.access.csv row:
project_task_personal_rule,Personal Tasks Only,project.task,project.group_project_user,r,"[('user_id', '=', user.id)]"One structural difference to watch for: ir.rule.groups was a many-to-many field, so one rule could target several groups at once. ir.access.group_id is a single many-to-one - a rule that used to list multiple groups now needs one row per group.
5. Treat Global Rule As Group-less Restriction Rows
In the new system, the global record rules will not have any special flag. Instead, if there is value assigned to group_id, then the meaning of the row differs:
- The row with group_id will be the permission, as this provides access to that group.
- The row without group_id will be the restriction, and this will apply to all the users just as ir.rule did before.
id,name,model_id,group_id/id,operation,domain
project_task_no_archived,Hide Archived Tasks,project.task,,r,"[('active', '=', True)]"
Not using group_id for the universal restriction is not an omission but actually the way to specify the universal restriction in the new system. While examining the migrated file, make sure to check every empty group_id/id intentionally since it's easy to miss this and unintentionally create the global restriction out of the group-based one.
6. Run the Automated Rewrite Script
There is a built-in script which creates ir.access.csv based on your module's ir.model.access.csv and ir.rules automatically:
./odoo-bin --addons-path=/path/to/your/addons upgrade_code --script 19.4-00-ir-access.py
This program uses deduction rather than substitution to figure out what your previous permissions and policies were, including any group inheritance you had set. Examine its log file output before trusting the result:
INFO - successfully processed, no additional work required.
WARNING - partially processed; check it over and probably fix it manually.
ERROR - not processed; requires manual translation.
Look carefully for warnings regarding group inheritance. If you previously had a group that was inheriting from a larger group's access policies, and your new row comes out without a group_id, an access policy meant for one group is silently translated into a global access policy allowing everybody in.
7. Update the Module Manifest
# Odoo 19 and earlier
'data': [
'security/ir.model.access.csv',
'security/ir_rule.xml',
# ... views and data files
],
# Odoo 20
'data': [
'security/ir.access.csv',
# ... views and data files
],
Discard any XML files that contained ir.rule data solely – that information is now contained in the CSV file.
Summary Checklist
- Rename security/ir.model.access.csv > security/ir.access.csv
- Merge perm_read/perm_write/perm_create/perm_unlink into one operation field value
- Use the model’s name directly (like “project.task”) rather than the XML ID (“model_project_task”) for the model_id field.
- Convert all nodes to one row in CSV with the domain field value.
- Break apart any rule that has more than one group into separate rows per group.
- Execute upgrade_code --script 19.4-00-ir-access.py and read all WARNING/ERROR messages
- Verify all rows that have blank group_id/id fields - ensure they are intended to be global.
- Edit __manifest__.py and remove all empty record-rule XML files
To read more about How to Perform Mass Data Migration Using CSV, refer to our blog How to Perform Mass Data Migration Using CSV.