Odoo 20 permissions migration: test company isolation

Back
Kevin Riedl

11 min read · 5 Nov 2026
Last reviewed

Next
Made on your device, with no Instagram connection. We copy the post link for Instagram’s Link sticker.

In the synthetic example below, the same user and record move from denied to allowed after a naive permissions rewrite. The intended business policy has not changed. The way its grants combine has.

Accept an Odoo 20 permissions migration only after the approved role and company matrix passes both allowed and denied operations on the upgraded database. Verify the converted grants and global restrictions, then test activated company context separately from current-company values. A successful install or administrator walkthrough does not supply that evidence.

Research checked: 5 November 2026. We inspected official Odoo 19 and 20 documentation, the 20.0 access implementation and its source-conversion script, then ran sixteen synthetic Python policy cases. Odoo, a database upgrade and the official converter were not run. The downloadable native acceptance matrix remains NOT_RUN.

What changed in Odoo 20 access control?

Odoo 20 brings operations and record domains into the same access model. That changes what a permissions migration must review: the combined meaning of the resulting grants, not whether a renamed security file loads. Preserve an approved policy reference and compare actual outcomes for each affected role.

The official 20.0 security reference defines ir.access with a group, operation and domain. Group entries grant access; groupless entries restrict it. The 19.0 reference describes separate model-level ACL grants followed by record rules.

For your migration, inventory the custom security records and the business methods that depend on them. Record which policy each entry implements: for example, “a case owner can read their own company’s cases” or “a reviewer can read all cases in the activated companies.” Those are different policies even if both users can open the same menu.

The broader Odoo migration acceptance guide covers records and workflow continuation. This article owns the narrower permission decision. An accurate migrated order is still unacceptable if the wrong identity can retrieve it.

How do group grants and global restrictions combine?

For an ordinary access check, a record needs at least one matching group grant and must satisfy every applicable global restriction. In our fixture, adding a broad grant can expand a user’s readable records; a global company restriction still bounds that expansion. A restriction alone supplies no grant. Test those combinations instead of reviewing each row in isolation.

The model expresses its decision as:

some matching group grant
AND
every applicable global restriction

Our invented own_reader group grants access only to records owned by user 7. A separate global restriction limits records to the activated companies. User 7 can read A-OWN and cannot read A-OTHER. Adding broad_reader makes the second record readable because that group supplies another matching grant.

Now keep company B inactive. The broad group still cannot read B-OWN while the global company restriction applies. Remove that restriction in the deliberately bad fixture configuration and the same record becomes readable. The test exposes the missing boundary; it does not establish that Odoo omits such a boundary by default.

Also test a legitimate allowed record. Our two contradictory global restrictions, one requiring A and one requiring B, deny it. A suite that checks only prohibited access could celebrate this broken configuration. Every important denial needs an allowed control that shows the user can still do their intended job.

Which company context should the test record?

Record company membership, activated companies and the current company separately. They answer different questions: which companies the user is authorized to select, which company scope this operation uses, and which company supplies context-dependent values. An acceptance result without that context cannot explain why a record was visible or hidden.

The 20.0 access-domain evaluator sets company_ids from env.companies.ids, the activated companies, and company_id from env.company.id. Do not substitute every company assigned to the user when recording the tested scope.

Separate inputs in the fictional company-access fixture
DimensionExampleAcceptance question
Authorized membershipUser 7 belongs to A and B.May this identity select company B at all?
Activated companiesOnly A is active for this call.Does a company-scoped operation exclude B records?
Current companyA is current while A and B are active.Which company’s value does the operation use?
Record ownershipB-OWN belongs to B.Does the resulting record belong to the intended company?

The environment implementation validates selected companies for ordinary users, falls back when the company list is empty, and skips the noted sanity checks in sudo mode. An empty list is therefore not a deny-all scope. Elevated test execution cannot establish ordinary-user isolation.

Our fixture makes the distinction visible. With memberships A/B and only A activated, reading the user’s B record is denied. Activate A/B and it is allowed. A user assigned only A cannot activate B in the model. Keep these as separate cases rather than describing all three as “multi-company works.”

Why can a naive ACL conversion widen access?

A naive conversion can widen access when it preserves an old broad model grant as a broad new record grant alongside a narrower grant. Our counterexample gives the user both groups. The old layered policy denies another owner’s record; the new union accepts it. This illustrates a faulty rewrite, not a result from Odoo’s official converter.

