第七章 安全性

篇别:第一篇 基础知识

本章学习目标

  • 建立 ACL → 记录规则 → 字段级权限 → 视图/菜单 的分层安全观。
  • 能独立编写 ir.model.access.csvir.rule,并理解 全局规则与分组规则 的组合逻辑。
  • 能识别 sudo、Controller、SQL、仅 invisible 等典型陷阱并给出修复思路。
  • 了解 Odoo 19 统一权限 APIcheck_accesshas_access_filtered_access 等,以官方签名为准)。

导读:没有「单一开关」的安全

Odoo 安全是 纵深防御:即使菜单隐藏、字段 invisible,RPC 仍可能直达模型方法。因此 模型层 ACL 与记录规则 是底线;界面层只是体验与误操作防护。


7.1 访问控制列表(ACL)

7.1.1 知识要点

  • ir.model.access:回答「某 用户组 对某 模型 能否 增删改查」。
  • 无 ACL:非超级用户通常 完全无法 通过 ORM 访问该模型(超级用户绕过)。
  • 多组权限:用户属于多组时,同一模型上 权限合并取并集(读/写/建/删任一允许则为允许,具体以 ORM 实现为准,建议用测试验证)。
  • CSV 文件security/ir.model.access.csvmodel_id:id 引用 model_<模型名点换下划线> 形式的 XML id。

7.1.2 案例

案例 A:普通用户可读写建、不可删

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_library_book_user,library.book.user,model_library_book,base.group_user,1,1,1,0

案例 B:仅图书馆管理员可删

access_library_book_librarian,library.book.librarian,model_library_book,my_library.group_librarian,1,1,1,1

用户需同时满足:属于有 read 的组 且对记录通过 record rule;删除还需 perm_unlink

7.1.3 截图占位

图 7-1 技术 → 模型访问权限:同一模型多条组规则

图 7-1b 从模型表单打开「访问权限」快捷入口

7.1.4 本节练习

  1. 简答perm_unlink=0 时,超级用户与普通用户分别能否 unlink
  2. 实操:新增组 group_library_readonly,仅 perm_read=1,其余 0,用测试用户验证无法 write
  3. 判断:只要定义了 ACL,就无需记录规则。( )

参考答案提示:1. 超级用户可;普通用户不可。3. 错,多租户/多公司/行级隔离靠 rule。


7.2 记录规则(Record Rules)

7.2.1 知识要点

  • ir.ruledomain_forcePython 表达式 或 domain 列表字符串,限制 可见/可操作 记录集。
  • perm_read/write/create/unlink:可细分;未勾选表示该规则 不限制 对应操作(以界面逻辑为准)。
  • 全局规则groups 为空时适用于所有人(慎用);分组规则 仅对列出的组生效。
  • 多规则:同一模型多条规则通常 AND 合并(理解:记录须同时满足所有适用规则)。

7.2.2 案例

案例 A:仅看自己创建的书

<record id="library_book_rule_own" model="ir.rule">
  <field name="name">图书:用户仅自己创建</field>
  <field name="model_id" ref="model_library_book"/>
  <field name="domain_force">[('create_uid','=',user.id)]</field>
  <field name="groups" eval="[(4, ref('base.group_user'))]"/>
  <field name="perm_read" eval="True"/>
  <field name="perm_write" eval="True"/>
</record>

案例 B:多公司标准形态(示意)

<field name="domain_force">['|',('company_id','=',False),('company_id','in', company_ids)]</field>

company_ids 为规则求值上下文中可用变量(以官方文档为准)。

7.2.3 截图占位

图 7-2 记录规则:domain_force 与 perm_*

7.2.4 本节练习

  1. 判断sudo() 后记录规则仍然生效。( )
  2. 实操:写一条规则:「经理组可看全部书,普通用户仅看 state=draft 的书」(两组两条 rule 或一条带组,思考哪种更清晰)。
  3. 简答:规则 domain_force 写错导致 所有用户看不到数据,如何快速恢复?

参考答案提示:1. 错。3. 超级用户登录关闭规则或改 XML 再 -u


7.3 用户组

