x2many 权限边界

Odoo 的 x2many 命令为什么不是 sudo 了就能随便改:_allow_sudo_commands 与真实执行用户边界

很多人以为给 recordset 套上 sudo() 或 with_user(admin) 之后,one2many / many2many 的 Command 就一定能通过。Odoo 19 实际专门给 x2many 命令加了一层防线:当目标模型声明 _allow_sudo_commands=False 时,框架会主动撤销这类提权写法,重新按事务原始用户做权限检查。

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

先说结论

很多开发者对 sudo() 有一个很自然、但很危险的直觉:

只要我把当前 recordset 提权了,后面通过 Command.create()Command.update()Command.link() 去改 x2many 关系,框架就会按提权用户放行。

Odoo 19 明确不愿意让你这么想。

odoo/orm/fields_relational.py 里,x2many 写入链路会先跑 _check_sudo_commands()

def _check_sudo_commands(self, comodel):
    if not comodel._allow_sudo_commands:
        return comodel.sudo(False).with_user(comodel.env.transaction.default_env.uid)
    return comodel

这段代码的含义非常直接:

  • 如果目标模型允许 sudo 命令,保留当前环境;
  • 如果目标模型 不允许 sudo 命令,框架会:
  • 关闭 sudo()
  • 把执行用户重置为事务起点的原始用户;
  • 再继续执行那些 Command

所以真正的结论不是“x2many 命令继承当前 env 权限”,而是:

x2many 命令是否能借提权穿透,取决于目标模型是否允许。


一、为什么 Odoo 要专门盯住 x2many Command

普通 write() 看起来已经有权限控制了,为什么还要单独防 Command

因为 x2many Command 太强了。

它不是“改一个外键”这么简单,而是可以顺手触发:

  • 新建目标记录;
  • 修改目标记录;
  • 删除目标记录;
  • 断开关系;
  • 重建整组关系。

也就是说,表面上你只是在写一条父记录,实际上你可能在对子模型做一整套高权限操作。

如果框架默认允许“只要父对象这边 sudo 了,子对象命令就一起跟着飞”,那很多敏感模型都会被绕过去。

这就是为什么这条防线不是写在业务模块里,而是下沉到了 ORM 的 relational field 层。


二、_allow_sudo_commands 真正控制的是“命令落地时谁说了算”

odoo/orm/models.py 里,模型默认有:

_allow_sudo_commands: bool = True

默认值是 True,说明 Odoo 并不是全面封杀这种能力。

但对于某些模型,官方认为风险太高,就会显式设成 False

测试里最典型的是:

  • res.users
  • test_orm.group

为什么这类模型敏感?因为它们代表:

  • 用户账号本身;
  • 用户与组的关系;
  • 权限提升入口;
  • 身份绑定关系。

这些对象一旦允许你通过“父模型 x2many 命令 + 临时 sudo”去改,就很容易出现越权链路。

所以 _allow_sudo_commands 不是在说“这个模型能不能被 sudo”; 它真正表达的是:

这个模型能不能通过别人的 x2many Command,在提权环境下被顺带操作。

这两个问题非常不一样。


三、官方测试其实已经把风险场景写明了

test_sudo_commands() 这组测试很值得读,因为它不是抽象谈安全,而是把真实攻击面写成了用例。

例如对 res.partner.user_ids 这条 one2many,测试覆盖了:

  • Command.create():试图新建用户;
  • Command.update():试图给自己加系统组;
  • Command.delete():试图删用户;
  • Command.unlink():试图拆掉绑定;
  • Command.link():试图把管理员账号绑到自己的 partner 上;
  • Command.clear() / Command.set():试图整体重写关系。

而且测试故意用了两种很多业务代码都会写出来的环境:

  • with_user(admin_user)
  • sudo()

也就是说,官方明确承认现实里经常有人这样写:

  • “临时借管理员身份做一步动作”;
  • “为了省事直接 sudo 一下”。

问题在于,这样一来,命令里真正被改的子记录 往往已经超出了开发者当下脑内关注的那张表。

