Odoo Developer Guide: Data Security & Access Controls in Odoo

About This Guide

This guide breaks down how Odoo protects your data and controls user permissions across your business. It explains how roles, permissions, and security rules work together to ensure employees only see and manage the specific records they need—keeping your business information safe and organized.

What You Will Learn

  • User Roles & Access Control: How to set up job roles and assign specific read, edit, or delete permissions across your business modules.
  • Row-Level Security: How to automatically restrict record visibility so users only view data relevant to their role or branch.
  • Field Privacy: How to hide sensitive information (like private medical notes or financial data) from unauthorized team members.
  • Best Practices: How to avoid common configuration mistakes and keep your overall system safe from unintended access.

Odoo provides two main data-driven mechanisms to manage or restrict access to data without writing custom hardcoded logic. Both mechanisms link to users through User Groups (res.groups): a user belongs to any number of groups, and security mechanisms are attached to groups to govern permissions.

This guide provides a comprehensive breakdown of Odoo security architecture using a Hospital Management (custom_hospital_management) module as an example.

Group Architecture (res.groups)

User groups define roles and serve as the foundation for both Access Rights and Record Rules.

Model Attributes (res.groups)

  • tname: Serves as user-readable identification for the group (spells out the role or purpose of the group, e.g., “Doctor”, “Patient Administrator”).
  • category_id: The module category. Associates groups with an Odoo App (a set of related business models) and converts them into an exclusive selection box on the User setup form.
  • implied_ids: Other groups to assign to the user alongside this one. This acts as a convenience pseudo-inheritance relationship (e.g., a “Hospital Manager” group implies the “Hospital User” group). It remains possible to explicitly remove implied groups from a user without removing the main group.
  • comment: Additional technical notes or descriptions detailing the purpose of the group.

Access Rights (ir.model.access)

Access Control Lists (ACLs) grant access to an entire model for a given set of operations (Create, Read, Update, Delete). If no access right matches an operation on a model for a user (through their assigned groups), the user is denied access.

Core Properties

  • Additive Nature: Access rights are cumulative. A user’s total access is the union of access rights from all groups they belong to.
    Example: If Group A grants Read and Create, and Group B grants Update, a user in both groups has Read, Create, and Update access.
  • Default Behavior: Unmatched operations default to denied access.

Model Attributes (ir.model.access)

  • name: Descriptive purpose or role of the access control record.
  • model_id: The target model whose access the ACL controls (formatted as model_<model_name_with_underscores>).
  • group_id: The res.groups record to which access is granted. Leaving group_id empty grants access to every user (including non-employees such as portal or public users).
  • CRUD Attributes (perm_*): Grant the corresponding operation when set to 1 (True). All permissions are unset (0 / False) by default:
    • perm_read: Read/View records.
    • perm_create: Create new records.
    • perm_write: Modify existing records.
    • perm_unlink: Delete records.

Implementation Example: Hospital Management

File location: custom_hospital_management/security/ir.model.access.csv

Code snippet

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

access_hospital_patient_user,access.hospital.patient.user,model_hospital_patient,base.group_user,1,1,1,0

In this configuration, standard internal users (base.group_user) can Read, Write, and Create patient records (hospital.patient), but cannot Delete (perm_unlink = 0) them.

Record Rules (ir.rule)

Record rules are row-level conditions evaluated record-by-record after Access Rights pass. While Access Rights grant access to the table, Record Rules filter which individual rows inside that table a user can see or modify.

Core Properties

  • Default-Allow: If Access Rights grant permission and no record rule applies to the operation/model for the user, access is allowed.
  • Operation Selection: Unlike ir.model.access, setting perm_* flags on an ir.rule determines which operations the rule actively filters. If an operation is unset (False), the rule ignores that operation and permits it freely. All operations are selected (True) by default.

Model Attributes (ir.rule)

  • name: Description of the rule.
  • model_id: The model to which the rule applies.
  • groups: The res.groups to which the rule applies. If no group is specified, the rule is marked as Global.
  • global: Computed field indicating whether the rule applies globally across all users.
  • domain_force: A domain predicate (Python expression). Records matching the domain are allowed; non-matching records are forbidden.
  • CRUD Operation Flags: perm_read, perm_write, perm_create, perm_unlink.

Available Domain Evaluation Context

When writing expressions inside domain_force, Odoo provides the following evaluation variables:

  • user: The current user record (as a singleton recordset).
  • company_id: The user’s currently active company ID (integer).
  • company_ids: All company IDs accessible to the user (list of integers).
  • time: Python’s native time module.

Global Rules vs. Group Rules

 

Rule Type

Composition Behavior

Technical Rule

Global Rules (groups empty)

Intersection (AND)

Adding global rules always restricts access further. All global rules must be satisfied simultaneously.

Group Rules (groups specified)

Unification (OR)

Adding group rules expands access. If any group rule matches, access is granted.

Combined Evaluation

