Quick Answer
Stop data breaches costing $50,000-$200,000. 50 staff members need different access: sales reps (only THEIR orders), warehouse (all shipping orders), finance (all invoices), managers (everything). Wrong: Privacy breaches, compliance issues, security risks, everyone has admin access. Right: Use Access Rights (model-level CSV) + Record Rules (row-level XML domains). Result: each person sees EXACTLY what they need, zero breaches, clean audits.
The Security Problem
Your D2C brand has 50 staff members:
❏ Sales reps (should see only THEIR orders)
❏ Warehouse team (should see all orders that need shipping)
❏ Finance team (should see all invoices)
❏ Managers (should see everything)
Get Permissions Wrong
❌ Sales rep A sees sales rep B's orders (privacy breach)
❌ Warehouse sees invoices they shouldn't (compliance issue)
❌ Finance sees customer data they don't need (security risk)
❌ Everyone has admin access (nightmare scenario)
Get Permissions Right
✓ Each person sees EXACTLY what they need
✓ Customers trust you with their data
✓ Compliance audits pass easily
✓ Your team is productive
We've implemented 150+ Odoo systems. The ones where developers understand security? They have zero data breaches, clean audit trails, and confident customers. The ones who don't? They've had data leaks costing $50,000-$200,000 in incident response, notification, and lawsuits. That's not acceptable.
The Security Framework (Two Layers)
| Layer | Type | Format | Purpose |
|---|---|---|---|
| Access Rights | Model-level | CSV file | Can user create/read/write/delete this model? |
| Record Rules | Row-level | XML domains | Which specific records can user see? |
How They Work Together
Step 1 - ACCESS RIGHTS: Does user have READ permission on sale.order model? NO → Access denied. YES → Continue.
Step 2 - RECORD RULES: Does specific record match rule domain? For sales rep: only records where salesperson = current user. NO → Record hidden. YES → Show record.
Part 1: Access Rights (Model-Level Security)
What it is: CSV file that defines which groups can create, read, write, delete records in each model.
Module Structure
custom_module/
├── security/
│ ├── ir.model.access.csv
│ └── record_rules.xml
Create ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_order_user,sale.order user access,sale.model_sale_order,sales_team.group_sale_salesman,1,1,1,0
access_order_manager,sale.order manager access,sale.model_sale_order,sales_team.group_sale_manager,1,1,1,1
access_invoice_finance,account.invoice finance,account.model_account_move,account.group_account_invoice,1,1,1,0
access_invoice_accountant,account.invoice accountant,account.model_account_move,account.group_account_accountant,1,1,1,1
CSV Column Meanings
| Column | Description |
|---|---|
| id | Unique identifier for this access rule |
| name | Human-readable description |
| model_id:id | Reference to model (e.g., sale.model_sale_order) |
| group_id:id | Which group gets this access |
| perm_read | 1 = can read, 0 = cannot read |
| perm_write | 1 = can write, 0 = cannot write |
| perm_create | 1 = can create, 0 = cannot create |
| perm_unlink | 1 = can delete, 0 = cannot delete |
Real D2C Example: Order Access by Role
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
# Sales reps can read/write/create, but NOT delete orders
access_order_salesman,Order - Salesman,sale.model_sale_order,sales_team.group_sale_salesman,1,1,1,0
# Sales managers can do everything with orders
access_order_manager,Order - Manager,sale.model_sale_order,sales_team.group_sale_manager,1,1,1,1
# Warehouse staff can read/write (mark shipped), but NOT delete
access_order_warehouse,Order - Warehouse,sale.model_sale_order,stock.group_stock_user,1,1,0,0
# Finance can only read orders
access_order_finance,Order - Finance,account.model_account_invoice,account.group_account_invoice,1,0,0,0
Part 2: Record Rules (Record-Level Security)
What it is: XML file that defines CONDITIONS for accessing specific records.
Example: Sales rep only sees their own orders
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- RECORD RULE: Salesman only sees own orders -->
<record id="sale_order_rule_own_records" model="ir.rule">
<!-- Rule name -->
<field name="name">Order - Personal (Salesman)</field>
<!-- Which model -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- Which group(s) this applies to -->
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<!-- The filter domain (only records matching this) -->
<!-- This sales rep only sees orders assigned to them -->
<field name="domain_force">[('user_id', '=', user.id)]</field>
<!-- Allow reading -->
<field name="perm_read">1</field>
<!-- Allow writing -->
<field name="perm_write">1</field>
<!-- Disallow creation outside own records -->
<field name="perm_create">0</field>
<!-- Disallow deletion -->
<field name="perm_unlink">0</field>
</record>
<!-- RECORD RULE: Manager sees all orders -->
<record id="sale_order_rule_all_records" model="ir.rule">
<field name="name">Order - All Records (Manager)</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<!-- This domain [(1, '=', 1)] means "always true" - no restriction -->
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>
</odoo>
Advanced Pattern: Multi-Company Data Isolation
Real scenario: You have 3 companies sharing one Odoo instance. No cross-company visibility.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- GLOBAL RULE: Everyone only sees their company's data -->
<!-- This applies to ALL users (no group specified) -->
<record id="order_rule_company_isolation" model="ir.rule">
<field name="name">Order - Company Isolation (Global)</field>
<field name="model_id" ref="sale.model_sale_order"/>
<!-- NO group specified = applies to ALL users -->
<!-- This is a GLOBAL rule (highest priority) -->
<!-- Users can only see orders where company = their company -->
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
<!-- Global rule applies to all operations -->
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>
</odoo>
Record Rule Domain Examples (Common Patterns)
<!-- Only see records created by current user -->
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<!-- Only see records assigned to current user -->
<field name="domain_force">[('user_id', '=', user.id)]</field>
<!-- Only see records in user's company -->
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
<!-- Only see records in user's department -->
<field name="domain_force">[('department_id', '=', user.employee_id.department_id.id)]</field>
<!-- Only see records with specific status -->
<field name="domain_force">[('state', 'in', ['draft', 'sent'])]</field>
<!-- Complex: Own orders OR created by my team -->
<field name="domain_force">['|', ('user_id', '=', user.id), ('team_id', '=', user.sale_team_id.id)]</field>
<!-- No restrictions (manager sees all) -->
<field name="domain_force">[(1, '=', 1)]</field>
Creating Custom User Groups
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- SALES TEAM MANAGER group -->
<record id="group_sales_manager" model="res.groups">
<field name="name">Sales Manager</field>
<!-- Category in Settings → Users & Groups menu -->
<field name="category_id" ref="base.module_category_sales_management"/>
<!-- Users in this group get these permissions -->
<field name="implied_ids" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
</record>
<!-- CUSTOMER SERVICE group -->
<record id="group_customer_service" model="res.groups">
<field name="name">Customer Service</field>
<field name="category_id" ref="base.module_category_sales_management"/>
</record>
<!-- INVENTORY MANAGER group -->
<record id="group_inventory_manager" model="res.groups">
<field name="name">Inventory Manager</field>
<field name="category_id" ref="base.module_category_inventory"/>
</record>
</odoo>
Action Items: Implement Security
Plan Your Security
❏ List all user roles (sales rep, manager, warehouse, finance, etc.)
❏ For each role, list what they should see/do
❏ Map to Odoo models and operations
❏ Create access rights in CSV
❏ Create record rules for row-level filtering
Implement
❏ Create security/ folder
❏ Write ir.model.access.csv with model permissions
❏ Write record_rules.xml with domain filters
❏ Add to __manifest__.py
❏ Create test users in each group
❏ Test access from each user's perspective
Test Thoroughly
❏ Login as each user role
❏ Verify they see ONLY allowed records
❏ Verify they can ONLY do allowed operations
❏ Test edge cases (deletion, bulk actions)
❏ Test cross-company if applicable
Frequently Asked Questions
What's the difference between Access Rights and Record Rules?
Access Rights (CSV): Model-level security. "Can user read/write/create/delete sale.order?" Record Rules (XML): Row-level security. "Which specific sale.order records can user see?" Use both together—Access Rights first, then Record Rules filter visible records.
How do I restrict sales reps to see only their own orders?
Create record rule with domain_force="[('user_id', '=', user.id)]" applied to sales_team.group_sale_salesman group. This filters orders where assigned salesperson = current user. Sales managers get separate rule with [(1, '=', 1)] (no restriction).
What are Global Rules and when should I use them?
Global Rules have no groups specified = apply to ALL users (highest priority). Use for multi-company isolation: [('company_id', '=', user.company_id.id)]. Every user must satisfy ALL global rules before group-specific rules apply.
How much does poor security cost vs. implementing it properly?
Poor security: Data breaches cost $50,000-$200,000 in incident response, customer notification, lawsuits, compliance fines. Proper security: Zero breaches, clean audit trails, confident customers, compliance audits pass easily. Implementing proper security upfront prevents massive incident costs.
Free Odoo Security Audit Workshop
Stop guessing about security. We'll review your current access structure, identify security gaps and vulnerabilities, design role-based access model, implement access rights and record rules, test with dummy users, and document your security policy. Most D2C brands discover they have security holes only after a breach. Implementing proper security upfront prevents $50,000-$200,000 in incident costs.