官方在测试注释里甚至点名了真实业务风格:

  • Documents.with_user(share.create_uid)
  • sign.request.with_user(...).sudo()

意思很明显:

这不是理论问题,而是官方见过的真实代码形态。


四、关键细节:框架不是拒绝整次写入,而是先把 comodel 拉回原用户

这点非常值得注意。

_check_sudo_commands() 不是简单抛错;它做的是:

  1. 拿到 x2many 目标模型 comodel
  2. 如果该模型不允许 sudo 命令;
  3. 就对这个 comodelsudo(False)
  4. with_user(transaction.default_env.uid)
  5. 后续 create / update / unlink 都在这个“还原后的环境”里执行。

这意味着框架的思路不是:

  • “一看到 sudo 命令就报错”;

而是:

  • “你可以继续走这条写入链,但真正涉及敏感子模型时,得按事务原始用户重新验权。”

这种做法很聪明,因为它保留了普通场景的便利性,也把敏感命令的落地点重新拉回真实权限边界。


五、为什么 with_user(admin) 也被一起防了

很多人会误以为只有 sudo() 才危险,with_user(admin) 更“正规”。

但从安全边界看,这两者在这里都可能产生同一个问题:

  • 当前代码块拿着一个更高权限的 env;
  • 然后通过 x2many Command 去改另一个敏感模型;
  • 事务真实发起者却并没有这些权限。

所以测试里对 with_user(admin_user)sudo() 是一视同仁的。

这背后体现的是 Odoo 的一个态度:

不要因为“命令写在父对象字段里”,就把对目标对象的权限判断模糊掉。


六、对开发实践最重要的启发

1)不要把 x2many Command 当作“字段赋值语法糖”

[(0, 0, vals)][(1, id, vals)][(4, id)] 这些写法看起来像字段更新, 但本质上它们是:

  • 代替你去操作另一张表;
  • 并可能触发另一套访问控制与业务副作用。

2)父记录能 sudo,不代表子模型就应该顺带 sudo

很多 bug 的根子就在这里:

  • 你想临时放宽父对象动作;
  • 结果顺手放宽了子对象;
  • 最后把账户、组、签署对象、共享对象一类敏感数据也一起暴露了。

3)如果确实需要高权限改子对象,最好显式拆开写

与其把敏感操作藏在 x2many Command 里,不如:

  • 先明确找到目标记录;
  • 明确说明为什么需要高权限;
  • 在可审计的位置单独执行;
  • 必要时再补业务级校验。

这样至少你知道自己提权改的是谁,而不是让一个看似普通的 write({'line_ids': [...]}) 偷偷带出一串副作用。


七、什么时候你最该想到这条规则

如果你遇到下面这些现象,就该立刻想到 _allow_sudo_commands

  • 父记录 sudo().write() 能走进去,但 x2many 子命令报 AccessError
  • with_user(admin) 明明已经换了用户,还是改不了某些 x2many 目标;
  • 同样的命令,对普通模型有效,对用户/组/敏感对象无效;
  • 你以为问题在 record rule,实际问题在 relational field 主动撤销了 sudo 命令环境。

这类问题如果只盯着当前 env.uid,很容易永远调不明白。

你真正要看的是:

  1. 目标 comodel 是谁;
  2. 它的 _allow_sudo_commands 是不是 False
  3. 命令最终在哪个用户环境里落地;
  4. 事务默认用户有没有相应访问权。

结语

这套设计很值得尊重,因为它处理的是一个很常见、但很容易被忽略的误区:

“我现在拿着高权限 env,所以我顺手塞进 x2many 的一切都应该高权限执行。”

Odoo 19 给出的答案是:不一定。

对于敏感模型,框架宁可在 relational field 层把命令执行环境拉回事务原始用户,也不愿意让 x2many Command 变成一条安静的越权隧道。

所以记住一句最实用的话:

sudo() 提的是当前 recordset 的执行环境,不是所有 x2many 子命令的永久免检通行证。

DISCUSSION

评论区

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