Portal 列表边界

Odoo Portal 列表为什么一加搜索就容易越权或漏数据:domain、pager 与 access token 的边界

从 Odoo 19 的 portal 与 sale portal 控制器源码出发,讲清门户列表页并不是“search 一把然后分页”这么简单。domain、pager、session history 与 access token 彼此有边界,自定义时最容易在这里做出越权或漏单问题。

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

先给一句结论

Odoo Portal 的列表页,从来不是:

  • search 一把
  • render 一个模板
  • 翻页继续 search

它真正做的是:

先定义“谁能看到哪些对象”,再定义“这批对象如何分页、排序和跳详情页”。

如果你把这两层混在一起,自定义 Portal 时很容易:

  • 列表页漏数据
  • 详情页越权
  • 分页跳页后条件丢失
  • access token 和登录态边界搞乱

第一层:Portal 列表的安全边界首先在 domain

addons/sale/controllers/portal.py,销售订单门户列表先准备 domain:

('partner_id', 'child_of', [partner.commercial_partner_id.id])

这句话的意义非常大。

它不是简单“只看当前联系人自己的单”,而是允许同一商业主体下的子联系人一起看。

也就是说,Portal 列表的核心不是模型 search 权限本身,而是:

  • 当前门户用户映射到哪个 partner
  • domain 把业务上允许看到的文档边界画在哪里

如果你自定义时只想“照抄一个 controller”,最容易漏掉的就是这层业务 domain。


第二层:分页不是装饰,它会固化你的查询条件

portal.pager() 做的不只是算页码。

它还会:

  • 根据 total / step 计算 page_count
  • 纠正非法 page 值
  • url_args 拼回每个分页链接

这意味着如果你有:

  • 日期范围
  • sortby
  • filterby
  • search 关键字

却没把它们放进 url_args,那翻到第 2 页时条件就丢了。

于是你会看到一种非常常见的 bug:

  • 第 1 页是筛选后的结果
  • 第 2 页突然变成全量列表

问题不在 pager 算错,而在你没有把查询上下文带过去。


第三层:列表页和详情页的放权机制不是一套

列表页通常走:

  • auth='user'
  • 当前登录用户的 partner domain

详情页则常常还要处理:

  • auth='public'
  • access_token

portal_order_page(),真正的入口不是你手写 search,而是 _document_check_access()

这一步的心智模型是:

  • 登录态能不能看
  • 没登录但带 token 能不能看
  • 没权限就 redirect / raise

所以很多人把列表页做得很严,结果详情页自己写了个 browse(order_id),边界立刻破掉。


第四层:session history 不是安全机制,是导航体验机制

销售 portal 列表会把当前页看到的 ids 放进 session:

  • my_orders_history
  • my_quotations_history

它的用途主要是:

  • 在详情页里做上一条 / 下一条导航

这不是授权依据。

也就是说:

  • session 里有 id,不代表用户就能看
  • 真正访问详情页时,还得再过 document access 检查

这点非常重要。

很多自定义误把“我把 id 存 session 了”当成“这条记录已经授权过”,这是不成立的。


第五层:为什么 token 化详情页常和列表页看起来“不一致”

因为两者本来就不是一个入口模型。

列表页

  • 面向当前登录用户
  • 通过 partner domain 批量约束

token 详情页

  • 面向单文档分享
  • 通过 access token 放开一条具体记录

所以你可能会看到:

  • 某人不在 portal 列表里看到订单
  • 但拿着分享链接能打开这张订单

这不一定是 bug,而可能是产品设计。

关键在于你自己要清楚:

你做的是“用户工作台列表”,还是“单对象受控分享链接”。

这两者不要混成一套授权逻辑。


自定义 Portal 时最常见的 4 个坑

坑 1:只改 search,不改 count

分页总数通常来自 search_count(domain)

如果列表数据用了新 domain,但 count 还沿用旧 domain,页码一定错。

坑 2:只改列表 domain,不改详情 access check

结果列表收紧了,详情页却还能猜 id 打开。

坑 3:忘了在 url_args 里保留筛选条件

翻页、排序、日期过滤经常因此失效。

坑 4:把 session history 当授权缓存

它只是导航辅助,不是权限证明。


一个稳妥的开发顺序

做 Portal 列表自定义时,建议按这个顺序:

  1. 先定义“谁能看哪些记录”的业务 domain
  2. 再定义 sort / filter / date range 如何影响 domain
  3. search_countsearch 保持同一套 domain
  4. 所有筛选参数都进 url_args
  5. 详情页统一走 _document_check_access() 或等价机制

这样做最不容易把安全边界和导航体验缠在一起。


结论

Portal 列表真正难的,不是模板或分页按钮,而是三件事必须始终一致:

  • domain 负责谁能看
  • pager 负责带着条件翻页
  • 详情 access check 负责单对象最终放权

把这三层分清楚,Portal 自定义就稳很多;混在一起,越做越像“偶尔能用、偶尔越权”的玄学系统。

DISCUSSION

评论区

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