Odoo 20 Access Rights: Life After Record Rules

Ask an Odoo 20 administrator where the Record Rules menu went, and the honest answer is: away. Odoo 20 removes record rules entirely and folds their job into access rights. Where two models — ir.model.access for model-level permissions and ir.rule for record-level filtering — used to share the work, there is now a single model, ir.access, where each access right carries both the operations it allows and a domain that determines which records it applies to. The release notes describe it in one sentence: access rights have been simplified by removing record rules and adding a domain directly at the access right level. For anyone with custom modules, this is the single biggest security migration task in Odoo 20 — every record rule you have ever written has to be re-expressed.

Diagram showing Odoo 20's single ir.access model, where each access right carries its operations plus a record-filtering domain, replacing record rules

The two-layer model you are leaving behind

Up to Odoo 19, security was a two-step check. First, access rights (ir.model.access) answered the coarse question: may this user's groups create, read, write, or delete records of this model at all? Permissions were additive — if any of your groups granted an operation, you had it. Then record rules (ir.rule) answered the fine question, record by record: a rule carried a domain (domain_force), a set of groups, and flags for which operations it applied to.

The subtlety that tripped up generations of developers was how rules combined. Global rules — those with no groups — were subtractive: every global rule had to match for a record to be accessible. Group rules were additive: if any one of them matched (and all global rules matched), the record was accessible. One model, two menus, two combination logics. It worked, but understanding why a user could or could not see a record often meant detective work across both.

What Odoo 20 does instead

Odoo 20 merges the two layers into one. Access rights are now records of ir.access, and each one is associated with a model, optionally a group, a set of operations, and an optional domain. Odoo's own forum answer to the inevitable “where did the menu go?” question is blunt: access rights and record rules have been refactored, and there is now only one menu — Access Rights. The company's analogy is a building with two security guards: previously one guard checked your badge at the door while a second checked a clipboard inside; in Odoo 20, one guard checks the badge and follows you inside, applying both the operation check and the record filter.

The semantics, as documented in Odoo's developer tutorial for the new model, split neatly by whether a group is present:

  • Access right with a group: it grants the listed operations to that group's users, limited to the records that satisfy the domain. Leave the domain empty and it behaves like a classic access right — all records.
  • Access right without a group: it grants nothing. Instead, it restricts access for all users to the records that satisfy the domain — the direct replacement for a global record rule.

That single distinction reproduces the old behaviour. What used to be “salespeople may read and update sales orders” plus a group rule “salespeople see only their own orders” becomes one access right for the sales group, with the operations ru and the domain [('user_id', '=', user.id)]. What used to be a global rule such as “everyone sees only their own companies' records” becomes a group-less restriction record whose domain encodes the company condition.

The new CSV shape

Module developers will feel the change first in the security file. The old ir.model.access.csv, with its four boolean perm_* columns, plus a separate XML data file for record rules, collapses into a single ir.access.csv:

id,name,model_id,group_id/id,operation,domainaccess_sale_order_sales,sale.order own orders,sale.order,sales_team.group_sale_salesman,ru,"[('user_id', '=', user.id)]"access_sale_order_company,sale.order company restriction,sale.order,,crud,"[('company_id', 'in', company_ids)]"

Two things to notice. The operations are a compact string drawn from create, read, update, and delete — ru means read and update, crud means all four. And because each record carries its own operation set, the old trick of a record rule that applied only to reads, or only to deletes, is now expressed as separate access records with different operation strings and domains. Domains keep their familiar evaluation context — user, company_ids, and friends — so most rule domains translate across with little or no rewriting. Odoo also notes that custom access permissions can be built with domains, including an access domain operator for finer-grained checks.

How evaluation works now

The mental model is simpler than the old two-logic system. For a given model and operation, Odoo collects the access rights that apply to the user: the records attached to any of the user's groups act as grants, each narrowed by its domain, and grants still combine additively across groups — belong to two groups and you get the union of what they allow. The group-less records act as universal restrictions on top: no matter what your groups grant, you only reach records that also satisfy every restriction domain.

Two long-standing truths survive the refactor. If no access rights are defined on a model at all, nobody gets in — Odoo still treats a model without access rights as closed, and still warns about it in the server log. And none of this replaces thinking about groups: group membership remains the organising unit of the whole system, which is why the release notes' Technical section also adds a small auditing gift — user group changes are now logged in the user's chatter as well as in the console logs.

What changes for administrators day to day

For functional administrators, the most obvious difference is navigational: instead of bouncing between the Access Rights and Record Rules lists under Settings → Technical, there is one Access Rights list to search, filter, and export. Troubleshooting changes character as well. When a user reports a missing record, the old workflow was to check the model's access rights, then check every record rule — global and group — that might apply, keeping two combination logics in your head at once. In Odoo 20 you filter the access rights for the model, look at the records attached to the user's groups, and then at the group-less restrictions. The answer is on one screen. The flip side is that the list itself becomes denser: databases that carried dozens of record rules will now carry dozens of extra access records, so naming conventions stop being a nicety and become the thing that keeps the screen readable.

Migrating your custom rules

  1. Take inventory. Search your custom modules for ir.rule records and for the old ir.model.access.csv files. Every rule is a migration item; there are no exceptions, because the model they target no longer exists.
  2. Classify each rule. Global rules (no groups) become group-less ir.access restriction records. Group rules become the domain on an access right for that group and model.
  3. Watch the operation flags. A rule that only restricted deletes, or only applied to writes, needs its own access record with the matching operation string — one combined record would silently widen or narrow access.
  4. Translate domains carefully. The domain language is unchanged, but verify any domain that relied on rule-specific context or on being combined with other rules; combination semantics are where subtle access bugs hide.
  5. Test with real accounts. Security errors are invisible to administrators, who see everything. Log in as one user per role and verify both directions: records that should be visible are, and records that should not be, are not.
  6. Audit after go-live. Use the new chatter logging of group changes to keep an eye on who gained which rights during the migration period.

Life after record rules is, on balance, simpler: one model, one menu, one place to answer the question “who can do what to which records?” The price is a disciplined migration pass. Teams that inventory their rules early will find the translation mostly mechanical; teams that discover their rules during user acceptance testing will not.

Further reading

Leave a Comment