has_group 陷阱

Odoo 的 `has_group()` 为什么不只是“查一下有没有组”:XMLID 解析、缓存与权限分支陷阱

很多开发者把 `has_group()` 当成一个轻量 if 判断,但从 Odoo 19 的 `res_users.py` 和 `res_groups.py` 看,它背后其实连着 XMLID 解析、组定义缓存、隐含组展开,以及对“是否允许检查别人权限”的访问边界。本文讲清 `has_group()` 真正改变的是什么,以及为什么它常常把 bug 藏得更深。

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

先给一句最实用的话

has_group() 不是“顺手读一眼当前用户身上的 group_ids”。

它真正做的是:

拿一个组的 XMLID,解析到正式组定义,再用包含 implied groups 的结果判断用户是否属于这条权限集合。

所以它比很多人想得更“语义化”,也更容易被误用。


第一层:你传进去的不是数据库 ID,而是 XMLID

has_group() 要求的是类似:

  • base.group_user
  • sales_team.group_sale_manager

这种 fully-qualified external id。

继续往下看 _has_group(),它不是直接查一个 Many2many,而是先走:

  • res.groups._get_group_definitions().get_id(group_ext_id)

也就是说,第一步其实是:

把 XMLID 解析成组定义里的正式 group id。

这件事有两个含义:

  1. 你写错 XMLID,不是“小问题”,判断会直接偏掉
  2. has_group() 的判断逻辑天然和模块数据定义绑定,而不只是和当前数据库里那条记录绑定

所以它更适合表达“我在检查一个业务含义明确的权限点”,而不是随手拿来替代任意组逻辑。


第二层:判断的不是“直接组”,而是包含 implied groups 的结果

res.groups 里有:

  • implied_ids
  • all_implied_ids

_get_group_definitions() 会把每个组的:

  • ref
  • supersets
  • disjoints

收集到一个 SetDefinitions 结构里。

再看 _has_group(),最终比的是:

  • group_id in self._get_group_ids()

_get_group_ids() 返回的是 all_group_ids

这代表:

has_group() 判断的是“用户最终拥有的权限集合”,不只是手工勾上的那一个组。

这正是很多权限现象“看起来像自动长出来”的根本原因。

比如你没直接给用户 A 组,但给了一个会 imply A 的管理组,has_group('A') 一样会返回 True。


第三层:这里有缓存,不是每次都从零重算

res.groups._get_group_definitions() 用了:

  • @tools.ormcache(cache='groups')

res.users._get_group_ids() 也用了:

  • @tools.ormcache('self.id')

说明 Odoo 默认假设:

  • 组定义解析是高频操作
  • 用户最终组集合也是高频读取

所以 has_group() 并不是一个“毫无代价的普通 Python if”,但框架已经尽量把它做成可复用缓存结果。

这也是为什么它适合做少量关键分支判断,却不适合被你在大循环里当成“完全免费的细粒度权限引擎”到处乱塞。


第四层:has_group() 不是想查谁就查谁

has_group() 里有一段很重要的保护:

如果不是:

  • 当前是 su
  • 或者 self == env.user
  • 或者当前用户本身属于 base.group_user

就会抛 AccessError

源码旁边注释写得很明白:

防止非内部用户通过 RPC 去探测其他用户的组信息。

这提醒我们一件事:

has_group() 不是纯本地函数,它本身就带访问边界。

所以如果你的 portal/public 逻辑里直接去探别人的组,很可能不是“False”,而是直接不被允许。


base.group_no_one 为什么是特殊组

has_group()base.group_no_one 还有一个额外判断:

  • 用户属于这个组
  • 并且当前 request 处于 debug 模式

两者都成立才算 True。

这说明 Odoo 里有些组并不是简单权限标签,而是带运行环境语义的开关

所以你如果把 has_group() 理解成“数据库权限位检查”,就会漏掉这类特殊规则。


开发里最常见的 4 个坑

1. 把 has_group() 当成 ACL / record rule 的替代品

这是最危险的误用。

has_group() 只是你代码里的条件分支。

它不会自动替你处理:

  • 模型 ACL
  • 记录规则
  • 字段级可见性

如果一个功能“只有加上 has_group 分支才安全”,那通常表示你的正式权限设计还没闭环。

2. 在循环里重复调用同一个 has_group()

虽然有缓存,但语义上仍然很丑。

如果你在 5000 条记录循环里反复检查同一个固定组,应该先把结果取出来,再复用。

3. 忘了 implied groups

很多“我没给这个组,怎么判断成 True”并不是系统有鬼,而是 implied chain 在生效。

4. 用 sudo() 把权限判断做平了

如果你先 sudo(),再用 has_group() 写业务分支,结果经常不是你以为的“当前真实用户权限”。

尤其当你混用:

  • sudo()
  • with_user()
  • has_group()

没理清执行身份时,权限 bug 会非常难查。


一个更稳的使用姿势

我更建议把 has_group() 用在这类场景:

  • 是否展示某个管理员按钮
  • 是否启用某条高层业务分支
  • 是否放开某个明确的增强能力

而不是用它去细碎地模拟完整权限系统。

也就是说:

  • 权限体系 交给 ACL / ir.rule
  • 业务分支 再用 has_group() 补充

这样边界会清楚很多。


最后一句话

has_group() 真正检查的,不是“这个用户手动勾了没勾某个组”,而是:

基于 XMLID、组定义缓存、implied groups 与当前执行身份,用户是否属于这条权限集合。

把它看成“语义化权限探针”是对的;把它看成“万能权限系统”就很容易出事。

DISCUSSION

评论区

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