Skip to content

OD401 model-without-access

A new model has no ir.model.access rule, or from saas~19.4 no ir.access rule with a group.

Category Severity Series Needs the index
Security Warning, Info from saas~19.4 all yes

What it reports

A class that defines a new model, as a Model or a TransientModel, when no indexed module grants access to it: no row of ir.model.access, or of ir.access with a group, in a CSV file or an XML record that Odoo loads, names the model. A row grants nothing until its file loads, so only a file that counts as loaded, as OD310 spells out, can hold one. The model is the class's _name, or for a class without one the name Odoo gives it from its class, as OD209 describes. The message names ir.access from saas~19.4 and ir.model.access before it and in a module of unknown series, while a row of either model counts on every series.

Why it matters

Odoo refuses every operation on a model to a user that no access row lets through. Only the superuser and code running under sudo() can use a model without rows, so its menus, views and wizards fail with an access error for everyone else. Odoo 18.0 logs The models [...] have no access rules when it installs or updates such a module, and its own superuser-only models carry a row with every permission set to 0 to say so on purpose.

From saas~19.4 Odoo replaces ir.model.access and ir.rule with ir.access, loaded from security/ir.access.csv. A row's operation is a non-empty subset of crud, so no row can say "nobody", and a model meant for the superuser alone has no row at all. From saas~19.4 the finding is therefore Info: a hotspot to review, which is a bug only when someone other than the superuser is meant to use the model. An ir.access row without a group restricts every user rather than granting, so it does not count as access.

Before saas~13.2 Odoo checks no access row for a TransientModel, so such a class is not reported on those series.

Abstract models and classes that only extend an existing model are not reported. Nor is a class whose own body sets _register false, since Odoo keeps it out of the registry; a subclass that does not set it is registered and reported. A loaded row in any indexed module counts, so a model that another module grants access to is not reported either. For the same reason, neither is a model when an unread data file that may hold the row is in the module or in a module that depends on it.

Example

models/library_loan.py
class LibraryLoan(models.Model):
    _name = "library.loan"
    _description = "Book loan"

    partner_id = fields.Many2one("res.partner", required=True)
    due_date = fields.Date()

The module ships no access row for library.loan, so internal users get an access error on it.

security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_library_loan_user,library.loan user,model_library_loan,base.group_user,1,1,1,1

List the file in the manifest's data. From saas~19.4 the same grant reads:

security/ir.access.csv
id,name,model_id,group_id/id,operation
access_library_loan_user,library.loan user,library.loan,base.group_user,crud

Suppressing

class LibraryLoan(models.Model):  # odooscan: ignore[OD401]

See suppressing a finding.