One intentionally small conversion counterexample
StageFixture policyOther owner’s A record
Old layered exampleA model ACL permits reading; an applicable own-record rule filters the result.Denied
Naive new rewriteA broad record grant is combined with an own-record grant.Allowed
Approved target policyThe business owner still requires own-record access for this role.Must remain denied

The official access conversion source performs group-aware inference and warns about broad ACL groups interacting with narrower rules. Its warning guidance calls for review. We inspected that source but did not execute it.

Keep the converter’s output and logs with the migration evidence. For each warning, identify the affected effective group combination and operation, then prove the approved outcome on the upgraded database. A clean conversion log still needs regression tests for your policy.

Do not turn the example into a universal instruction to delete broad grants. Another role may legitimately require them. Compare the whole approved matrix before changing a shared group, including users who inherit it through another role.

What did the sixteen local cases show?

The sixteen local cases matched their expected observations. They demonstrate how an access test can isolate a missing restriction, an extra grant or the wrong company context. The fixture also separates visibility from value selection and relation consistency. These are observations about a small Python model, not a security assessment of an Odoo database.

Executed synthetic policy cases on 5 November 2026
CaseExpectedObserved
Own record in active AAllowAllowed
Other owner’s record in active ADenyDenied
Authorized B is not activatedDeny B recordDenied
Authorized B is activatedAllow own B recordAllowed
Activate B without membershipReject contextContext error
Add broad group grantOther owner’s A record becomes readableAllowed
Broad group with global company restrictionInactive B stays deniedDenied
Omit global company restrictionBad configuration exposes inactive BAllowed
Conflicting A-only and B-only global restrictionsLegitimate A record is blockedDenied
Shared record without a companyStrict policy denies; explicit shared policy allowsDenied / allowed
Global restriction without any group grantDenyDenied
Read-only fixture roleAllow read; deny write and deleteAllowed / denied / denied
Naive conversion of the old layered policyOld denial becomes an unintended allowanceDenied / allowed
B record visible with A or B currentCompany value differs: 100.00 versus 120.00100.00 / 120.00
Visible A record linked to a B recordAccess passes; example relation check failsAllowed / inconsistent
Empty activated-company inputFall back to memberships A/BB record readable

Download the runnable permission model and recorded results. With Python 3.10 or later, run:

python3 permission-model.py

The model takes effective groups as already-expanded inputs. It does not implement group inheritance, Odoo domains, field permissions, caches or native create/write hooks. The relation check is deliberately a small equality example. It cannot establish the behavior of every shareable or company-dependent field in a real module.

What belongs in the native role and company matrix?

The native matrix needs an approved expected result for each relevant identity, company context and operation, followed by evidence from the upgraded system. Pair ordinary allowed work with denied access. For denied writes, inspect the resulting state as well as the error. A screenshot of a missing menu is insufficient evidence of the underlying operation.

The proposed native acceptance matrix starts entirely as NOT_RUN. Its fixture expectations are examples to approve or replace, not a universal Odoo access policy. Fill the actual user and effective groups, build revision, entry point, record IDs, company context and evidence before using it for a release decision.

Evidence to collect on the actual upgraded database
DimensionRequired contrastEvidence
RoleOrdinary owner versus approved broader reviewerEffective groups and expected record set for each identity
Company scopeA only, A/B activated, and unauthorized B selectionExact context and target record ownership
OperationPermitted read or write versus prohibited changeReturned records, result state and absence of forbidden changes
Entry pointUser action and each relevant integration or jobActual execution identity and called operation
Policy migrationOld approved expectation versus intended new expectationConverter warning, reviewed change and observed native result
Business consistencyCorrect company value and permitted related recordSelected value and relation evidence, separately from visibility

Use direct record identities as well as the normal navigation path. Define the expected observation for each entry point, including whether a prohibited search should return no foreign records or a requested operation should be rejected. Do not accept a broad result merely because the interface hides the extra rows.

Run destructive-operation cases only on controlled test records in the rehearsal environment. Include allowed changes so a configuration that blocks everyone cannot pass. The agent business-state test guide extends the same evidence principle to an agent’s complete task, where a plausible response may conceal a wrong record.

Why test values and related records after access passes?

