Security — Access Control in Odoo

Series: Odoo 18 Development for Python Developers Target Audience: Python developers who are new to web development and want to learn Odoo Prerequisites: Lessons 1–7 completed, library_app module with views, menus, and chatter working


Table of Contents

Odoo Security Model Overview

If you’ve been testing the library module as the admin user, everything works perfectly. But try logging in as a regular user — you’ll get “Access Denied” errors everywhere. That’s because Odoo’s security model follows a deny-by-default philosophy:

Nothing is accessible unless you explicitly grant access.

This is the opposite of most web frameworks (Django, Flask) where you add restrictions as needed. In Odoo, you add permissions as needed.

The Four Layers of Security

Odoo security works in layers, from broad to specific:

Layer 1: User Groups
  "Which team does this user belong to?"
  (e.g., Library User, Library Manager, Administrator)

    │
    ▼

Layer 2: Access Control Lists (ACL)
  "What can this group DO with this model?"
  (e.g., Library Users can read and create books, but not delete them)

    │
    ▼

Layer 3: Record Rules
  "Which RECORDS can this group see?"
  (e.g., Library Users can only see available books)

    │
    ▼

Layer 4: Field-Level Access
  "Which FIELDS can this group see?"
  (e.g., Only managers can see the cost_price field)

Each layer adds granularity. You need at least Layers 1–2 for your module to work for non-admin users.

How a Request is Checked

When a user tries to read a book record, Odoo checks:

  1. Is the user in a group that has ACL access to library.book? → If not, Access Denied
  2. Does any record rule restrict which books this user can see? → If so, filter the results
  3. Does the user’s group have access to all requested fields? → If not, remove restricted fields

The admin user (base.group_system) bypasses most checks, which is why everything works for admin.


User Groups (res.groups) and Module Categories

What is a Group?

A group is a collection of users who share the same permissions. Think of groups like roles:

  • Library User — Can view and borrow books
  • Library Manager — Can manage the entire library (create, edit, delete books and authors)

Module Categories

Groups are organized under categories (also called “Application” categories). Categories appear in the user settings form as sections.

Defining Groups in XML

Create the file security/library_security.xml:

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- ================================== -->
    <!--        MODULE CATEGORY             -->
    <!-- ================================== -->
    <record id="module_category_library" model="ir.module.category">
        <field name="name">Library</field>
        <field name="description">Library Management</field>
        <field name="sequence">200</field>
    </record>

    <!-- ================================== -->
    <!--          USER GROUPS               -->
    <!-- ================================== -->

    <!-- Group 1: Library User (basic access) -->
    <record id="group_library_user" model="res.groups">
        <field name="name">User</field>
        <field name="category_id" ref="module_category_library"/>
        <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
        <field name="comment">
            Library Users can view books and authors.
            They can borrow and return books.
        </field>
    </record>

    <!-- Group 2: Library Manager (full access) -->
    <record id="group_library_manager" model="res.groups">
        <field name="name">Manager</field>
        <field name="category_id" ref="module_category_library"/>
        <field name="implied_ids" eval="[(4, ref('group_library_user'))]"/>
        <field name="comment">
            Library Managers have full access to all library features.
            They can create, edit, and delete books, authors, and tags.
        </field>
    </record>

</odoo>

Let’s break this down piece by piece:

The Category Record

<record id="module_category_library" model="ir.module.category">
    <field name="name">Library</field>
    <field name="sequence">200</field>
</record>
  • model="ir.module.category" — This is the model that stores application categories
  • name — Appears as a section header in Settings → Users → user form
  • sequence — Position in the list (lower = higher)

The Group Records

<record id="group_library_user" model="res.groups">
    <field name="name">User</field>
    <field name="category_id" ref="module_category_library"/>
    <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
  • model="res.groups" — The model that stores security groups
  • name — The group name (shown as a radio button or checkbox in user settings)
  • category_id — Links to the category (so it appears under “Library” in settings)
  • implied_idsGroup inheritance. This group implies (includes) the specified groups

