跳至内容

第9章 多公司、`sudo()` 与常见绕过路径

9.1 多公司由三套机制协作

用户允许公司        res.users.company_ids
本次启用公司        context['allowed_company_ids'] / env.companies(无或为空时回退用户全部有权公司)
记录可见性          ir.rule 中 company_id domain
关联数据一致性      _check_company_auto + field(check_company=True)

它们不能互相替代。

res.users.company_id 是用户主公司,company_ids 是用户可选择的公司集合。当 context['allowed_company_ids'] 为非空列表时,env.company 取列表首项,env.companies 取列表中的全部公司;当该键缺失或列表为空时,env.company 回退到用户主公司,而 env.companies 回退到用户有权访问的全部 active 公司,并非只包含主公司。常规 Web/RPC 请求通常会提供 allowed_company_ids,但定时任务、后台脚本和其他无完整上下文的调用必须注意这一差异。源码见 odoo/api.py:678-746res_users.py:874-878

9.2 记录隔离与关联一致性不是一回事

记录规则:

[('company_id', 'in', company_ids)]

回答“当前用户能看到哪些公司记录”。

字段与模型配置:

_check_company_auto = True

company_id = fields.Many2one("res.company", required=True)
partner_id = fields.Many2one("res.partner", check_company=True)

回答“当前记录能否关联到不兼容公司的另一条记录”。_check_company() 位于 odoo/models.py:4270-4368,并在启用 _check_company_auto 的 create/write 流程调用。它不是可见性授权,也不会自动替代公司规则。

company_dependent=True 表示同一字段按公司保存不同值,同样不是数据可见性权限。

9.3 group_multi_company 只是易用性组

base.group_multi_company 控制公司切换等 UI 能力,用户公司数量变化时会自动同步,见 res_users.py:1869-1890。它不授予任何公司,也不替代 company_ids 或公司规则。

9.4 sudo()with_user()with_company()

API uid 是否变化 su 是否变化 权限结果
records.sudo() 设为 True 绕过 ACL、规则、字段 groups
records.sudo(False) 设为 False,UID 1 例外 恢复普通检查
records.with_user(user) 改为目标用户 设为 False,UID 1 例外 用目标用户测试
records.with_company(company) 非空列表中将公司移到首位;无列表时创建 [company_id];不授予公司
records.with_context(...) 修改上下文,可能影响公司和规则缓存键

sudo() 源码注释明确警告它可能跨越记录规则和公司边界,见 odoo/models.py:6226-6252。它不改变当前用户,所以审计字段通常仍会显示原 uid;这很容易让开发者误以为“审计还在,所以权限也还在”。

with_user() 位于 models.py:6254-6263,非常适合自动化测试。with_company() 位于 models.py:6265-6297,只修改上下文,不立即验证公司授权。已有非空 allowed_company_ids 时,它把指定公司移到首位;原列表缺失或为空时,它建立单项列表 [company_id],不会把 env.companies 的 fallback 集合自动并入。非 sudo 下,未授权或无效公司 ID 通常在随后求值 env.companyenv.companies,或记录规则间接访问它们时才抛 AccessError;仅构造 recordset 本身不会报错。sudo 模式不会执行这项公司合法性检查。

9.5 sudo 的正确使用边界

可以考虑 sudo 的场景:

  • 已经验证 access token 后读取被 portal 规则隔离的目标记录;
  • 系统级定时任务执行明确的后台维护;
  • 读取业务无关的技术元数据;
  • 在授权已通过后,执行一个最小、明确的跨权限写入。

危险写法:

# 搜索和授权都在 sudo 后,调用者可以猜 ID。
record = request.env["demo.document"].sudo().browse(document_id)
return record.read()

较安全的结构:

record = request.env["demo.document"].browse(document_id)
try:
    record.check_access("read")
except AccessError:
    if not valid_access_token(record, token):
        raise
    record = record.sudo()

return serialize_allowed_fields(record)

检查和 sudo 应紧邻,范围最小,并在代码注释中说明为什么需要提升。

9.6 原始 SQL 绕过 ORM

