很多人学 Odoo 权限时,脑中会形成一个非常扁平的图:
- ACL 决定能不能访问模型;
- 记录规则决定能不能访问具体记录;
- 两者叠一下就结束。
这个说法作为入门口诀没问题,但一到实战就不够用了。
因为 /home/ubuntu/odoo-temp/odoo/addons/base/models/ir_rule.py 和 odoo/orm/models.py 里的真实逻辑,比“两个开关串联”复杂得多。
Odoo 真正在做的是:
- 先检查模型级 ACL;
- 再为当前用户、当前操作、当前上下文计算出一条综合
domain; - 这条 domain 里同时可能包含:
-
_inherits父模型规则; - 全局规则; - 按组生效的规则; - 最后对真实记录集做过滤;
- 如果过滤后还有禁止访问的记录,再抛出访问错误。
所以 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 来用,而是:
- 先处理
_inherits父模型规则; - 再查当前模型适用于该操作的所有 rule;
- 然后把规则拆成两类: - 全局规则 - 分组规则
- 最后按固定逻辑拼成一条
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,缓存键里包括:
uidsumodel_namemode_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、父模型规则、全局/分组合并、上下文、多公司,以及当前操作模式。
九、实战建议
调试权限问题,我更建议按下面顺序来:
- 先看 ACL 是否过了模型入口;
- 再确认当前用户到底命中了哪些 rule;
- 区分全局 rule 和分组 rule;
- 确认当前操作是 read / write / unlink 哪一种;
- 检查 allowed_company_ids 和当前公司上下文;
- 最后再判断是不是需要
sudo(),还是应该修正规则本身。
这样你会比“看到 AccessError 就开始乱改 domain_force”稳得多。
总结
Odoo 权限的真实链路不是“ACL + rule 两层门”这么简单,而是:
- ACL 先决定模型入口;
_compute_domain()合成父模型规则、全局规则和分组规则;_check_access()再把这条综合 domain 落到真实记录集上;- 不允许的记录最终通过
_make_access_error()报出来。
如果只记一句,就记这句:
record rule 不是单独的门锁,它是 Odoo 整条访问判定流水线里,用来裁剪真实记录集的那一层。
DISCUSSION
评论区