7.3.1 知识要点

  • res.groupscategory_id 决定设置里的分组;implied_ids 可继承子组。
  • 视图/菜单 groups:XML 属性,隐藏 UI;不替代 ACL。
  • 字段 groups:模型字段上限制 读写(仍需 ACL 打底)。

7.3.2 案例

<button name="action_reset" type="object" string="重置"
        groups="base.group_system"/>
internal_note = fields.Text(groups='my_module.group_manager')

7.3.3 截图占位

图 7-3 用户表单 → 访问权限:多组勾选

7.3.4 本节练习

  1. 简答:菜单 groups="sales_team.group_sale_salesman" 与模型 ACL 仅 group_user 同时存在时,销售员能否从 RPC 读到数据?
  2. 实操:新建「图书馆」应用分类与两个组:读者/管理员,并各挂不同菜单。

参考答案提示:1. 若能通过其他 action 打开同一模型且 ACL 允许,则能。


7.4 字段级权限(新增)

7.4.1 知识要点

薪资、密钥、身份证号 等字段,除 不可见 外,可配置 字段级访问(名称与入口以 19.0 为准:如 Field Access / ir.model.fields 相关安全扩展),限制 读/写 组。

导出、API、报表 仍须测试是否泄露。

7.4.2 案例

流程建议:在技术菜单创建字段访问规则 → 指定模型、字段、组、只读或可写 → 用普通用户测 read 返回字典中是否含该 key。

7.4.3 截图占位

图 7-4 字段级访问配置界面

7.4.4 本节练习

  1. 实操:为 res.partner 自定义字段 x_id_card 限制仅 group_hr 可读。
  2. 简答:字段级只读能否被 fields_get 元数据暴露字段存在性?敏感字段如何进一步处理?

参考答案提示:2. 可能暴露存在;可配合 ACL、独立模型或不在无关视图出现。


7.5 安全陷阱(新增)

7.5.1 知识要点

陷阱 说明
Controller + sudo + 用户 id 未校验 该用户是否有权访问该 id
invisible 前端隐藏,RPC 仍可读
SQL 拼接 注入与越权
复制 method_route 无 CSRF 状态变更应用 POST + token
记录规则未测多公司 串库、空数据

7.5.2 案例

反面案例

# 危险
rec = request.env['library.loan'].sudo().browse(int(kw.get('id')))
return rec.read()

改进思路(示意)

rec = request.env['library.loan'].browse(int(kw.get('id')))
rec.check_access('read')
return rec.read()

或使用 request.env.user_filtered_access 过滤后的记录集(以 19.0 API 为准)。

7.5.3 截图占位

图 7-5 安全评审检查表(自绘或内部规范截图)

7.5.4 本节练习

  1. 改错:将反面案例改为无 sudo 并校验权限。
  2. 情景题:公开路由 auth='public' 返回「今日借阅册数」如何既不泄露明细又防刷?

7.6 Odoo 19 权限新方法(新增)

7.6.1 知识要点

在业务代码中优先使用统一入口:has_access(布尔)、check_access(不通过抛异常)、_filtered_access(返回当前用户可见子集),减少手写 if not user.has_group 的分散逻辑。

方法名与参数ORM / 安全相关文档 为准。

7.6.2 案例(伪代码)

records = self.env['library.book'].search(domain)
records = records._filtered_access('read')

7.6.3 截图占位

图 7-6 官方文档权限检查代码片段

7.6.4 本节练习

  1. 实操:在报表方法开头对 model 调用 check_access('read'),用无权限用户跑单元测试断言 AccessError
  2. 简答_filtered_accesssearch + sudofiltered 的差异?

本章综合练习

  1. 威胁建模:为「借阅」模块做 STRIDE 简表(至少 欺骗、越权、信息泄露 各一条与缓解)。
  2. 渗透思路:普通用户能否对无 create ACL 的模型调用 create?如何用 HttpCaseRPC 脚本 验证?
  3. 设计题:「读者只能借自己的书,管理员可看全部」——写出需要的 组、ACL、规则 提纲(不必写全 XML)。
  4. 拓展:阅读官方 Security 文档中 SuperuserTesting 章节,记录一条测试建议。

本章对应白皮书目录:第七章 安全性。