计算权限

Odoo 里的 compute_sudo 不是“性能开关”:它其实在改计算权限边界

很多人把 compute_sudo 当成一个不起眼的小参数,但它真正决定的是计算字段在谁的权限边界下执行。默认值、存储字段、related 字段和数据泄露风险,这篇一次讲透。

Odoo 开发
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 10 阅读

先说结论

compute_sudo 不是一个“顺手加一下更稳”的参数。

它真正决定的是:

这个计算字段在重算时,到底按当前用户权限执行,还是按超级用户权限执行。

这件事一旦理解错,就会出现两类常见问题:

  1. 你以为字段算不出来是 bug,其实是权限不够
  2. 你以为字段只是展示数据,结果把不该让用户看到的信息也算出来并展示了

所以 compute_sudo 的本质,不是性能,也不是语法细节,而是权限边界


源码里已经把默认规则写得很直白

odoo/orm/fields.py 里,Odoo 对这个参数的说明很清楚:

  • compute_sudo=True/False 决定计算字段是否以 superuser 身份重算
  • 默认规则是:
  • 存储计算字段:默认 True
  • 非存储计算字段:默认 False

同一个文件里还有初始化逻辑:

  • 只要字段是 compute,默认 attrs['compute_sudo'] = store
  • 如果字段是 related,默认会走 related_sudo,而它默认又通常是 True

这意味着很多开发者根本没显式写 compute_sudo=True,但系统其实已经默默这么做了。

所以第一层认知要先纠正:

不是“只有你手写了 compute_sudo=True 才会提权”,而是很多 stored compute / related 字段本来就默认在 sudo 语境里工作。


为什么 stored compute 默认倾向 sudo

因为 Odoo 认为“存储型计算字段”更像一种系统维护的派生结果,而不是一次临时界面展示。

比如:

  • 汇总数量
  • 统计计数
  • 关联状态
  • 业务聚合值

这类字段如果完全依赖当前登录用户权限,系统就会碰到一个很尴尬的问题:

  • A 用户改了数据,算出一个结果
  • B 用户权限更少,再触发重算,结果又变了
  • 同一条记录的存储值可能随着触发者不同而不一致

这显然不适合作为数据库里的“稳定派生值”。

所以 Odoo 默认让 stored compute 更偏向:

由系统用更完整的视角算出一个确定值,再存下来。

这就是默认 store=True -> compute_sudo=True 背后的设计逻辑。


非存储字段为什么默认不 sudo

因为非存储字段更接近“当前用户眼前这次读取时临时算一下”。

这种字段更像是:

  • 当前页面展示
  • 当前用户上下文下的即时信息
  • 不必写入数据库的动态结果

如果这里默认 sudo,就很容易把“系统知道的全部信息”直接展示给权限较低的用户。

所以 Odoo 选择的默认策略是:

  • 临时展示型字段,更尊重当前用户视角
  • 系统派生型字段,更重视计算稳定性

这个分层其实很合理。


一个最危险的误区:把 compute_sudo 当成“算不出来就开 sudo”

很多人开发时会遇到这种情况:

  • 计算方法里读到了别的模型
  • 普通用户打开表单报空、报错、统计不对
  • 然后第一反应就是:compute_sudo=True

这确实可能“让字段正常显示”。

但你要先问一句:

它正常显示的是“业务上应该给这个用户看的值”,还是“系统内部其实不该暴露给他的值”?

比如:

  • 某字段统计了其他部门的记录数
  • 某字段拼接了用户本不该读取的对象名称
  • 某字段暴露了多公司下别家公司的关联结果

一旦字段本身又没有额外做输出过滤,前端最终就会把 sudo 计算结果直接展示出来。

这就不是“修复显示”,而是制造越权泄露


你可以把它理解成“厨房权限”和“上菜权限”是两回事

一个很好记的类比是:

  • compute_sudo 决定的是:厨房里谁可以进仓库拿食材
  • 页面展示决定的是:最后哪道菜被端到谁桌上