Access passing means the identity can perform the tested operation under that policy. It does not by itself show that the operation used the intended company’s value or chose a compatible related record. Our fixture can read the B record while selecting A’s value. It can also allow a record whose proposed relation fails a separate company check.

The official multi-company guide ties company-dependent values to the current company and describes separately configured relational checks through _check_company_auto and check_company. Verify those mechanisms on the specific models and fields in your module.

In the example, both companies are readable. Choosing A as current yields the invented value 100.00; choosing B yields 120.00. If the task concerns B, a successful read of 100.00 can still be the wrong business result. Record the expected company alongside the expected value.

For relations, choose a representative allowed link and a prohibited cross-company link. Inspect the actual operation that assigns them. Do not assume that a visible company field or an access domain proves every custom relationship is checked. The custom-module audit guide covers the broader implementation review around these contracts.

What evidence should block or release the migration?

Block the permissions migration when an unexpected identity can access a protected record, an intended user loses required work, or the evidence cannot establish the tested company context. Release only after the approved matrix passes on the intended build, with deliberate policy changes signed off and unresolved conversion warnings assigned a concrete disposition.

Retain the expected matrix, effective group snapshot and exact upgraded module revision with the observations. An intentional broader reviewer role can be valid, but it needs an explicit policy change. An old denial is not automatically the correct future rule, and an old allowance is not automatically safe to preserve.

The release QA checklist places this evidence within the broader launch decision. The existing Odoo API architecture guide retains ownership of hosting and protocol constraints; changing an API route does not replace the native permission checks.

For Wavect’s software quality assurance service, bring one affected role, two company contexts and the custom module’s conversion output. The first useful deliverable is an agreed allowed/denied matrix with reproducible native evidence for that scope.

Odoo 20 permission migration FAQ

Does a hidden menu prove the underlying access is denied?
No. The acceptance question concerns the record and operation the identity may access. Test the normal user path and the relevant direct entry point with controlled records. Capture returned data and the resulting state. A menu screenshot can document the interface, but it cannot replace evidence that the protected operation obeyed the approved policy.
Can an empty allowed_company_ids list force access to no companies?
No. In the inspected 20.0 environment implementation, an empty list falls back to the user’s companies. Define an explicit permitted context and test its result. If your application needs a no-access state, specify that as a policy outcome and verify it; do not rely on an empty context list to create it.
How should records with no company be accepted?
Decide which records are intentionally shared and which must have an owner. Our fixture tests both a strict company policy and an explicitly shared-record policy; they produce different outcomes. Use representative records for each approved category, including related-record assignments. A missing company value should not silently become an instruction to share sensitive business data.
How should inherited and extra groups be handled?
Review the effective group set for the tested identity, including groups obtained through another role. The downloadable model receives that set as input and does not calculate inheritance. Run the actual combinations your users hold, especially a narrow role combined with a broader reviewer role. A test of either group alone can miss their combined result.
Can the sixteen model cases prove the real database is safe?
No. They demonstrate named policy and context outcomes in a small standard-library Python program. They do not execute Odoo, parse native domains or test custom methods. Use the counterexamples to improve your acceptance design, then run the separate NOT_RUN matrix against the exact upgraded build and approved business policy.
What changes for service accounts and scheduled jobs?
Record the identity and company context each automation uses, then test its approved operations. Do not substitute a human administrator’s walkthrough for that execution path. Include the records it should process and a controlled record it must not touch, and inspect resulting changes. A broad credential deserves an explicit business justification and acceptance scope.
Should every difference from Odoo 19 block the release?
An unexplained difference should block the affected scope until it is understood. An intentional policy correction can be approved, including fixing an old overbroad grant. Record the old expectation, the desired new rule and the owner who approved the change. Compare the upgraded system with that approved target, not with old behavior alone.

Build the product, not just the backlog

If this article maps to a real product decision, Wavect can help you scope, build, harden, or lead the software work with senior founder-level judgment.

Useful service paths:

Inbox, without the noise

Follow the work that matters to you

Get a short email when we publish something new. Follow the whole blog or only the problems you care about.

What would you like to receive?
Choose your topics

Free, double opt-in, no tracking pixels.

Back
Kevin Riedl

11 min read · 5 Nov 2026
Last reviewed

Next

Get the next Delivery and QA field note

One concise email when we publish. No tracking pixels, and no inbox filler.

Free, double opt-in, no tracking pixels.