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 却忘记规则。
8.2 search() 链路
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_id、company_id、state时,必须验证这些初始值。
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 失败。
- 规则只证明用户能开始修改旧记录,不证明任何新值都合法。
- 工作流状态、负责人、公司转移、额度等要在业务代码中检查。
8.6 unlink() 链路
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.order、sale.order.line、sale.report 分别建规则,就是很好的源码示范。测试安全需求时要沿数据流列出每个可查询模型,而不是只测表单主模型。
8.9 模型公开方法的安全模板
一个安全业务方法通常按这个顺序思考:
确认 recordset 形状
-> 非 sudo 的 ACL + 记录访问
-> 业务能力/组
-> 当前公司和组织关系
-> 当前状态、金额、职责分离
-> 校验全部输入值
-> 用最小权限执行
-> 必要时记录审计
不要把所有检查都换成 has_group()。有组不代表对目标记录有权;对记录有 write 也不代表能审批。两者是不同维度。
老赵解读
读源码时不要只找一个 check_access 函数,而要沿着一次真实操作走完整条链:模型能力、字段、记录范围、公司一致性、业务条件,以及可能的 sudo。尤其要把公开模型方法的参数视为不可信输入;先缩小记录集并完成校验,再做最小范围提权,才是可审计的写法。
目前没有任何评论。