记录规则

Odoo 权限不是“ACL 再加个 rule”这么简单:记录规则域合并、组 OR 与访问校验边界

很多开发者把 Odoo 权限理解成“两层门”:先看 ACL,再看 record rule。但从 ir_rule.py 的 _compute_domain 和 models.py 的 _check_access 看,系统实际做的是“先判模型权限,再把全局规则、组规则、继承模型规则合成一个 domain,最后对真实记录集做过滤和报错”。

Odoo 开发 框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

很多人学 Odoo 权限时,脑中会形成一个非常扁平的图:

  • ACL 决定能不能访问模型;
  • 记录规则决定能不能访问具体记录;
  • 两者叠一下就结束。

这个说法作为入门口诀没问题,但一到实战就不够用了。

因为 /home/ubuntu/odoo-temp/odoo/addons/base/models/ir_rule.pyodoo/orm/models.py 里的真实逻辑,比“两个开关串联”复杂得多。

Odoo 真正在做的是:

  1. 先检查模型级 ACL;
  2. 再为当前用户、当前操作、当前上下文计算出一条综合 domain
  3. 这条 domain 里同时可能包含: - _inherits 父模型规则; - 全局规则; - 按组生效的规则;
  4. 最后对真实记录集做过滤;
  5. 如果过滤后还有禁止访问的记录,再抛出访问错误。

所以 record rule 不是“domain 写上去就算完”,而是参与到最终访问判定流水线里的中间表达式

一、ACL 先拦的是“模型级入口”

models.py 里的 _check_access() 一开始先调:

  • ir.model.access.check(self._name, operation, raise_exception=False)

如果 ACL 都没过,后面的 record rule 根本没资格上场。

这说明一个很容易被忽略的事实:

record rule 不是 ACL 的替代品,它只在模型级访问已经允许时,继续收紧到记录级。

因此你如果遇到:

  • 明明 rule domain 写得很宽;
  • 但用户还是完全打不开模型;

优先看 ACL,而不是先怀疑 rule。

二、_compute_domain() 不是读一条规则,而是“合成一条最终规则”

ir_rule.py 里的 _compute_domain(model_name, mode) 是整件事的核心。

它不会只拿一条 ir.rule 来用,而是:

  1. 先处理 _inherits 父模型规则;
  2. 再查当前模型适用于该操作的所有 rule;
  3. 然后把规则拆成两类: - 全局规则 - 分组规则
  4. 最后按固定逻辑拼成一条 Domain.AND(...)

这一步很关键,因为很多人对 Odoo rule 的误解,就出在“多个规则到底是 AND 还是 OR”。

三、全局规则和分组规则的合并方式并不一样

源码里逻辑非常明确:

  • 没有 groups 的 rule,进 global_domains
  • 有 groups 的 rule,进 group_domains
  • 最后如果存在任何 group_domains,会把它们先做 Domain.OR(group_domains),再追加到全局域里
  • 最终返回 Domain.AND(global_domains)

翻成人话就是:

1)全局规则彼此是 AND 关系

只要你写了多条全局 rule,它们会一起收紧访问范围。

2)同一用户命中的分组规则,先 OR 再参与总 AND

也就是说,分组规则更像:

  • “你属于这些组里的任意一个规则口子,都可以放行到这一步”;
  • 但放行结果仍然要再满足所有全局规则。

这正是很多权限事故的根源。

有人以为“我再加一条分组 rule 放开一点就好了”,结果发现还是进不去,通常就是因为:

全局规则仍在外层继续收紧。

四、rule 还会受到上下文和缓存键影响

_compute_domain() 上挂了 ormcache,缓存键里包括:

  • uid
  • su
  • model_name
  • mode
  • _compute_domain_context_values() 产出的上下文值

_compute_domain_context_values() 又会把关键上下文字段读出来,例如允许公司列表这类会影响权限边界的数据。

这意味着两件事:

1)同一个用户,不同上下文,domain 可能不是同一条

尤其是多公司场景,你切换 allowed_company_ids,最终 rule domain 可能就不同。

2)调试 record rule,不能只看数据库规则文本

你还得看:

  • 当前是不是 sudo
  • 当前上下文里有哪些公司或其他影响权限计算的键;
  • 命中的是 read 还是 write;
  • 缓存是否因为上下文不同而重算。

五、真正对记录做裁剪的地方,在 _check_access()

很多人以为 _compute_domain() 已经完成权限判断,其实还差一步。

models.py 里的 _check_access() 在拿到 domain 之后,会做:

  • self.sudo().with_context(active_test=False).filtered_domain(domain)

然后用:

  • self - allowed_records

算出 forbidden 记录。

这一步非常值得注意,因为它说明:

record rule 的最终效果,是把当前记录集做一次过滤,而不是只在 SQL 查询阶段“顺手少查一点”。

这也是为什么某些记录你“能搜到一点痕迹”或“调试时感觉像有了 recordset”,但真正读写还是报错——因为最后还有真实记录集过滤这道关。

六、_filtered_access()check_access() 只是同一底层结果的不同包装

  • check_access():不允许就抛错
  • has_access():返回布尔值
  • _filtered_access():返回允许访问的子集

它们底层都基于 _check_access()

这意味着你做定制时,最好理解这些 API 不是三套不同权限系统,而是同一个权限判定核心的三种表现形式。

七、为什么错误信息里经常提到多公司

_make_access_error() 也很有意思。

它不只是吐一个“Access Denied”,还会:

  • 记录日志;
  • 查出失败规则;
  • 取前几个失败记录做展示;
  • 如果 rule domain 里涉及 company_id,还会判断是不是多公司问题,并给出切公司提示。

这说明官方源码也默认承认:

Odoo 里最常见的 record rule 误判之一,就是多公司上下文。

因此现场遇到“同一个用户一会儿能看、一会儿不能看”,先别急着改 rule,先确认当前公司和 allowed companies。

八、新手最容易误解的四件事

1)“加一条分组 rule 就能放开所有限制”

不对。

分组 rule 是先 OR,但外层还要过全局 rule 的 AND。

2)“rule 只影响 search,不影响现成 recordset”

不对。

_check_access() 最后会对真实 recordset 做过滤。

3)“sudo 只是略过一点点校验”

也不对。

缓存键里包含 su,而且很多 rule 逻辑在 sudo 场景本来就会完全不同。

4)“看 rule.domain_force 就知道最终权限”

不对。

最终权限取决于:ACL、父模型规则、全局/分组合并、上下文、多公司,以及当前操作模式。

九、实战建议

调试权限问题,我更建议按下面顺序来:

  1. 先看 ACL 是否过了模型入口
  2. 再确认当前用户到底命中了哪些 rule
  3. 区分全局 rule 和分组 rule
  4. 确认当前操作是 read / write / unlink 哪一种
  5. 检查 allowed_company_ids 和当前公司上下文
  6. 最后再判断是不是需要 sudo(),还是应该修正规则本身。

这样你会比“看到 AccessError 就开始乱改 domain_force”稳得多。

总结

Odoo 权限的真实链路不是“ACL + rule 两层门”这么简单,而是:

  • ACL 先决定模型入口;
  • _compute_domain() 合成父模型规则、全局规则和分组规则;
  • _check_access() 再把这条综合 domain 落到真实记录集上;
  • 不允许的记录最终通过 _make_access_error() 报出来。

如果只记一句,就记这句:

record rule 不是单独的门锁,它是 Odoo 整条访问判定流水线里,用来裁剪真实记录集的那一层。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。