跳至内容

第5章 记录规则 Record Rules

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 展开。每次新增规则都应写出“全局规则交集 ∩ 适用组规则并集”的结果,并用至少两个角色、两家公司、正反两类记录验证。能运行不等于正确,权限规则最怕的是安静地多放了一批数据。

额定值
0 0

目前没有任何评论。

成为第一个发表评论的人。