Group Inheritance with implied_ids

The implied_ids field creates a hierarchy of groups:

base.group_user (Internal User)
    └── group_library_user (Library User)
            └── group_library_manager (Library Manager)

This means:

  • A Library Manager automatically has all permissions of a Library User
  • A Library User automatically has all permissions of an Internal User (base.group_user)
  • You don’t need to duplicate permissions — they cascade down
<!-- Library User implies Internal User -->
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>

<!-- Library Manager implies Library User (which implies Internal User) -->
<field name="implied_ids" eval="[(4, ref('group_library_user'))]"/>

(4, ref('...')) is the “link” command for Many2many fields — we learned this in Lesson 4.

How Groups Appear in the UI

After installing the module, go to Settings → Users & Companies → Users. Open any user, and you’ll see a new “Library” section with two options:

Library
  ○ (empty)        — No library access
  ○ User           — Basic access
  ○ Manager        — Full access

The admin user can assign any user to one of these groups.


Access Control Lists (ACL) — ir.model.access.csv

ACLs define what operations a group can perform on a model. They answer the question: “Can users in this group Create, Read, Update, or Delete records in this model?”

The CSV File Format

Create the file security/ir.model.access.csv:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
library_book_user,library.book user,model_library_book,group_library_user,1,0,0,0
library_book_manager,library.book manager,model_library_book,group_library_manager,1,1,1,1
library_author_user,library.author user,model_library_author,group_library_user,1,0,0,0
library_author_manager,library.author manager,model_library_author,group_library_manager,1,1,1,1
library_tag_user,library.tag user,model_library_tag,group_library_user,1,0,0,0
library_tag_manager,library.tag manager,model_library_tag,group_library_manager,1,1,1,1
library_publisher_user,library.publisher user,model_library_publisher,group_library_user,1,0,0,0
library_publisher_manager,library.publisher manager,model_library_publisher,group_library_manager,1,1,1,1

This is a CSV file (Comma-Separated Values). Let’s understand each column:

Column Breakdown

Column Purpose Example
id Unique XML ID for this ACL record library_book_user
name Human-readable description library.book user
model_id:id The model to grant access to model_library_book
group_id:id The group receiving access group_library_user
perm_read Can read? (1=yes, 0=no) 1
perm_write Can update? (1=yes, 0=no) 0
perm_create Can create? (1=yes, 0=no) 0
perm_unlink Can delete? (1=yes, 0=no) 0

The model_id:id Naming Convention

The model_id:id value follows a specific pattern:

model_library_book
│     │       │
│     └───────┴── Model name with dots replaced by underscores
└── Always starts with 'model_'

Examples:

Model _name model_id:id
library.book model_library_book
library.author model_library_author
library.tag model_library_tag
res.partner model_res_partner

Reading the ACL Table

Let’s read our CSV as a table to understand the permissions:

Model Library User Library Manager
library.book Read only Full access (CRUD)
library.author Read only Full access
library.tag Read only Full access
library.publisher Read only Full access

Since Library Manager implies Library User, a manager has both rows applied. Odoo uses the most permissive combination:

  • Manager inherits User’s read=1 AND has their own read=1, write=1, create=1, unlink=1
  • Result: Manager has full CRUD access

Important Rules

  1. Every model needs at least one ACL line for each group that accesses it. If a group has no ACL for a model, users in that group get “Access Denied.”
  2. ACLs are additive. If a user belongs to multiple groups, they get the union (OR) of all permissions.
  3. Don’t forget transient models and related models. If your wizard uses library.book.borrow.wizard, that model needs ACL too.
  4. The admin group (base.group_system) has access to everything by default — you don’t need ACL lines for admins.

Common Mistake: Missing ACL

Error: You are not allowed to access 'Library Tag' (library.tag) records.

