5.1 规则回答什么问题
记录规则在 ACL 已经放行之后回答:用户对哪些记录执行该操作?
如果模型 read ACL 不通过,记录规则不会把 read 权限“授予回来”。规则是范围过滤器,不是模型权限 grant。
5.2 ir.rule 的重要字段
定义见 odoo/addons/base/models/ir_rule.py:12-32:
| 字段 | 说明 |
|---|---|
name |
规则名称 |
active |
是否启用 |
model_id |
目标模型 |
groups |
适用组;为空即全局规则 |
global |
not groups 的计算结果 |
domain_force |
Python 表达式形式的 Odoo domain |
perm_read |
read 时是否应用该规则 |
perm_write |
write 时是否应用该规则 |
perm_create |
create 时是否应用该规则 |
perm_unlink |
unlink 时是否应用该规则 |
至少一个 perm_* 必须为 True。再次强调:perm_write=False 是“写时忽略此规则”,不是“禁止写”。要禁止整个模型 write,应撤销 write ACL;要限制哪些记录能写,应让一条 perm_write=True 的规则只匹配允许记录。
5.3 domain 的求值上下文
_eval_context() 只正式提供:
| 变量 | 内容 |
|---|---|
user |
当前用户;env.user 本身以 sudo 取得,再清空 context |
time |
safe_eval 中的时间模块 |
company_id |
当前主公司 ID,即 env.company.id |
company_ids |
公司切换器中当前启用的公司 ID,即 env.companies.ids |
源码:ir_rule.py:35-50。
例子:
<field name="domain_force">[('owner_id', '=', user.id)]</field>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="domain_force">['|', ('user_id', '=', user.id), ('user_id', '=', False)]</field>
规则表达式由受信管理员维护。这里的 user 是 sudoed user record,可信表达式能读取受限用户字段,甚至可借 user.env 发起搜索。safe_eval 不是给不受信业务人员的完整安全沙箱;表达式还可能形成昂贵搜索或复杂 SQL,因此不应向普通业务管理员开放任意 Python domain 编辑器。
5.4 全局 AND、组规则 OR
_compute_domain() 的核心逻辑在 ir_rule.py:140-168:
# 语义化伪代码
global_domains = [domain(rule) for rule in global_rules]
group_domains = [domain(rule) for rule in matching_group_rules]
if not group_domains:
final = AND(global_domains)
else:
final = AND(global_domains, OR(group_domains))
假设有三条规则:
G1 全局:company_id in 当前启用公司
G2 全局:active = True
R1 销售员组:user_id = 当前用户
R2 销售经理组:True
结果:
| 用户组 | 最终 domain |
|---|---|
| 只有销售员 | G1 AND G2 AND R1 |
| 销售经理且经理隐含销售员 | G1 AND G2 AND (R1 OR R2),即 G1 AND G2 |
| 没有匹配任何组规则但 ACL 已允许 | G1 AND G2 |
因此:
- 第一条组规则往往会从“规则层默认全量”收紧范围。
- 再加一条组规则可能扩大范围,因为组规则做 OR。
- 一条
[(1, '=', 1)]的组规则会放宽所有其他组规则,但不能突破全局规则。 - 强制多公司隔离、合规封锁等不能被角色放宽的条件,应设计为全局规则。
5.5 一个容易写反的例子
错误理解:
<!-- 开发者以为 perm_write=False 表示普通用户不能写 -->
<record id="rule_read_own" model="ir.rule">
<field name="domain_force">[('owner_id', '=', user.id)]</field>
<field name="perm_write" eval="False"/>
</record>
实际结果是写操作完全忽略这条规则。如果用户有 write ACL,且没有其他 write 规则,他可能写所有记录。
正确方式之一:
<record id="rule_write_own" model="ir.rule">
<field name="domain_force">[('owner_id', '=', user.id)]</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
同时还要单独设计 read、create、unlink 的规则和 ACL,不能靠规则名称猜测含义。
5.6 搜索、读取与显式 browse 的表现不同
搜索时规则被编入 SQL:
search()/search_fetch()
-> _search()
-> 检查模型 read ACL
-> _where_calc(用户 domain)
-> _apply_ir_rules(query, 'read')
-> SQL 只返回可见记录
源码:odoo/models.py:5525-5538,5735-5763。
因此 search([]) 对不可见记录通常是静默过滤。若代码已经 browse(id) 并显式 read/write/unlink,访问检查发现 recordset 中有禁止记录时会抛 AccessError。批量 recordset 中只要混入一条禁止记录,整个操作通常失败,而不是只跳过那一条。
这一区别是排障关键:
# 可能返回空,不抛错
record = self.env["demo.document"].search([("id", "=", target_id)])
# browse 本身不查数据库;后续 read 会检查并可能抛错
record = self.env["demo.document"].browse(target_id)
record.read(["name"])
不要把 search() 返回空一概解释为“记录不存在”,它也可能是被规则过滤。
5.7 create 是后验规则,write 是前态规则
Odoo 18 的关键调用顺序:
create(vals)
1. 检查模型 create ACL
2. 准备默认值、预计算值和委托父记录
3. 内部 _create() 写入记录、关系字段并执行存储字段约束
4. 对该检查点的新记录执行 create ACL + 记录规则
5. 返回外层继续 inverse、非存储 inverse 约束和 _check_company
write(vals)
1. 对现有 recordset 检查 write ACL + write 记录规则
2. 检查字段访问
3. 写入新值
4. 不再用新状态重复检查 write 记录规则
源码:
- create:
odoo/models.py:4895-4928,5207-5240。 - write:
odoo/models.py:4618-4666。
这意味着规则 [('owner_id', '=', user.id)] 可以保证用户只能开始修改自己负责的记录,却不能自动阻止他把 owner_id 改给别人。修改后记录可能直接从他的视野中消失。
负责人不可转移、审批后关键字段不可改、不能自批等业务不变量,必须在 create()、write() 或专用业务方法中显式校验。不要误以为记录规则会验证 write 后的新状态。
create 的规则在插入后、外层 inverse 和 _check_company() 之前检查。它验证的是 _create() 检查点已经落库的新记录状态,不保证后续 inverse 或自定义 super().create() 返回后的代码不会再改变规则依赖字段。正常异常会使整个 RPC 事务回滚;自定义代码不要捕获 AccessError 后继续提交,否则可能破坏这个事务假设。
5.8 _inherits 的规则传播
若模型使用委托继承,父模型规则会通过父字段转换成 any domain 加入子模型的全局条件,源码见 ir_rule.py:143-147。这使父对象的记录范围约束可以传播,但也可能让子模型查询生成更复杂的 SQL。
不要把它扩展成“父 ACL 自动并入子 ACL”。父记录的 ACL/规则是在 ORM 真正调用 parent.create() 或 parent.write() 时分别触发,委托父记录创建链见 odoo/models.py:4961-4977;子模型自身仍需要明确 ACL。
5.9 多公司规则的标准写法
典型严格隔离:
<record id="rule_document_company" model="ir.rule">
<field name="name">Document multi-company</field>
<field name="model_id" ref="model_demo_document"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
不设置 groups,因此它是全局规则。company_ids 是用户本次请求已启用的公司,不是 user.company_ids 全集。
有些模型允许共享记录,会采用:
['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
company_id=False 到底代表集团共享、系统模板还是脏数据,必须逐模型定义,不能全局套用一种解释。
5.10 原生业务模块值得读的例子
销售模块 addons/sale/security/ir_rules.xml:5-83 同时展示:
- 订单、订单行、分析模型分别配置公司规则,说明只保护主表不够。
- 销售员“本人订单”规则。
- 看全部订单组的
[(1, '=', 1)]放宽规则。
费用模块 addons/hr_expense/security/ir_rule.xml:12-75 展示:
- 本人、部门经理、直属上级、费用审批人等多条组织关系路径。
- 草稿与非草稿在不同操作上的范围。
- 公司规则作为全局约束。
阅读业务模块时不要只抄 domain,要同时核对组继承、ACL、模型业务方法和关联/报表模型。
5.11 规则缓存与扩展风险
规则计算有缓存,默认键包含:
uid + su + model_name + mode + allowed_company_ids
源码:ir_rule.py:74-76,134-178。规则 create/write/unlink 会清 registry cache,见 ir_rule.py:180-201。
如果增强 _eval_context(),把 user.department_id、岗位、授权有效期或自定义 context 值求成 domain 常量,就必须同步解决缓存:
- 新增 context 变量时覆盖
_compute_domain_keys()。 - 用户部门、岗位或授权变化时主动清除相关缓存。
- 不要假设 domain 中使用
time就会在午夜自动重新计算;缓存可能继续复用旧结果。 - 对有效期应在每次能力/范围决策时校验,cron 只做清理或投影同步。
复杂关系路径、child_of、大量 OR 和超长 ID 列表可能产生昂贵 SQL。规则设计必须配合索引、查询计划和真实数据量测试。
老赵解读
记录规则最容易出错的地方,是开发者凭直觉理解 AND/OR,而没有把最终 domain 展开。每次新增规则都应写出“全局规则交集 ∩ 适用组规则并集”的结果,并用至少两个角色、两家公司、正反两类记录验证。能运行不等于正确,权限规则最怕的是安静地多放了一批数据。
目前没有任何评论。