问题在于,很多计算字段把这两件事混了。

如果厨房里用了 sudo,把全部食材都拿到了; 但上菜时你又没有二次过滤; 那用户最终吃到的,就是他本不该看到的数据。

所以正确姿势不是盲目提权,而是分两步想:

  1. 计算阶段需不需要更高权限才能得到稳定结果?
  2. 展示阶段要不要按当前用户再做裁剪?

源码里还有一个很容易被忽略的信号

odoo/orm/models.py 里,Odoo 在把某些 related 字段转换到 SQL 表达式时,会检查:

  • 当前环境是否已经 sudo
  • 字段是否 compute_sudo
  • 字段是否 inherited

否则会直接报:

  • 不能把这个 related 字段转换到 SQL,因为它不是 sudoed related / inherited field

这背后说明一个很重要的事实:

compute_sudo 不只是 UI 层一个显示参数,它会影响 ORM 如何认为这个字段是“可安全展开”的。

也就是说,它是 ORM 访问语义的一部分,不是装饰品。


官方测试也在提醒你:默认值不是拍脑袋

odoo/addons/test_orm/tests/test_fields.py 里,Odoo 专门测了:

  • 普通非存储计算字段默认 compute_sudo=False
  • 存储计算字段默认 compute_sudo=True
  • 某些可编辑的 stored compute 字段虽然可复制、可编辑,但仍然默认 compute_sudo=True

这等于告诉开发者:

默认值本来就是框架明确设计过的行为,不是偶然。

所以你修改它时,要知道自己是在改框架默认的安全与一致性假设。


什么场景更适合保留 compute_sudo=True

通常是这些情况:

1. 你要维护一个稳定的系统级派生值

比如:

  • 数量汇总
  • 计数器
  • 状态聚合
  • 后台统计字段

2. 计算依赖的底层对象并不要求和最终阅读者权限一致

例如:

  • 系统内部流程要先聚合,再只展示一个安全结果

3. 不同触发者不应该算出不同持久值

这是 stored compute 最常见的诉求。


什么场景更应该谨慎,甚至显式写 compute_sudo=False

1. 字段值天然就是“用户视角相关”的

比如:

  • “我是否可见”
  • “我是否可操作”
  • “当前用户能读到几条”
  • “根据当前用户身份展示不同提示”

2. 字段里会拼接敏感对象信息

比如名称、金额、联系人、内部记录摘要。

3. 字段结果会被直接展示给低权限用户

尤其是在 portal、website、公开页、跨部门共享界面里。

这种时候,默认 sudo 往往不是你真正想要的行为。


一个非常实用的判断问题

每次写计算字段前,先问自己:

这个字段是在表达“系统事实”,还是“当前用户看到的事实”?

  • 如果是系统事实,更可能接受 compute_sudo=True
  • 如果是用户看到的事实,就要非常警惕 sudo

这句话基本能帮你避开一半以上的坑。


实战建议:别把安全和易用性交给默认碰运气

建议 1:stored compute 主动审视,不要因为默认值就忽略风险

很多 stored field 没写 compute_sudo,并不代表没有 sudo 风险。

建议 2:字段展示前,确认结果是否已经脱敏

如果是 sudo 算出来的,最好确认:

  • 是否只是布尔结果
  • 是否只是安全计数
  • 是否只是抽象状态

建议 3:和 depends_context('uid') 放一起时尤其小心

用户维度依赖 + sudo 计算,语义很容易打架。

建议 4:多公司场景再多看一眼

sudo 之后,跨公司对象更容易被带进计算路径。


一句话总结

compute_sudo 讲的不是“算得快不快”,而是:

这个计算字段,到底是在当前用户的权限世界里求值,还是在系统超级用户的权限世界里求值。

一旦你把它当成权限边界问题来看,很多 stored compute、related 字段、多公司统计和前台泄露问题,都会一下子看清楚。

DISCUSSION

评论区

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