跳至内容

第8章 ORM 的完整检查链路

8.1 Odoo 18 的统一入口

BaseModel.check_access(operation) 位于 odoo/models.py:4370-4428。核心伪代码:

def _check_access(self, operation):
    if not ir_model_access.check(self._name, operation):
        return acl_error

    if self contains real records:
        domain = ir_rule._compute_domain(self._name, operation)
        forbidden = self - self.sudo().filtered_domain(domain)
        if forbidden:
            return rule_error

    return None

统一 API:

方法 行为
records.check_access(op) 全部允许则无返回,否则抛 AccessError
records.has_access(op) 返回 bool
records._filtered_access(op) 返回允许的子集

旧 API 在 Odoo 18 已 deprecated:

  • check_access_rights()
  • check_access_rule()
  • _filter_access_rules()

新代码优先使用统一 API,避免只检查 ACL 却忘记规则。

search(domain)
  -> search_fetch()/search_count()
  -> _search(domain)
       1. 空 recordset 检查 read ACL
       2. 检查 domain 中字段是否可读
       3. _where_calc(domain)
       4. _apply_ir_rules(query, 'read')
       5. SQL 返回符合 用户domain AND 规则domain 的记录

结果特点:

  • 不可见记录被静默过滤。
  • search、count、分组统计通常共享同一规则边界。
  • 复杂规则会直接影响 SQL 性能。

8.3 read() / fetch() 链路

read(fields)
  -> check_field_access_rights('read', fields)
  -> fetch(fields)
       -> 通过带 read rules 的搜索取真实记录
       -> 显式 ID 有禁止记录时抛 AccessError
  -> 格式化返回值

直接属性访问在 cache miss、进入 _fetch_field() 时会检查字段 groups;cache hit 可能直接返回已有值,源码见 odoo/fields.py:1215-1266。因此尤其要警惕同一 prefetch/cache 在 sudo 与降权 recordset 间复用,见第 9 章。

8.4 create() 链路

create(vals_list)
  1. 空 recordset check_access('create') -> 实际只检查模型 ACL
  2. 补默认值、预计算值、继承父记录
  3. _create(): INSERT、关系字段、parent_path、存储字段约束
  4. _create(): 对新 records check_access('create') -> ACL + create rules
  5. 回到外层:inverse、非存储 inverse 约束
  6. _check_company(模型启用时)

关键语义:

  • create 规则检查 _create() 时已经落库的新记录,而不是所有 inverse 和自定义后续逻辑完成后的绝对最终状态。
  • 当前 Odoo 18 BaseModel.create() 没有通用字段 groups 检查,敏感模型应显式补上。
  • create 覆盖中若先 sudo,再调用 super,会让 ACL、规则和字段组全部失去意义。
  • 允许调用者指定 owner_idcompany_idstate 时,必须验证这些初始值。

8.5 write() 链路

records.write(vals)
  1. 对整个旧 recordset check_access('write')
  2. check_field_access_rights('write', vals.keys())
  3. 更新字段、关系、inverse、constraints
  4. _check_company(模型启用时)
  5. 不对新状态重新运行 write rule

重要结果:

  • 一条禁止记录混进 recordset,整批 write 失败。
  • 规则只证明用户能开始修改旧记录,不证明任何新值都合法。
  • 工作流状态、负责人、公司转移、额度等要在业务代码中检查。
records.unlink()
  1. 对整个 recordset check_access('unlink')
  2. ondelete / 关系清理 / 数据库删除
  3. 缓存失效与删除日志

unlink 没有字段权限问题,但经常需要业务约束,例如已过账凭证不可删、仅草稿可删。ACL 和规则只说明身份/范围允许,不代表业务状态允许。

8.7 五种操作对比

操作 ACL 记录规则 字段 groups 不可见记录表现 状态检查时点
search read 编入 SQL domain 字段检查 静默过滤 查询时
read read read 检查 显式 ID 常抛错 读取时
create create _create() 插入后检查 本快照需显式加固 抛错并正常回滚 已落库、外层 inverse 前的状态
write write 写前检查 write 检查 整批抛错 旧记录状态
unlink unlink 删除前检查 不适用 整批抛错 旧记录状态

8.8 关联模型必须分别保护

保护主单模型不等于保护:

  • 明细行;
  • chatter 消息和附件;
  • SQL view/分析模型;
  • 报表自定义模型;
  • 中间关系模型;
  • 导出或聚合接口。

销售模块为 sale.ordersale.order.linesale.report 分别建规则,就是很好的源码示范。测试安全需求时要沿数据流列出每个可查询模型,而不是只测表单主模型。

8.9 模型公开方法的安全模板

一个安全业务方法通常按这个顺序思考:

确认 recordset 形状
  -> 非 sudo 的 ACL + 记录访问
  -> 业务能力/组
  -> 当前公司和组织关系
  -> 当前状态、金额、职责分离
  -> 校验全部输入值
  -> 用最小权限执行
  -> 必要时记录审计

不要把所有检查都换成 has_group()。有组不代表对目标记录有权;对记录有 write 也不代表能审批。两者是不同维度。

老赵解读

读源码时不要只找一个 check_access 函数,而要沿着一次真实操作走完整条链:模型能力、字段、记录范围、公司一致性、业务条件,以及可能的 sudo。尤其要把公开模型方法的参数视为不可信输入;先缩小记录集并完成校验,再做最小范围提权,才是可审计的写法。

额定值
0 0

目前没有任何评论。

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