很多实施同学第一次维护用户权限时,都会遇到一种“像闹鬼一样”的现象:
- 我只给用户勾了 A 组;
- 结果用户界面里自己又带出了 B、C、D;
- 我把某个组去掉,刷新后又像是还在;
- Portal、Internal User、Public 还会互相打架。
如果只把组理解成普通 Many2many,这些现象确实很难解释。
但看 /home/ubuntu/odoo-temp/odoo/addons/base/models/res_groups.py 和 res_users.py 就会发现,Odoo 对组的建模从来不是“平铺勾选列表”,而是:
一张带传递关系的权限图。
一、implied_ids 不是“顺带推荐”,而是真正的继承边
在 res.groups 上,implied_ids 的语义不是“顺便关联几个组看看”,而是:
- 当前组蕴含哪些下游组;
- 你给了上层组,就等于也给了这些被蕴含的组能力。
所以在权限语义上,implied_ids 更接近:
- 角色继承
- 权限包展开
- 高级组自动包含基础组
例如一个“经理”组,往往会 implied 一个“用户”组; 这样做不是为了 UI 好看,而是因为经理天然也该拥有普通用户那部分基础能力。
二、真正起作用的是 all_implied_ids,不是只看直接边
源码里 _compute_all_implied_ids() 的注释写得很直白:
- Compute the reflexive transitive closure of implied_ids
翻成大白话就是:
- 不只算你直接 implied 了谁;
- 还要一直沿着 implied 关系往下展开;
- 并且把自己也算进去。
这就是 all_implied_ids。
所以如果:
- A implied B
- B implied C
那么给用户 A,不是只拿到 A + B,而是会拿到:
- A
- B
- C
这也解释了为什么权限经常“自己长出来”:
你看到的不是脏数据,而是闭包展开后的真实权限集合。
三、用户真正持有的是 all_group_ids
在 res_users.py 里,用户并不只看手工勾选的 group_ids。
源码明确有:
@api.depends('group_ids.all_implied_ids')user.all_group_ids = user.group_ids.all_implied_ids
也就是说,系统真正拿来判断用户属于哪些组时,核心不是裸 group_ids,而是:
- 你直接勾选的组
- 再加上这些组通过 implied 链条展开出的全部组
所以很多权限判断、菜单显示、记录规则匹配时,你感觉“我没给这个组,为什么它也命中了”,答案往往就在这里。
不是误命中,而是:
- 你直接没有给;
- 但它被上游组隐式继承了;
- 对 Odoo 来说,这就是真给了。
四、为什么“去掉某个组”有时不会像你预期那样生效
res_groups.py 里还有两个很关键的方法:
_apply_group()_remove_group()
它们都不是只改当前一条边,而是会基于 all_implied_ids 去判断真实影响范围。
这背后反映的设计边界很重要:
在一张传递图里,删除一个可见权限,不等于删除它的所有来源。
举个最常见的坑:
- 用户有 A 组
- A implied B
- 你试图把 B 从用户“拿掉”
如果 A 还在,那么 B 仍然会从 A 的 implied 链里回来。
这时候你以为系统“删不干净”,其实系统是在保持权限图自洽:
- 只要上游角色还成立,下游能力就不能被局部打断。
所以真正的修法通常不是“从用户身上硬删 B”,而是:
- 改组设计;
- 或者改上游 implied 关系;
- 或者让用户不要再属于 A。
五、为什么 Portal / Public / Internal User 不能随便共存
res_users.py 里的 _check_disjoint_groups() 很值得每个实施和开发都看一眼。
它会取出用户类型组:
base.group_userbase.group_portalbase.group_public
然后检查用户的 all_group_ids 与这些组的交集。
如果交集数量大于 1,就直接抛错:
- 用户不能同时属于互斥的用户类型组
这说明 Odoo 不把这三类组当成普通业务权限,而把它们当作:
身份层级 / 会话语义 / 安全边界的根角色。
原因也很直观:
- Internal User 代表后台内部用户;
- Portal 代表受限外部门户用户;
- Public 代表未登录或极弱身份的访问者。
如果同一个用户同时落在多个用户类型里,很多菜单、模型访问、控制器行为都会逻辑冲突。
六、最容易被忽略的坑:互斥关系也会通过 implied 链传播
很多人以为只有直接给用户勾 Portal 和 Internal User 才会报冲突。
其实源码看的不是裸 group_ids,而是 all_group_ids。
所以即便你没有显式勾:
- 只要某个业务组 implied 到了 portal;
- 另一个业务组又 implied 到了 internal user;
- 用户同时拿到这两条链;
最终一样会触发互斥校验。
这类问题最难排查,因为表面上你没直接勾冲突组。
但从 Odoo 角度看, 只要闭包展开后同时命中多个用户类型组,就已经是不合法状态。
七、实施和开发应该怎么设计组层级
比较稳的做法通常是:
1)把组分成“身份组”和“业务能力组”
- 身份组:Internal / Portal / Public
- 业务能力组:销售用户、采购经理、库存主管等
身份组尽量保持根级、清晰、互斥; 业务能力组在对应身份组下展开。
2)高阶组 implied 基础组,但别反过来
比如:
- 销售经理 implied 销售用户
- 不要让销售用户再 implied 销售经理
否则很容易把角色图搞成循环或权限膨胀。
3)排查用户权限时,永远看“展开后结果”
不要只盯界面上手工勾选项。 真正该问的是:
- 这个用户的
all_group_ids到底包含什么? - 这些组分别是从哪条 implied 链传播来的?
八、这套设计解决的其实不是“勾选方便”,而是权限一致性
从源码看得很清楚,Odoo 做这些闭包与互斥约束,并不是为了让管理员少点几下鼠标,而是为了保证:
- 上层角色天然包含下层能力;
- 权限不会因为局部手工删改而自相矛盾;
- 用户类型身份边界始终明确;
- 安全判断、菜单可见性、记录规则命中都能基于稳定的组集合运行。
所以以后再遇到“组怎么自己冒出来了”,更准确的理解不是:
系统在偷偷改数据。
而是:
Odoo 正在把你配置的权限图展开成用户真正持有的角色闭包。
DISCUSSION
评论区