Odoo access rights, groups and record rules: what actually wins
TechnoVision Engineering
Publication date pending · 11 min read
Ask an Odoo administrator who can delete invoices and you will usually get a pause, then a screenshot of a user form. The form shows which boxes were ticked. It does not show the result — and the result is the accumulated effect of four mechanisms stacked on top of each other, none of which is visible from there.
The four layers
Every permission question in Odoo is answered by some combination of these, in this order. Skipping one is how you end up confidently wrong.
| Layer | What it decides | How it combines |
|---|---|---|
| Group membership and inheritance | Which groups a user effectively holds | Transitive closure — you hold everything your groups imply |
| Model access (ir.model.access) | Can this group read / write / create / delete this model at all | UNION across every group held. There is no deny. |
| Record rules (ir.rule) | Which rows of that model | Global rules AND together; group rules OR together |
| Superuser and Settings | Whether any of the above applies | Bypasses everything |
Groups are transitive, and the form hides it
A group can imply other groups through implied_ids, and those groups can imply more. Ticking "Accounting / Billing Administrator" may hand a user a dozen groups they were never shown. The user form displays the boxes you ticked, not the closure of what those boxes mean.
This is why a permission review that reads the user form is not a permission review. The question is never "which boxes are ticked", it is "what is the transitive closure of those boxes, and what does that closure grant".
There is no deny
This is the single most consequential fact about Odoo permissions, and the one most often discovered by accident.
Model access is a union across every group a user holds. If any group grants unlink on account.move, the user can delete invoices — and removing one of the granting groups changes nothing at all.
There is no rule that says "this group may not delete". An ir.model.access row can only grant. So the instinct — find the group that gives them delete, remove it — fails silently whenever two groups grant the same right. The change is made, the ticket is closed, and the permission is still there.
The practical consequence: when you remove a group to take a right away, you have to check whether anything else still grants it. Not the first group you find. Every group.
Record rules compose in two different ways
Once model access says yes, record rules decide which rows. And they do not all combine the same way, which is where the genuinely counter-intuitive behaviour lives.
- A global rule (one with no groups attached) applies to everyone and ANDs with everything else. Global rules only ever narrow.
- A group rule applies to users in that group, and group rules OR with each other. Group rules can widen.
- The two are then ANDed together: the global constraints, and the union of whatever group rules apply to you.
Adding a group can show someone more records
The accounts where none of this applies
Two categories bypass access control entirely: the superuser, and anyone holding Settings access (base.group_system). For those accounts, every rule above is irrelevant. Listing the models they can reach is a list of every model in the database, which is true and useless.
The meaningful finding for such an account is not what it can reach. It is that it bypasses — and how many accounts are in that state, which in most databases is more than anyone expects.
How to actually answer the question
To answer "who can delete invoices" correctly you have to resolve the group closure for every user, union the model access across that closure, compose the record rules, and set aside the bypass accounts. That is a database query most administrators cannot write, against a question they are asked regularly.
You can do it by hand for one user. It stops being feasible at about three, and it has to be redone every time anyone changes a group.
This is the question our Access X-Ray module was built to answer: it resolves all four layers and shows the result with the path that produced it — including every granting group rather than the first one found, which is exactly the trap described above.
What to check this week
- How many accounts hold Settings access? Every one of them bypasses everything else you have configured.
- For any right you believe you removed recently: is there a second group still granting it?
- Do you have group record rules where you assumed global ones? If so, adding a group may be widening access.
- Which accounts have not logged in for ninety days and still hold their full permissions?
None of those four needs a module to answer. All four need somebody to actually look, and the looking is the part that never gets scheduled.