env.cr.execute() 和自写 SQL 不会自动执行:

  • ACL;
  • 记录规则;
  • 字段 groups;
  • _check_company()
  • Python constraints;
  • ORM 缓存失效。

如果确需 SQL,要在 SQL 前显式检查调用者权限,准确构造允许记录集合,并在写后正确 flush/invalidate。对于业务模型,优先使用 ORM;性能问题应先用 query count、索引和执行计划证实,而不是直接以 SQL 逃离安全边界。

9.7 控制器和公共方法绕过

常见漏洞模式:

Controller 只写 auth='user'
  + 接受前端 document_id
  + sudo().browse(document_id)
  + 返回字段或执行动作
  = 已登录任意用户可越权访问猜到的 ID

另一个模式是把私有 helper 写成没有下划线的公开方法,或者公开方法接受调用者可控 context 作为“已授权”标志。RPC 调用者可以自行构造 context,所以不能信任诸如 context['skip_security']context['from_approve_button']

安全旁路标记只应存在于不可 RPC 调用的私有 Python 调用栈中,最好直接调用私有 helper,而不是依赖用户可伪造的 context。

9.8 缓存与 prefetch 风险

Odoo 的主要权限相关缓存包括:

缓存 典型键
ACL 允许模型 uid + mode
用户组 ID user.id
规则最终 domain uid + su + model + mode + allowed_company_ids
组定义 registry 的 groups cache

ACL、规则、用户组变化会清相关 registry cache;跨 worker 通过 registry signaling 传播,相关源码包括:

  • ir_model.py:2123-2149
  • ir_rule.py:180-201
  • res_users.py:267-276,775-786
  • odoo/modules/registry.py:765-793,871-937

自定义权限依赖用户部门、岗位或授权表时,原生失效字段并不知道这些依赖。增强模块必须主动失效,否则用户调岗后可能继续复用旧 domain。

同一游标和事务中的多个 Environment 共享 transaction.cache,但并不因此自动共享同一个 recordset prefetch。with_env() 会保留原 recordset 的 prefetch,因而基于它实现的 sudo()with_user() 也会保留该 prefetch,见 odoo/api.py:563-585odoo/models.py:6215-6263。不要用下面的模式做脱敏验证:

records = records.sudo()
records.mapped("secret")       # 先以 sudo 预取
records = records.with_user(user)
return records.mapped("secret")  # 不能想当然地认为一定重新做完整读取

测试降权时,不能把同一游标和事务内新建的 Environment、recordset 或查询当作缓存隔离边界;它们仍可能使用同一个 transaction.cache。应显式失效相关记录和字段缓存,或使用独立游标/事务重新查询,并覆盖“sudo 预取后降权”的回归用例。依赖用户或公司的敏感计算字段还要正确声明 @api.depends_context('uid', 'company')

9.9 Odoo 18 的 x2many sudo 命令加固

res.usersres.groupsir.ruleir.model.access 等敏感模型设置 _allow_sudo_commands = False。Odoo 18 在 x2many Command 执行时恢复来源 uid/su 边界,避免通过一个 sudo 的父模型关系命令间接提权,相关实现见 odoo/fields.py:4472-4477

这是一项有价值的纵深防御,但不能据此认为所有 sudo 关系写都安全。自定义模型、公开方法和显式 sudo recordset 仍需审计。

9.10 高风险旁路清单

代码审查时全文搜索:

.sudo(
env.cr.execute
request.env[
@http.route
def action_
def button_
_compute_domain
_eval_context
allowed_company_ids
check_field_access_rights

逐处确认:谁能调用、输入是否可信、何时检查记录、sudo 范围多大、公司是否校验、失败是否留下审计、缓存是否失效。

老赵解读

多公司和 sudo 是权限事故的高发区。sudo 不是“让这段代码顺利运行”的快捷键,而是一笔必须说明理由的安全债务。每次使用都应明确:绕过了什么、仍需手工校验什么、提权覆盖多少行、是否会跨公司、是否可能把预取缓存带回普通用户环境。范围越小,后续越容易证明安全。

额定值
0 0

目前没有任何评论。

成为第一个发表评论的人。