Ad – 728×90
⚙️ Module Development

Odoo Security – Access Rights, Groups, and Record Rules

Odoo's security model has two layers: access rights (which user groups can perform CRUD operations on which models) and record rules (row-level filters that restrict which specific records a user can see or modify). Getting security right is one of the most important tasks in module development — an incorrectly configured module can expose data to users who shouldn't see it.

⏱️ 25 min 🎯 Beginner 📅 Updated 2026

Access Rights (ir.model.access.csv)

Access rights are defined in a CSV file — security/ir.model.access.csv. Each row grants or denies CRUD permissions for a user group on a model.

CSV format:

CSV
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink

Real example:

CSV
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_library_book_user,library.book user,model_library_book,library_management.group_library_user,1,0,0,0
access_library_book_librarian,library.book librarian,model_library_book,library_management.group_library_librarian,1,1,1,0
access_library_book_manager,library.book manager,model_library_book,library_management.group_library_manager,1,1,1,1
access_library_book_public,library.book public,model_library_book,base.group_public,1,0,0,0
ColumnValueMeaning
idXML IDUnique identifier for this access rule
nameStringHuman-readable description
model_id:idmodel_ + model name (dots→underscores)The model this rule applies to
group_id:idXML ID of res.groups recordThe user group; empty = all users
perm_read1 or 0Can read/search records
perm_write1 or 0Can update existing records
perm_create1 or 0Can create new records
perm_unlink1 or 0Can delete records
⚠️
Always load security before views

The ir.model.access.csv file must be the FIRST item in your manifest's data list. If views load before security, Odoo may throw access errors during development.

Defining User Groups

User groups (res.groups) define categories of users with different permission levels. Define them in an XML file under security/:

XML
<odoo>
    <data>
        <!-- Base group — all users start here -->
        <record id="group_library_user" model="res.groups">
            <field name="name">Library User</field>
            <field name="category_id" ref="base.module_category_services_library"/>
            <field name="users" eval="[(4, ref('base.user_demo'))]"/>
        </record>

        <!-- Librarian group — inherits from user -->
        <record id="group_library_librarian" model="res.groups">
            <field name="name">Librarian</field>
            <field name="category_id" ref="base.module_category_services_library"/>
            <field name="implied_ids" eval="[(4, ref('group_library_user'))]"/>
        </record>

        <!-- Manager group — inherits from librarian -->
        <record id="group_library_manager" model="res.groups">
            <field name="name">Library Manager</field>
            <field name="category_id" ref="base.module_category_services_library"/>
            <field name="implied_ids" eval="[(4, ref('group_library_librarian'))]"/>
        </record>
    </data>
</odoo>

implied_ids — a group that implies another group automatically grants all the implied group's permissions too. Manager implies Librarian implies User — a user in the Manager group has all three groups' permissions.

XML IDWho it represents
base.group_publicNon-logged-in (anonymous) visitors
base.group_portalPortal users (customers with login)
base.group_userInternal users (all logged-in employees)
base.group_systemSettings administrators
base.group_erp_managerFull ERP access
Ad – 336×280

Record Rules (ir.rule)

Record rules are row-level security filters — they restrict which records a user can see or modify, applied as an automatic domain filter on every query.

XML
<odoo>
    <data noupdate="1">

        <!-- Users can only see their own borrowings -->
        <record id="rule_borrowing_user" model="ir.rule">
            <field name="name">Borrowings: see own only</field>
            <field name="model_id" ref="model_library_borrowing"/>
            <field name="groups" eval="[(4, ref('group_library_user'))]"/>
            <field name="domain_force">[('borrower_id', '=', user.id)]</field>
            <field name="perm_read">1</field>
            <field name="perm_write">1</field>
            <field name="perm_create">1</field>
            <field name="perm_unlink">0</field>
        </record>

        <!-- Librarians can see all borrowings in their branch -->
        <record id="rule_borrowing_librarian" model="ir.rule">
            <field name="name">Borrowings: librarian sees branch</field>
            <field name="model_id" ref="model_library_borrowing"/>
            <field name="groups" eval="[(4, ref('group_library_librarian'))]"/>
            <field name="domain_force">[('branch_id', 'in', user.branch_ids.ids)]</field>
        </record>

        <!-- Global rule: never show deleted/archived records -->
        <record id="rule_borrowing_global" model="ir.rule">
            <field name="name">Borrowings: active only (global)</field>
            <field name="model_id" ref="model_library_borrowing"/>
            <!-- No groups = global rule (applies to everyone) -->
            <field name="domain_force">[('active', '=', True)]</field>
        </record>

    </data>
