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-746、res_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.company、env.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-2149ir_rule.py:180-201res_users.py:267-276,775-786odoo/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-585、odoo/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.users、res.groups、ir.rule、ir.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 不是“让这段代码顺利运行”的快捷键,而是一笔必须说明理由的安全债务。每次使用都应明确:绕过了什么、仍需手工校验什么、提权覆盖多少行、是否会跨公司、是否可能把预取缓存带回普通用户环境。范围越小,后续越容易证明安全。
目前没有任何评论。