门户授权

Odoo Portal 权限为什么不是“勾上就完了”:grant / revoke、联系人展开与用户边界讲透

很多人把 Portal Access Wizard 理解成一个简单的开关,但 portal_wizard 实际同时处理联系人展开、邮箱唯一性、public/portal/internal 组切换、signup token 与归档边界。

网站
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的 Portal Access Management 不是一个“给联系人加个 portal 组”这么简单的按钮。

addons/portal/wizard/portal_wizard.pyaddons/portal/tests/test_portal_wizard.py 来看,这条链路至少同时处理了五件事:

  1. 选中的对象到底是公司、联系人,还是两者都要展开;
  2. 这个联系人是不是已经绑定现有用户;
  3. 这个用户到底是 public、portal 还是 internal;
  4. 发放访问权时要不要创建用户、准备 signup token、发送邀请;
  5. 撤销访问权时是删用户、改组,还是直接归档。

所以正确理解应该是:

Portal 向导管理的是“外部身份生命周期”,不是单纯的勾组权限。

这也是为什么很多项目里“客户怎么突然拿到入口了”“撤销了为什么账号还在”“为什么一个公司点授权会冒出一串联系人”,答案都在这条向导链路里。

一、为什么它会把一个 partner 展开成多个联系人

_default_partner_ids() 并不会只拿当前选中的 partner 本身。

源码会对每个 res.partner 做一轮展开:

  • child_idstype in ('contact', 'other') 的联系人一起纳入;
  • 再把当前 partner 自己也并进去。

这意味着什么?

如果你在客户公司级别点“Grant Portal Access”,系统心里的对象并不是“这家公司”一个抽象主体,而是:

  • 公司本体;
  • 它下面真正可能要收邀请邮件的联系人集合。

这就是为什么实务里经常会出现“我明明选了一个客户,向导里却出来多行联系人”。

它不是 UI 多余,而是 Odoo 在明确:

门户权限最终落在可登录身份上,而不是纯粹落在公司标签上。

二、为什么邮箱检查比很多人想的严格

portal.wizard.user 会计算一个 email_state,状态只有三种:

  • ok:可用;
  • ko:邮箱格式无效;
  • exist:跟现有用户撞邮箱。

这里关键不是“有邮箱就行”,而是它会先做标准化,再去查相似用户登录。

_assert_user_email_uniqueness() 甚至会直接拦下两类情况:

  • 联系人邮箱本身不合法;
  • 邮箱已经被系统里别的用户占用。

测试文件里也专门覆盖了这条逻辑:

  • 无效邮箱必须报错;
  • 与已有用户重复的邮箱必须报错;
  • email_state 要正确变成 koexist

这说明 portal 向导不是“业务联系人留个邮箱备注”,而是在为真正的登录身份做预检。

三、为什么 public、portal、internal 不是一个连续刻度

_compute_group_details() 会把用户明确分成三类:

  • internal 用户;
  • portal 用户;
  • 既非 internal 也非 portal 的其他状态,例如 public。

action_grant_access() 的逻辑也很明确:

  • 如果已经是 internal 或 portal,就不允许再按普通 portal 流程处理;
  • public 用户可以被提升成 portal;
  • internal 用户不能通过这个向导降格或重配。

测试也印证了这个边界:

  • public 用户授予 portal 后,应移除 base.group_public,加入 base.group_portal
  • internal 用户尝试 grant / revoke 都应该报错。

这背后的产品意图很清楚:

portal 是外部访问层,不是后台员工权限的轻量版。

所以你不能把 internal 用户当作“权限更高的 portal 用户”去管理;两者属于不同身份轨道。

四、grant access 真正在做什么

action_grant_access() 至少做了四步关键动作:

  1. 校验邮箱唯一性;
  2. 如有必要,回写 partner 邮箱;
  3. 如果还没有绑定用户,则按 partner 所属公司创建新用户;
  4. 把用户加入 group_portal、移出 group_public,然后 signup_prepare() 并发邀请邮件。

这里最容易被忽视的,是 signup_prepare()

它说明 portal 授权并不等于“密码已经设置好、可以立刻后台登录”,而是:

  • 系统先准备一条受控的注册 / 设密链路;
  • 然后通过邮件把入口发给外部人。

也就是说,grant access 处理的是“资格 + 邀请”两步组合。

所以如果邮件没发出去、模板被改坏,业务方常会误以为“授权失败”,其实可能只是邀请环节断了。

五、revoke access 为什么不是删用户,而是改组并归档

action_revoke_access() 的动作非常克制:

  • 先把 partner 的 signup_type 清空,避免旧 token 继续可用;
  • 再把用户从 group_portal 移出,并补回 group_public
  • 最后把用户设为 active = False

注意,它不是直接删除 res.users

测试也明确写了:

  • revoke 之后用户记录应该还在;
  • 只是恢复到 public 组并被归档;
  • 不再是 portal,也不是 internal。

这套设计很合理,因为删除用户会破坏历史引用:

  • 邮件线程;
  • 登录记录;
  • 业务关联;
  • partner 对应关系。

所以 Odoo 选择的是:

撤销访问资格,而不是抹掉身份痕迹。

六、为什么“联系人有 portal 权限”不等于“他能看所有公司数据”

Portal 向导本身做的,只是把某个 partner 绑定到一个 portal 用户身份上,并准备登录入口。

它并没有在这里直接定义:

  • 这个用户能看哪些订单;
  • 能看哪些项目;
  • 能看哪些工单;
  • 能不能跨公司、跨联系人查看对象。

那些对象级访问边界,仍然取决于对应模块自己的 portal 规则、token 机制和控制器检查。

所以项目里一个常见误区是:

  • 给了 portal,就以为“整家公司都能看全部客户资料”;
  • 或者撤了 portal,就以为“所有分享链接也同时失效”。

实际上:

  • portal 身份生命周期是一层;
  • 具体业务对象的公开 / 门户 / token 访问是另一层。

两层叠在一起,才是完整外部访问边界。

最后给实施方的 4 个提醒

1. 不要把公司 partner 当成唯一授权对象

很多实际能登录的人,都是它下面的联系人。

2. 不要忽视邮箱唯一性

门户邀请失败,很多时候不是 SMTP,而是邮箱已经被系统别的用户占了。

3. 不要试图用 portal 向导管理 internal 用户

这条向导是外部身份通道,不是后台账号治理工具。

4. revoke 后要连同对象分享链路一起审视

撤掉 portal 资格,不代表所有历史分享入口自动等于零风险。

参考源码

  • addons/portal/wizard/portal_wizard.py
  • addons/portal/tests/test_portal_wizard.py

DISCUSSION

评论区

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