</odoo>
Access RightsRecord Rules
ScopeAll records of a modelSpecific records matching a domain
LevelModel levelRow level
Defined inCSV fileXML file
When appliedEvery CRUD operationAs an additional WHERE clause on queries
Multiple rulesCombined with OR (any rule allows = access granted)Combined with OR per group

Field-Level Security

Restrict specific fields to certain groups using the groups attribute on the field definition:

Python
class LibraryBook(models.Model):
    _name = 'library.book'

    name = fields.Char(string='Title')
    cost_price = fields.Float(
        string='Cost Price',
        groups='library_management.group_library_manager',  # only managers see this
    )
    internal_note = fields.Text(
        string='Internal Notes',
        groups='library_management.group_library_librarian',
    )

Fields with groups are invisible AND not sent in RPC responses to users outside the group — this is server-side security, not just UI hiding.

Or in view XML:

XML
<!-- Hide a field in the view for non-managers (client-side only — use Python groups for real security) -->
<field name="cost_price" groups="library_management.group_library_manager"/>

Using sudo() to Bypass Access Rights

sudo() gives a recordset superuser rights for a specific operation. Use carefully:

Python
def action_auto_confirm(self):
    # This runs as the current user — may fail if user lacks write access
    self.write({'state': 'confirmed'})

def action_system_confirm(self):
    # Run as superuser — bypasses all access checks
    # Use only when the business logic requires it and you've manually checked permissions
    self.sudo().write({'state': 'confirmed'})

# sudo() with a specific user
def action_confirm_as_admin(self):
    admin = self.env.ref('base.user_admin')
    self.sudo(admin).write({'state': 'confirmed'})
⚠️
Don't use sudo() to hide problems

Never use sudo() to silently work around an access error you don't understand. Always fix the access rights properly. Reserve sudo() for intentional privilege elevation — background jobs, automated flows, or cases where the operation legitimately needs to run as a different user.

Checking and Debugging Access Rights

In developer mode, go to Settings → Technical → Security → Access Rights to see all loaded rules. To check rights programmatically:

Python
# Check if current user can read a model
self.env['library.book'].check_access_rights('read', raise_exception=False)
# Returns True or False

# Check access on specific records
self.env['library.book'].browse([1, 2, 3]).check_access_rule('write')
# Raises AccessError if any record is blocked

# Useful for debugging — print current user's groups
print(self.env.user.groups_id.mapped('full_name'))

📋 Key Points

  • ir.model.access.csv defines CRUD permissions per model per group — load it FIRST in manifest
  • model_id:id format: model_ + model _name with dots→underscores
  • User groups (res.groups) define permission tiers; implied_ids creates inheritance
  • Record rules add row-level domain filters — applied automatically as WHERE clauses
  • Field-level security via groups attribute on field definitions (server-side, not just UI)
  • sudo() bypasses access rights — use deliberately and sparingly

FAQ

What happens if a user has no access rule for a model? +

They get an AccessError when trying to read, write, create, or delete records on that model. An empty CSV or missing access rule means no access at all — not full access. Security in Odoo is deny-by-default.

What is the difference between groups on a <field> in a view vs in the Python model? +

In a view, groups hides the field from the UI — it's still sent in RPC if you know the field name. In the Python model definition, groups prevents the field value from being sent at all in RPC responses, and makes it invisible in all views automatically. Use Python-level groups for true security, view-level groups for UX.

Can record rules apply to all users (not just a specific group)? +

Yes. A record rule with no groups field is a global rule — it applies to all users including administrators. Use global rules for fundamental data constraints like "never show archived records" or "multi-company isolation".

What does the (4, ref('base.group_user')) syntax mean? +

This is an Odoo ORM command for Many2many fields. 4 means "link this record without removing existing links". Other commands: 3 = unlink (disconnect), 6, 0, [ids] = replace all with these IDs. eval="[(4, ref('base.group_user'))]" links the base.group_user group to this record's Many2many field.