先给一句最实用的话
has_group() 不是“顺手读一眼当前用户身上的 group_ids”。
它真正做的是:
拿一个组的 XMLID,解析到正式组定义,再用包含 implied groups 的结果判断用户是否属于这条权限集合。
所以它比很多人想得更“语义化”,也更容易被误用。
第一层:你传进去的不是数据库 ID,而是 XMLID
has_group() 要求的是类似:
base.group_usersales_team.group_sale_manager
这种 fully-qualified external id。
继续往下看 _has_group(),它不是直接查一个 Many2many,而是先走:
res.groups._get_group_definitions().get_id(group_ext_id)
也就是说,第一步其实是:
把 XMLID 解析成组定义里的正式 group id。
这件事有两个含义:
- 你写错 XMLID,不是“小问题”,判断会直接偏掉
has_group()的判断逻辑天然和模块数据定义绑定,而不只是和当前数据库里那条记录绑定
所以它更适合表达“我在检查一个业务含义明确的权限点”,而不是随手拿来替代任意组逻辑。
第二层:判断的不是“直接组”,而是包含 implied groups 的结果
res.groups 里有:
implied_idsall_implied_ids
而 _get_group_definitions() 会把每个组的:
refsupersetsdisjoints
收集到一个 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
评论区