This error means the current user’s group doesn’t have any ACL line for library.tag. Fix: add a line in the CSV.


Record Rules (ir.rule) — Row-Level Security

ACLs control what operations a group can do. Record rules control which records a group can see. Think of it as adding a WHERE clause to every database query.

The Concept

Without record rules:

  • A Library User with read access to library.book sees ALL books

With record rules:

  • A Library User sees only books where state = 'available'
  • A Library Manager sees all books (no restriction)

Defining Record Rules in XML

Add these to security/library_security.xml:

    <!-- ================================== -->
    <!--          RECORD RULES              -->
    <!-- ================================== -->

    <!-- Rule 1: Library Users can only see available and borrowed books -->
    <record id="library_book_user_rule" model="ir.rule">
        <field name="name">Library Book: Users see available/borrowed</field>
        <field name="model_id" ref="model_library_book"/>
        <field name="domain_force">[('state', 'in', ['available', 'borrowed'])]</field>
        <field name="groups" eval="[(4, ref('group_library_user'))]"/>
        <field name="perm_read" eval="True"/>
        <field name="perm_write" eval="True"/>
        <field name="perm_create" eval="True"/>
        <field name="perm_unlink" eval="True"/>
    </record>

    <!-- Rule 2: Library Managers can see all books (no restriction) -->
    <record id="library_book_manager_rule" model="ir.rule">
        <field name="name">Library Book: Managers see all</field>
        <field name="model_id" ref="model_library_book"/>
        <field name="domain_force">[(1, '=', 1)]</field>
        <field name="groups" eval="[(4, ref('group_library_manager'))]"/>
        <field name="perm_read" eval="True"/>
        <field name="perm_write" eval="True"/>
        <field name="perm_create" eval="True"/>
        <field name="perm_unlink" eval="True"/>
    </record>

Record Rule Fields Explained

<record id="library_book_user_rule" model="ir.rule">
    <field name="name">Library Book: Users see available/borrowed</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="domain_force">[('state', 'in', ['available', 'borrowed'])]</field>
    <field name="groups" eval="[(4, ref('group_library_user'))]"/>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="True"/>
    <field name="perm_create" eval="True"/>
    <field name="perm_unlink" eval="True"/>
</record>
Field Purpose
name Human-readable description
model_id Which model this rule applies to
domain_force The domain filter (which records are accessible)
groups Which groups this rule applies to
perm_read Apply this rule to read operations?
perm_write Apply this rule to write operations?
perm_create Apply this rule to create operations?
perm_unlink Apply this rule to delete operations?

The domain_force Field

This is a domain (like we learned in Lesson 4) that filters which records are accessible:

<!-- Only see available and borrowed books -->
<field name="domain_force">[('state', 'in', ['available', 'borrowed'])]</field>

<!-- See ALL records (no restriction) -->
<field name="domain_force">[(1, '=', 1)]</field>
<!-- (1, '=', 1) is always True — a common pattern for "no restriction" -->

<!-- Only see own records (records created by this user) -->
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<!-- 'user' is a special variable referring to the current user -->

<!-- Only see records from user's company -->
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<!-- 'company_ids' refers to the companies the user has access to -->

Special Variables in domain_force

Inside domain_force, you can use these special variables:

Variable Type Meaning
user res.users recordset The current user
user.id Integer The current user’s ID
company_id Integer The current user’s active company ID
company_ids List of integers All companies the user has access to
time Python time module For date comparisons
<!-- Example: Only see books that belong to the user's company -->
<field name="domain_force">[('company_id', '=', company_id)]</field>