Intersection (AND)

Global rulesets and Group rulesets intersect. The user must pass ALL Global rules AND at least ONE matching Group rule.

DANGER: Creating multiple Global rules is risky. If two global rules are mutually exclusive (non-overlapping), all user access to the model will be completely blocked.

Field-Level Security

Fields can be restricted directly in Python models using the groups attribute:

Python

class HospitalPatient(models.Model):

    _name = ‘hospital.patient’

    _description = ‘Hospital Patient Record’

 

    name = fields.Char(string=’Full Name’, required=True)

    # Medical notes restricted strictly to Healthcare Managers

 

    note = fields.Text(string=’Medical History’, groups=’custom_hospital_management.group_hospital_manager’)

 

Security Enforcement Controls:

  1. Restricted fields are automatically stripped from view rendering XML layouts.
  2. Restricted fields are removed from fields_get() ORM calls.
  3. Direct attempts to read or write to restricted fields via RPC or Python trigger an explicit AccessError.

Security Pitfalls & Developer Guidelines

A. Unsafe Public Methods

Any public Python method (methods not starting with _) can be executed remotely via RPC calls. Access Rights are only enforced automatically during standard ORM CRUD operations.

Python

#INCORRECT: Arguments and state execution cannot be trusted

def action_discharge(self):

    if self.state == “draft” and self.env.user.has_group(‘base.group_user’):

        self._set_state(“discharged”)

 

#CORRECT: Keep business execution private, apply explicit checks

def _set_state(self, new_state):

 

    self.sudo().write({“state”: new_state})

 

B. Direct Database Cursor Access & SQL Injections

Never bypass the ORM to execute raw SQL queries via self.env.cr when ORM methods (search, write) can perform the task. Bypassing the ORM skips automated security checks, domain filtering, and record-level rules.

Vulnerability Examples & Fixes:

Python

# CRITICAL VULNERABILITY: SQL Injection via string concatenation

self.env.cr.execute(‘SELECT id FROM hospital_patient WHERE state = ‘ + user_input)

 

# POOR PRACTICE: Bypassing ORM security logic manually

self.env.cr.execute(‘SELECT id FROM hospital_patient WHERE state = %s’, (state_val,))

 

# CORRECT: Clean ORM usage enforcing access rights & domain safety

patients = self.search([(‘state’, ‘=’, state_val)])

 

If manual SQL queries are unavoidable, strictly use Odoo’s SQL wrapper to prevent injection:

Python

from odoo.tools import SQL

 

# SECURE: Safe parameter handling via SQL wrapper

self.env.cr.execute(SQL(“””

    SELECT id FROM hospital_patient 

    WHERE age > %s AND state = %s

 

“””, min_age, state_val))

 

C. Safe Domain Construction

Do not concatenate raw lists to build search domains, as unnormalized user inputs can manipulate domain logic. Use the Domain utility object:

Python

from odoo.osv.expression import Domain

 

# UNSAFE: User domain can override security rules via logical ORs

domain = user_provided_domain 

domain += [(‘create_uid’, ‘=’, self.env.uid)]

 

# SECURE: Enforce security parameters using domain intersection

domain = Domain(user_provided_domain) & Domain([(‘create_uid’, ‘=’, self.env.uid)])

 

patients = self.search(domain)

 

D. Cross-Site Scripting (XSS) Prevention & HTML Escaping

  • Never use t-raw in QWeb: Always use t-esc to render string variables safely in user interfaces.
  • Escaping vs. Sanitizing:
    • Escaping (html_escape): Converts plain text to safe HTML markup. Mandatory every time untrusted text is mixed into HTML strings.
    • Sanitizing (html_sanitize): Cleans existing HTML code by stripping malicious tags (<script>) or inline styles.

Safe HTML Generation using Markup:

Python

from odoo.tools import Markup, escape

 

# UNSAFE: Raw dynamic strings inserted directly into HTML structure

html_out = f”<div>User: {patient.name}</div>”

 

# SECURE: Using Markup to ensure variables are safely escaped

 

html_out = Markup(“<div>User: %s</div>”) % patient.name

 

E. Dynamic Attribute Access

Do not use generic getattr() or setattr() calls on ORM records, as they expose internal private methods and system attributes. Access fields dynamically using record dictionary indexing:

Python

# UNSAFE: Gives full access to internal private methods/attributes

value = getattr(patient_record, user_input_field)

 

# SECURE: Restricts dynamic access strictly to model fields

 

value = patient_record[user_input_field]

 

Safe Content Evaluation Reference Table

Avoid using Python’s eval() or safe_eval() to parse string inputs. Use specific non-evaluating data parsers:

Data Type

Target Language

Safe Parser

Integer / Float

Python

int(), float()

Integer / Float

JavaScript

parseInt(), parseFloat()

Dictionaries / Lists

Python

json.loads(), ast.literal_eval()

Objects / Arrays

JavaScript

JSON.parse()