组继承

Odoo 组权限为什么会自己长出来:implied_ids、all_implied_ids 与互斥用户类型组讲透

很多人勾选一个组以后,发现用户权限自己多了一串组。真正原因不是后台在“乱补”,而是 res.groups 通过 implied_ids 计算 all_implied_ids,再让用户 all_group_ids 获得传递闭包;portal、public、employee 这类组还存在互斥约束。

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

很多实施同学第一次维护用户权限时,都会遇到一种“像闹鬼一样”的现象:

  • 我只给用户勾了 A 组;
  • 结果用户界面里自己又带出了 B、C、D;
  • 我把某个组去掉,刷新后又像是还在;
  • Portal、Internal User、Public 还会互相打架。

如果只把组理解成普通 Many2many,这些现象确实很难解释。

但看 /home/ubuntu/odoo-temp/odoo/addons/base/models/res_groups.pyres_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_user
  • base.group_portal
  • base.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

评论区

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