<!-- Example: Only see books created in the last 30 days -->
<field name="domain_force">
    [('create_date', '&gt;=', (datetime.datetime.now() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]
</field>

How Multiple Record Rules Interact

Rules for the same group and model are combined with OR (union):

Rule A: domain = [('state', '=', 'available')]  → for group_library_user
Rule B: domain = [('state', '=', 'borrowed')]   → for group_library_user

Result: User sees records where state = 'available' OR state = 'borrowed'

Rules for different groups are combined with OR (if the user is in both groups):

Rule A: domain = [('state', '=', 'available')]  → for group_library_user
Rule C: domain = [(1, '=', 1)]                  → for group_library_manager

If user is a Manager (which implies User):
Result: User sees ALL records (because Rule C matches everything)

Global Rules (No Group)

If you don’t assign a group to a record rule, it becomes a global rule — applied to ALL users (except superuser):

<!-- Global rule: nobody can see lost books (not even managers) -->
<record id="library_book_hide_lost_rule" model="ir.rule">
    <field name="name">Library Book: Hide lost books globally</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="domain_force">[('state', '!=', 'lost')]</field>
    <!-- No 'groups' field = global rule -->
</record>

Global rules are combined with AND with group rules. This means:

  • Group rule says: user can see books where state in ('available', 'borrowed')
  • Global rule says: nobody can see books where state = 'lost'
  • Combined: user sees books where state in ('available', 'borrowed') AND state != 'lost'

Use global rules sparingly — they restrict everyone, including managers.


Field-Level Access: groups Attribute on Fields

For finer control, you can restrict individual fields to specific groups:

# In models/book.py

cost_price = fields.Float(
    string='Cost Price',
    groups='library_app.group_library_manager',
    # Only managers can see this field
)

internal_notes = fields.Text(
    string='Internal Notes',
    groups='library_app.group_library_manager,base.group_system',
    # Visible to library managers AND system administrators
    # Multiple groups separated by commas = OR logic
)

What happens in practice:

  • If a Library User opens the book form, cost_price is completely invisible — the field doesn’t appear in the UI and is stripped from API responses
  • If a Library Manager opens the same form, cost_price is visible and editable
  • The groups attribute works on both Python field definitions and XML view elements

Groups on View Elements

You can also use groups in XML views:

<!-- Only managers see this group of fields -->
<group string="Financial" groups="library_app.group_library_manager">
    <field name="cost_price"/>
    <field name="profit_margin"/>
</group>

<!-- Only managers see this button -->
<button name="action_mark_lost"
        type="object"
        string="Mark as Lost"
        groups="library_app.group_library_manager"/>

<!-- Only managers see this menu -->
<menuitem
    id="library_config_menu"
    name="Configuration"
    parent="library_root_menu"
    groups="library_app.group_library_manager"
    sequence="90"/>

groups on Fields vs Views

Where Effect
On Python field Field is completely hidden from the ORM for unauthorized users (both UI and API)
On XML view element Element is hidden in the UI, but the field is still accessible via the API

For true security, put groups on the Python field. The XML groups attribute is more of a UI convenience — a determined user could still access the field through the API if it’s not restricted at the Python level.


Superuser vs sudo() — When and Why

The Superuser

Odoo has a special superuser (also called the __system__ user). This user:

  • Bypasses ALL security: ACLs, record rules, field-level groups
  • Cannot log in through the web interface
  • Is only accessible through code (self.env.su)
  • Has user.id = SUPERUSER_ID (usually 1)

The admin user (the one you log in with) is NOT the superuser. The admin user belongs to base.group_system, which has broad permissions — but still follows record rules and ACLs.

sudo() Recap

From Lesson 5, we know sudo() switches to the superuser:

# Normal: follows current user's security rules
books = self.env['library.book'].search([])

# sudo(): bypasses ALL security
all_books = self.env['library.book'].sudo().search([])

When to Use sudo()

Scenario Use sudo()? Why
Business logic needs to read a model the user can’t access ✅ Yes e.g., reading system settings
Creating log records in a restricted model ✅ Yes Users shouldn’t need direct access to log models
Checking permissions for another user ✅ Yes Need to read group memberships
Displaying data to the user ❌ No Users would see data they shouldn’t
Bypassing broken security rules ❌ No Fix the rules instead
“Making things work” without understanding why ❌ No You’re hiding a real problem

Best Practice Pattern

def action_borrow_book(self):
    self.ensure_one()

    # Check something with sudo (need to read system config)
    max_days = int(
        self.env['ir.config_parameter']
        .sudo()
        .get_param('library.max_borrow_days', '14')
    )

    # Business logic runs with NORMAL permissions
    # (don't use sudo for the actual operation)
    self.write({
        'state': 'borrowed',
        'borrow_date': fields.Date.today(),
        'return_date': fields.Date.today() + timedelta(days=max_days),
    })

Multi-Company Security Rules

If your Odoo instance has multiple companies, you’ll need multi-company security to ensure users only see data from their own company.

Adding Company Support to Your Model

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

    company_id = fields.Many2one(
        'res.company',
        string='Company',
        required=True,
        default=lambda self: self.env.company,
        # Default to the current user's active company
    )

Company Record Rule

<record id="library_book_company_rule" model="ir.rule">
    <field name="name">Library Book: Multi-Company</field>
    <field name="model_id" ref="model_library_book"/>
    <field name="domain_force">[('company_id', 'in', company_ids)]</field>
    <!-- 'company_ids' = companies the user has access to -->
    <field name="global" eval="True"/>
    <!-- global=True means this applies to ALL users -->
</record>

For our library module, we won’t add multi-company support in the hands-on (to keep things simple). But in production, most business models should have a company_id field and a corresponding rule.


Common Security Pitfalls for New Developers

Pitfall 1: Forgetting ACL for a Model

Symptom: “You are not allowed to access ‘Library Tag’ records”

Fix: Add a line in ir.model.access.csv for the missing model.

Prevention: Every time you create a new model, immediately add ACL lines for it.

Symptom: The book form loads, but the Author dropdown is empty or shows an error.

Cause: The user has access to library.book but not to library.author. The Many2one dropdown tries to search library.author and fails.

Fix: Add read access to library.author for the same group.

Pitfall 3: Using sudo() Instead of Fixing Security

Symptom: Your code is full of .sudo() calls.

Cause: The security rules are too restrictive or missing.

Fix: Review and fix your ACLs and record rules. Reserve sudo() for legitimate cases (system config, cross-model operations).

Pitfall 4: Record Rule Blocks Create/Write

Symptom: User can see records but gets “Access Error” when trying to save.

Cause: The record rule’s domain filters out the record the user is trying to create (e.g., creating a draft book when the rule only allows state = 'available').

Fix: Make sure the record rule domain allows the states that users need to create. Or use separate perm_read / perm_create rules:

<!-- Rule for reading: only see available/borrowed -->
<record id="rule_read" model="ir.rule">
    <field name="domain_force">[('state', 'in', ['available', 'borrowed'])]</field>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="False"/>
    <field name="perm_create" eval="False"/>
    <field name="perm_unlink" eval="False"/>
</record>

<!-- Rule for writing: can edit any non-lost book -->
<record id="rule_write" model="ir.rule">
    <field name="domain_force">[('state', '!=', 'lost')]</field>
    <field name="perm_read" eval="False"/>
    <field name="perm_write" eval="True"/>
    <field name="perm_create" eval="True"/>
    <field name="perm_unlink" eval="False"/>
</record>

Pitfall 5: Forgetting noupdate

When you define record rules and later change them, Odoo might not update them because they’re protected by noupdate. We’ll cover this in Lesson 10 (Data Files), but for now:

  • During development, you can manually delete old rules in Settings → Technical → Record Rules
  • Or wrap your data in to force updates

Security Checklist

Before releasing a module, check:

  • [ ] All models have ACL entries in ir.model.access.csv
  • [ ] At least two groups defined (User and Manager or similar)
  • [ ] Record rules defined for sensitive data
  • [ ] groups attribute on sensitive fields (cost, profit, internal notes)
  • [ ] Menus restricted with groups where appropriate
  • [ ] sudo() used only where truly necessary
  • [ ] Tested with a non-admin user

Hands-on: Define Groups, ACL, and Record Rules for the Library Module

Let’s make our library module work for non-admin users.

Step 1: Create security/library_security.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="0">

        <!-- ================================== -->
        <!--        MODULE CATEGORY             -->
        <!-- ================================== -->
        <record id="module_category_library" model="ir.module.category">
            <field name="name">Library</field>
            <field name="description">Library Management Application</field>
            <field name="sequence">200</field>
        </record>

        <!-- ================================== -->
        <!--          USER GROUPS               -->
        <!-- ================================== -->

        <!-- Library User: can read books/authors/tags, borrow and return books -->
        <record id="group_library_user" model="res.groups">
            <field name="name">User</field>
            <field name="category_id" ref="module_category_library"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="comment">
                Library Users can browse the book catalog,
                view author information, and borrow/return books.
            </field>
        </record>

        <!-- Library Manager: full access to all library features -->
        <record id="group_library_manager" model="res.groups">
            <field name="name">Manager</field>
            <field name="category_id" ref="module_category_library"/>
            <field name="implied_ids" eval="[(4, ref('group_library_user'))]"/>
            <field name="comment">
                Library Managers have full control over books, authors,
                tags, and all library configuration.
            </field>
        </record>

        <!-- ================================== -->
        <!--          RECORD RULES              -->
        <!-- ================================== -->

        <!-- Users: can read all active books (any state) -->
        <record id="library_book_user_rule" model="ir.rule">
            <field name="name">Library Book: User access</field>
            <field name="model_id" ref="model_library_book"/>
            <field name="domain_force">[('active', '=', True)]</field>
            <field name="groups" eval="[(4, ref('group_library_user'))]"/>
        </record>

        <!-- Managers: can access all books including archived -->
        <record id="library_book_manager_rule" model="ir.rule">
            <field name="name">Library Book: Manager full access</field>
            <field name="model_id" ref="model_library_book"/>
            <field name="domain_force">[(1, '=', 1)]</field>
            <field name="groups" eval="[(4, ref('group_library_manager'))]"/>
        </record>

    </data>
</odoo>

Step 2: Create security/ir.model.access.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,group_library_user,1,1,0,0
access_library_book_manager,library.book.manager,model_library_book,group_library_manager,1,1,1,1
access_library_author_user,library.author.user,model_library_author,group_library_user,1,0,0,0
access_library_author_manager,library.author.manager,model_library_author,group_library_manager,1,1,1,1
access_library_tag_user,library.tag.user,model_library_tag,group_library_user,1,0,0,0
access_library_tag_manager,library.tag.manager,model_library_tag,group_library_manager,1,1,1,1
access_library_publisher_user,library.publisher.user,model_library_publisher,group_library_user,1,0,0,0
access_library_publisher_manager,library.publisher.manager,model_library_publisher,group_library_manager,1,1,1,1

Permission breakdown:

Model Library User Library Manager
Book Read + Write (can borrow/return) Full CRUD
Author Read only Full CRUD
Tag Read only Full CRUD
Publisher Read only Full CRUD

Library Users can write to books (to change state when borrowing/returning) but cannot create or delete books.

Step 3: Update __manifest__.py

Make sure security files are loaded first in the data list:

'data': [
    'security/library_security.xml',     # Groups and record rules (FIRST)
    'security/ir.model.access.csv',      # ACLs (after groups)
    'views/book_views.xml',
    'views/author_views.xml',
    'views/tag_views.xml',
    'views/menu_views.xml',              # Menus (LAST)
],

Step 4: Add groups to the Configuration Menu

Update views/menu_views.xml to restrict the Configuration menu to managers:

<menuitem
    id="library_config_menu"
    name="Configuration"
    parent="library_root_menu"
    groups="group_library_manager"
    sequence="90"/>

Step 5: Upgrade and Test

# Upgrade
docker compose exec odoo odoo -d odoo18dev -u library_app --stop-after-init
docker compose restart odoo

Step 6: Create a Test User

  1. Go to Settings → Users & Companies → Users
  2. Click “New”
  3. Fill in:

– Name: Library Test User – Email: libraryuser@example.com – Password: (set one)

  1. In the “Library” section, select “User”
  2. Save

Step 7: Test as the Library User

  1. Log out of the admin account
  2. Log in as Library Test User
  3. Check:

– ✅ Can you see the Library menu? – ✅ Can you see the book list? – ✅ Can you open a book form and view details? – ✅ Can you click “Borrow” and “Return” buttons? – ❌ Can you create a new book? (Should NOT be allowed) – ❌ Can you delete a book? (Should NOT be allowed) – ❌ Can you see the Configuration menu? (Should be hidden)

If all checks pass, your security is working correctly.


Summary & What’s Next

Key Takeaways

  1. Odoo security is deny-by-default. You must explicitly grant access.
  2. Four security layers: Groups → ACL → Record Rules → Field-Level.
  3. Groups define user roles. Use implied_ids for hierarchy (Manager implies User).
  4. ACLs (ir.model.access.csv) control CRUD operations per model per group. Every model needs ACL entries.
  5. Record Rules (ir.rule) add domain filters to restrict which records a group can see. Group rules combine with OR; global rules combine with AND.
  6. Field-level groups hide sensitive fields from unauthorized users. Put groups on the Python field for true security.
  7. sudo() bypasses all security — use it sparingly and only for legitimate purposes.
  8. Test with a non-admin user before releasing your module.

Security Cheat Sheet

# Define groups in XML:
<record id="group_library_user" model="res.groups">
    <field name="name">User</field>
    <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>

# ACL in CSV:
# id, name, model_id:id, group_id:id, read, write, create, delete
access_book_user,book.user,model_library_book,group_library_user,1,0,0,0

# Record Rule in XML:
<record id="book_rule" model="ir.rule">
    <field name="model_id" ref="model_library_book"/>
    <field name="domain_force">[('state', '=', 'available')]</field>
    <field name="groups" eval="[(4, ref('group_library_user'))]"/>
</record>

# Field-level security:
cost = fields.Float(groups='library_app.group_library_manager')

# Menu security:
<menuitem groups="library_app.group_library_manager"/>

What’s Next?

In Lesson 9: Inheritance — Odoo’s Most Powerful Pattern, we’ll learn:

  • Classical inheritance (extending existing models)
  • Prototype inheritance (copying models)
  • Delegation inheritance (_inherits)
  • View inheritance deep dive (XPath revisited)
  • Extending built-in models like res.partner

Inheritance is how professional Odoo development works — you rarely build from scratch, you extend what exists.


Exercises: 1. Create a third group group_library_librarian that sits between User and Manager. Librarians can create and edit books but cannot delete them. Update the ACL accordingly. 2. Add a record rule so that Library Users can only see books that are available (not borrowed, not draft, not lost). Test by logging in as the test user. 3. Add a cost_price field to library.book with groups='library_app.group_library_manager'. Verify that the field is invisible when logged in as a Library User. 4. Test what happens when you try to access a model without ACL: – Remove the ACL line for library.tag for the User group – Upgrade the module – Log in as a Library User and try to open a book with tags – What error do you see? Restore the ACL and upgrade again 5. Create a global record rule that hides all books with state = 'lost' from everyone (including managers). Test to verify even the admin can’t see lost books. Then remove the rule (global rules can be dangerous!).


Previous lesson: Lesson 7 — Advanced Views & Widgets Next lesson: Lesson 9 — Inheritance: Odoo’s Most Powerful Pattern

Leave a Reply

Your email address will not be published. Required fields are marked *