先说结论
很多人第一次调 Odoo 权限时,都会把问题简化成一句话:
“是不是 domain 写错了?”
其实不是。
Odoo 的权限至少有两层锁:
- ACL / model access:你能不能对这个模型做读写删改
- record rule / ir.rule:你能不能看到或操作这条具体记录
所以一条记录能不能被访问,往往不是一个 domain 决定,而是多层规则叠出来的结果。
第一层:check_access_rights() 和 check_access_rule()
在 odoo/orm/models.py 里,这两个旧接口现在都被标成 deprecated,但它们仍然把思路讲得很清楚:
def check_access_rights(self, operation, raise_exception=True):
return self.browse().check_access(operation)
def check_access_rule(self, operation):
self.check_access(operation)
你可以把它理解成:
check_access_rights先看“模型级权限”check_access_rule再看“记录级权限”
这就是为什么你在 debug 时,不能只盯着一条 ir.rule 域。
第二层:ir.rule._compute_domain() 是怎么合并规则的
记录规则并不是“有多少条 rule,就把 domain 一条条硬拼起来”。
ir.rule._compute_domain() 做了更细的合并:
- 先处理父模型继承过来的规则
- 再取当前模型的规则
- 全局规则 走 AND
- 分组规则 先 OR,再并回整体 AND
这个结构非常关键,因为它决定了:
- 你不是只要满足一个 rule 就行
- 也不是每条 rule 都完全独立
- 而是 global 和 group 规则以不同方式叠加
代码里还会对 domain_force 做 safe_eval,说明规则域本身也是运行时求值的。
为什么 sudo() 经常让你“看起来像权限好了”
sudo() 不是“改个函数名而已”。
它会改变当前环境的访问边界,让你绕过用户上下文看到的数据更宽。
这就是为什么很多权限 bug 会出现一种假象:
- 在
sudo()下正常 - 不
sudo()就报错
这时别急着改 domain,先想:
到底是 ACL 卡住了,还是 record rule 卡住了?
read_group 里那个 many2many 特殊分支,为什么值得单独记
在 addons/web/models/models.py 里,有一个非常值得注意的注释:
# Special case for many2many because (<many2many>, '=', False) domain bypass ir.rule.
为了避免这个洞,Odoo 对 many2many 分组值做了特殊处理:
- 空值时用
not any - 有值时用
=搭配具体 id
这意味着:
某些看起来“语法上完全合法”的 domain,在权限层面却可能绕开 record rule。
所以 read_group 这段代码不是普通的 UI 辅助,而是在补一个权限漏洞边界。
最容易误判的几个点
1)把 ACL 和 record rule 混为一谈
ACL 解决“你有没有这个动作的资格”,record rule 解决“你能不能碰这条数据”。
2)把 domain 问题当成唯一原因
有时问题不在 domain 本身,而在 company、active_test、用户组、父模型继承规则。
3)忽略缓存
ir.rule 的 domain 计算有缓存,改完规则后不重启或不清缓存,排查时会很迷惑。
实战排查顺序
如果你在做权限排障,建议按这个顺序查:
- 先看模型 ACL 是否允许动作
- 再看当前用户是否命中 record rule
- 然后检查公司上下文和用户组
- 最后才去看 domain 语法和 UI 分组字段
对 read_group 特别要注意 many2many 分支,不要以为所有 domain 行为都一样。
一句话总结
Odoo 的权限不是“一个 domain 过滤器”,而是 ACL、record rule、上下文和特殊字段分支共同组成的访问系统。
只看 ir.rule 不够,只有把模型权限、记录规则和 read_group 的 many2many 特例一起看,才能真正解释权限行为。
DISCUSSION
评论区