14.1 适合一人或小团队的五阶段路线
阶段 0:学习、测试与只读权限解释器
目标:不改变 Odoo 最终授权,只把原生结果解释清楚。
交付物:
- 本教材与
odoo_permission_lab全部实验通过。 - 用户、组、隐含组、ACL、全局规则、组规则关系图。
- 给定 user/model/operation 的只读解释结果。
- 菜单/动作/按钮“仅入口”风险扫描。
sudo()、公开方法、controller 的代码检查清单。
完成标准:解释器不 sudo 泄露业务记录;对官方 ACL/rule 测试语义一致;不修改 _compute_domain()。
这个阶段适合先做成开源能力证明和内容资产,也能在真实咨询诊断中直接使用。
阶段 1:角色模板、来源和原生组投影
目标:先解决“全局岗位授权、来源可追踪、可预览和可撤销”,不宣称已经支持按公司/组织隔离的岗位。
交付物:
- 1A:稳定 role 身份、不可变 role version、assignment、来源和差异预览,不改变最终授权。
- 1B:仅对 managed allowlist 中的全局、低风险原生组做确定性投影。
- 现有手工关系的基线迁移、直接写边界和漂移检测。
- implied groups 展开后的 SoD 静态检查,以及传递闭包增删 reconcile 测试。
- 变更预览、审批后生效、撤销和基础审计。
- 原生手工组与增强模块管理组的边界。
此阶段只支持长期、立即生效的全局 assignment。可以预留有效期字段作为未来模型结构,但不能用它投影未来生效或定时到期的组;公司/组织字段此时只能标记为“声明范围,未运行时强制”。临时、代理、紧急和 company-scoped 角色均不在阶段 1 生效。
完成标准:多来源授权撤销正确;幂等 reconcile;同事务和跨 worker 缓存失效正确;不会扩大未映射 ACL;任何界面都不会把声明范围误报为已强制范围。
阶段 2:选择一个业务包做数据范围和 capability
建议只选一个:销售订单或费用报销。费用更适合验证本人、部门经理、直属上级、自批限制;销售更适合验证本人/全部、团队范围、折扣审批和多公司。
交付物:
- 组织树和首批结构化 scope。
- 该业务模型及关联行/报表的 adapter。
- 3 至 5 个高价值 capability。
- 在该业务包内真正强制 company/org-scoped assignment。
- 对候选投影组展开 implied groups、ACL、规则和动作,形成完整授权面清单;优先创建窄技术组。
- 直接 RPC、导出、报表、多公司的完整测试。
- SQL 性能基线。
完成标准:原生 domain 始终保留;增强层按 OR_i(Assignment_i 的 capability + company + scope + limit) 计算后再与原生结果 AND;无有效 assignment 默认拒绝已 enforce 的能力;组织调动和撤销在提交后的新请求生效;候选组的每项传递授权都已受 adapter/capability 强制或书面接受为全局权限,主单、行、分析模型及组带出的其他模型/动作均无旁路。
阶段 3:审批、临时授权、SoD 和审计
目标:从权限配置工具升级为可治理的授权流程。
交付物:
- 多级审批、临时、代理、紧急授权。
- 静态/动态 SoD。
- 周期性复核。
- 追加式审计和拒绝事件外部日志。
- 高风险角色和 capability 告警。
完成标准:运行时决策按精确时间拒绝过期 assignment;撤销提交后的新请求立即拒绝;临时授权不向未受保护的授权面投影扩权组;不能自批;异常回滚时拒绝审计仍可用;敏感字段脱敏。
阶段 4:外部组织与审批集成
目标:在核心模型稳定后接企业微信、钉钉、飞书或客户已有 IAM。
交付物:
- 稳定 ID 映射、dry-run、幂等增量同步。
- 冲突与停用人工复核。
- 回调验签、防重放和审批版本校验。
- Excel 草稿导入与差异预览。
完成标准:外部平台故障不导致权限被清空;重复回调不重复授权;名称变化不破坏身份映射。
14.2 何时才适合产品化
权限增强很容易过早平台化。建议先从咨询、诊断和一个受控业务包中验证重复问题,再扩大通用层。至少观察:
- 是否有多家客户反复提出同一种岗位/范围/审批问题。
- 是否完成至少两次有边界的付费交付。
- 是否能明确哪些是通用内核、哪些是客户特例。
- 是否有测试、文档、迁移和持续维护负责人。
- 是否有真实的性能数据和升级兼容记录。
在达到重复验证前,优先沉淀模型适配器、测试矩阵、诊断报告和实施清单,不承诺“大而全中国式权限平台”。
14.3 模块边界建议
起步时不要拆太多仓库。一个可行演进是:
odoocc_auth 核心角色、assignment、解释、审计
odoocc_auth_hr 只有确实依赖 hr 组织/员工时再拆
odoocc_auth_sale 销售 adapter 与 capability
odoocc_auth_expense 费用 adapter 与 capability
odoocc_auth_connector_* 外部平台稳定后再拆
MVP 可以只有核心模块加一个业务 adapter,等依赖与维护边界真实出现再拆分。
增强模块自身也是高敏感资源,应至少分离:
- 权限配置管理员。
- 授权审批人。
- 权限审计员,只读且不能授权。
- 系统技术管理员。
普通用户不能直接 write 自己的 assignment、role、scope 或审计事件。对这些模型谨慎使用 groupless ACL 和 sudo 关系命令。
14.4 权限核心模型速查表
| 模型 | 关键字段 | 核心职责 |
|---|---|---|
res.users |
groups_id, company_id, company_ids, share, action_id |
运行时主体 |
res.groups |
users, implied_ids, category_id, model_access, rule_groups |
技术能力集合 |
ir.module.category |
name, sequence, parent_id 等 |
组的应用分类与用户表单组织 |
ir.model |
model, name, state |
模型元数据,ACL/rule 的目标 |
ir.model.fields |
model_id, name, ttype, relation |
字段元数据;不是 Python 字段 groups 的完整替代配置 |
ir.model.access |
model_id, group_id, 四个 perm_*, active |
模型 CRUD grant |
ir.rule |
model_id, groups, global, domain_force, 四个 perm_* |
记录范围 |
ir.ui.menu |
parent_id, groups_id, action |
导航入口 |
ir.ui.view |
model, arch_db, inherit_id, mode, groups_id |
页面架构与节点裁剪 |
ir.actions.act_window |
res_model, domain, context, groups_id |
打开模型工作台 |
ir.actions.server |
model_id, state, code, groups_id |
受额外执行检查的服务器动作 |
ir.actions.report |
model, report_name, groups_id, attachment_use |
报表入口和渲染配置 |
res.company |
公司主数据 | 多公司上下文,不等于任意组织节点 |
14.5 开发前安全设计模板
为每个新业务模型先填写:
模型:
用户类型:Internal / Portal / Public
ACL:
哪些组有 R/W/C/U?为什么?
记录范围:
全局强制规则:
各组 read 规则:
各组 write 规则:
create 后新记录必须满足:
unlink 范围:
字段:
敏感字段:
Python groups:
create payload 加固:
状态/额度字段写策略:
业务方法:
公开方法:
capability:
状态前置条件:
公司/组织条件:
SoD:
旁路:
Controller / portal token:
Report / export / import:
Attachments / related lines / analysis models:
sudo / SQL:
测试:
允许案例:
拒绝案例:
多公司:
缓存与撤销:
性能基线:
14.6 代码审查清单
- [ ] 所有新模型都有明确组 ACL,没有意外 groupless grant。
- [ ] ACL 矩阵按并集分析过,没有把 False 当 deny。
- [ ] 全局规则和组规则分别列出,并写出最终公式。
- [ ] 所有
perm_* = False都按“不参与操作”复核过。 - [ ] 强制公司/合规规则不会被组规则 OR 放宽。
- [ ] create 验证敏感字段、初始状态、负责人和公司。
- [ ] write 验证变更后的业务不变量,不只依赖旧记录规则。
- [ ] Python 字段 groups 与视图节点 groups 没有混淆。
- [ ] 每个
type="object"公开方法都有服务端校验。 - [ ] 窗口/报表 groups 没有被当成最终授权。
- [ ] controller 对每个 docid 做访问或 token 校验。
- [ ]
sudo()都有明确理由、最小范围并位于授权之后。 - [ ] 原始 SQL 有等价权限和缓存/约束处理。
- [ ] 主模型、关联行、附件、分析和报表模型都已覆盖。
- [ ] 多公司 rule 与
_check_company分别测试。 - [ ] 组、部门、岗位、有效期变化会正确失效缓存。
- [ ] 正向、反向、边界、批量和直接 RPC 测试齐全。
14.7 四周源码学习计划
第一周:用户、组和 ACL
- 读
res_users.py:175-415,1142-1209,1434-1610。 - 画组继承图并在 shell 观察
groups_id。 - 读
ir_model.py:2006-2149。 - 完成实验 1、2,自己写三组 ACL 真值表。
第二周:记录规则和 CRUD
- 逐行读
ir_rule.py:12-201。 - 读
models.py:4370-4428,4476-4488,4618-4666,4895-5240。 - 完成实验 3 至 6、9。
- 阅读销售和费用规则并写出最终组合 domain。
第三周:字段、视图、动作和按钮
- 读
fields.py:950-956和models.py:3693-3782。 - 读
ir_ui_menu.py:76-152、ir_ui_view.py:989-1060。 - 跟
/web/action/load和/web/dataset/call_button。 - 完成实验 7、8、10,写一个 HttpCase 验证直达 RPC/报表。
第四周:多公司、sudo、缓存和增强设计
- 读
api.py:529-746、models.py:6215-6297。 - 做两公司规则、非法 context、sudo 回归。
- 为一个真实业务模型做权限矩阵和 SQL 计划。
- 只设计阶段 0/1 的增强 MVP,不急着实现全局动态规则。
14.8 复习题
- 用户没有模型 read ACL,但有一条匹配全部记录的 read rule,能否读取?
- 用户同时拥有两个组,一个 ACL read=True,另一个 read=False,最终能否 read?
- 两条全局规则和两条适用组规则如何组合?
- 规则
perm_write=False是否禁止 write? - 为什么用户能 search 到 0 条,却在 browse(id).read() 时收到权限错误?
[('owner_id', '=', user.id)]write rule 能否阻止把 owner 改给别人?- Python 字段
groups=和 XML 节点groups有何不同? - 按钮只给 Manager 显示,普通用户是否一定不能调用按钮方法?
- 窗口动作 domain 是否是数据权限?
- 服务器动作 groups 与窗口动作 groups 有何关键差异?
- 报表 Print 菜单隐藏后,为什么还要检查直达报表 URL?
sudo()是否会把env.uid改为 1?check_company=True是否能代替公司记录规则?- 为什么不能把岗位、部门、公司全部表示成
res.groups或res.company? - 多个业务岗位的 capability、公司和数据范围应怎样组合,才能避免跨岗位拼接放权?
14.9 参考答案
- 不能。ACL 先检查,规则不能授予模型能力。
- 能。ACL 是 grant 并集,False 不是 deny。
Global1 AND Global2 AND (Group1 OR Group2)。- 不禁止;表示 write 时不应用该规则。
- search 把规则编入 SQL 静默过滤;显式 recordset 访问会检查禁止记录并抛错。
- 不能保证。write rule 检查旧记录,需在 write/业务方法中验证 owner 变更。
- Python 字段 groups 是 ORM 字段访问边界;XML groups 只裁剪该视图节点。
- 不一定。公开方法可直接 RPC,方法内必须复核。
- 不是,只是该入口的默认过滤。
- 服务器动作
run()会再次检查允许组/写权限;窗口动作直达加载没有同等通用强制。 - 标准报表 groups 主要过滤绑定入口,直达 route 不统一强制该组。
- 不会。它保留 uid,只把环境设为 su;UID 1 是约定超级用户。
- 不能。前者管关联一致性,后者管记录可见性。
- 它们是技术能力、任职组织和法人账套三个不同维度,生命周期和组合方式不同。
- 先在每个 assignment 内把 capability、公司、scope、额度和 SoD 绑定为一个 AND 分支,再对完整分支做 OR;最后与原生权限和合规/业务不变量做 AND。不能分别合并 capability 与 scope。
14.10 核心源码索引
版本与 Environment
| 主题 | 源码位置 |
|---|---|
| Odoo 18.0 版本 | odoo/release.py:11-15 |
| Environment 构造与切换 | odoo/api.py:529-634 |
| 同事务 Environment 与共享 cache | odoo/api.py:563-585,981-1025 |
| superuser/admin/system | odoo/api.py:656-676 |
| 当前公司与启用公司 | odoo/api.py:678-746 |
sudo/with_user/with_company |
odoo/models.py:6226-6297 |
用户和组
| 主题 | 源码位置 |
|---|---|
res.groups 字段 |
odoo/addons/base/models/res_users.py:175-200 |
res.users 字段 |
odoo/addons/base/models/res_users.py:318-415 |
| 用户类型互斥 | odoo/addons/base/models/res_users.py:605-637 |
| 用户本人可读写字段特例 | odoo/addons/base/models/res_users.py:640-788 |
| 模板用户新增组同步到现有内部用户 | odoo/addons/base/models/res_users.py:711-757 |
has_groups/has_group |
odoo/addons/base/models/res_users.py:1142-1209 |
| 隐含组物化 | odoo/addons/base/models/res_users.py:1434-1589 |
| 动态用户组表单 | odoo/addons/base/models/res_users.py:1591-1979 |
| 基础组继承链 | odoo/addons/base/security/base_groups.xml:10-92 |
ACL、规则与 ORM
| 主题 | 源码位置 |
|---|---|
ir.model.access |
odoo/addons/base/models/ir_model.py:2006-2149 |
ir.rule 字段/求值/组合/缓存 |
odoo/addons/base/models/ir_rule.py:12-201 |
| Odoo 18 统一访问入口 | odoo/models.py:4370-4474 |
| unlink | odoo/models.py:4476-4616 |
| write | odoo/models.py:4618-4893 |
| create | odoo/models.py:4895-5240 |
| 公司一致性 | odoo/models.py:4270-4368 |
| 搜索附加规则 | odoo/models.py:5525-5538,5735-5763 |
字段、菜单、视图和动作
| 主题 | 源码位置 |
|---|---|
字段 groups 判定 |
odoo/fields.py:950-956 |
| 字段属性访问与 cache miss 检查 | odoo/fields.py:1215-1266 |
fields_get/check_field_access_rights |
odoo/models.py:3693-3782 |
| 菜单字段与可见性 | odoo/addons/base/models/ir_ui_menu.py:19-44,76-152 |
| 视图记录组约束 | odoo/addons/base/models/ir_ui_view.py:422-428 |
| 视图记录访问检查及主要调用 | odoo/addons/base/models/ir_ui_view.py:672-688,2176-2183 |
| 视图节点组编码与 ACL 后处理 | odoo/addons/base/models/ir_ui_view.py:989-1154 |
| 视图缓存与加载 | odoo/addons/base/models/ir_ui_view.py:2553-2776 |
| 动作基类与 Web 安全字段 | odoo/addons/base/models/ir_actions.py:52-73,218-252 |
| 动作绑定过滤 | odoo/addons/base/models/ir_actions.py:149-215 |
| 窗口动作字段 | odoo/addons/base/models/ir_actions.py:302-327 |
| 服务器动作字段和 run | odoo/addons/base/models/ir_actions.py:503-588,936-1011 |
| 报表动作字段/查找 | odoo/addons/base/models/ir_actions_report.py:141-176,623-657 |
| 报表缓存附件/文档上下文 | odoo/addons/base/models/ir_actions_report.py:248-261,774-814,1082-1097 |
| QWeb 节点 groups | odoo/addons/base/models/ir_qweb.py:1847-1864 |
| 顶层 template groups 转换 | odoo/tools/convert.py:450-514 |
| 嵌入式动作可见性 | odoo/addons/base/models/ir_embedded_actions.py:8-29,71-91 |
Web 入口
| 主题 | 源码位置 |
|---|---|
| 菜单 Web 加载入口 | addons/web/controllers/home.py:76-88; addons/web/models/ir_ui_menu.py:12-88 |
| 动作加载 | addons/web/controllers/action.py:22-51 |
| 动作结果清洗 | addons/web/controllers/utils.py:23-47 |
| 按钮调用 | addons/web/controllers/dataset.py:32-43 |
| 按钮 object/action 前端分支 | addons/web/static/src/webclient/actions/action_service.js:1399-1443 |
| 前端组判断 | addons/web/static/src/core/user.js:51-78,106-110 |
| 报表路由 | addons/web/controllers/report.py:23-50 |
| 公开方法名检查 | odoo/models.py:151-158 |
| Controller auth 语义 | odoo/http.py:661-701 |
| HTTP CSRF 实际校验 | odoo/http.py:2067-2078 |
| Portal token 参考 | addons/portal/controllers/portal.py:373-392 |
官方测试与业务示例
| 主题 | 源码位置 |
|---|---|
| 字段、ACL、规则基础测试 | odoo/addons/base/tests/test_acl.py:13-303 |
| search/read/write/unlink 表现 | odoo/addons/base/tests/test_orm.py:55-95 |
| 视图 groups 测试 | odoo/addons/base/tests/test_views.py:2328-2368 等 |
| 服务器动作组测试 | odoo/addons/base/tests/test_ir_actions.py:394-428 |
| sudo/with_user 测试 | odoo/addons/test_access_rights/tests/test_feedback.py:23-87 |
| ACL 拒绝提示测试 | odoo/addons/test_access_rights/tests/test_feedback.py:89-158 |
| 全局/组规则测试 | odoo/addons/base/tests/test_acl.py:194-303 |
| 多公司一致性测试 | odoo/addons/test_new_api/tests/test_company_checks.py:46-212 |
| 销售规则 | addons/sale/security/ir_rules.xml:5-97 |
| 费用规则 | addons/hr_expense/security/ir_rule.xml:4-89 |
14.11 官方文档入口
- Odoo 18 Security in Odoo:https://www.odoo.com/documentation/18.0/developer/reference/backend/security.html
- Multi-company Guidelines:https://www.odoo.com/documentation/18.0/developer/howtos/company.html
- Restrict access to data:https://www.odoo.com/documentation/18.0/developer/tutorials/restrict_data_access.html
官方文档适合建立概念;遇到版本差异时,以当前部署版本源码和自动化测试为准。
14.12 术语表
| 术语 | 本文含义 |
|---|---|
| Authentication | 身份认证,确认“你是谁” |
| Authorization | 授权,确认“你能做什么” |
| ACL | 模型级 CRUD grant,ir.model.access |
| Record Rule | 记录范围 domain,ir.rule |
| Global Rule | 无 groups 的规则,与其他全局规则 AND |
| Group Rule | 有 groups 的规则,适用规则之间 OR |
| Capability | CRUD 之外的业务操作,如审批、过账 |
| Scope | 本人、部门、公司等数据范围 |
| SoD | Separation of Duties,职责分离 |
| Assignment | 某用户在组织/公司/有效期内获得角色的生效授权 |
| Projection | 把业务角色确定性映射到原生 groups 等运行时结构 |
su |
超级用户模式标志,不等同 Settings 组 |
allowed_company_ids |
本次请求启用的公司上下文 |
14.13 最终心智模型
遇到任何权限需求,都把自然语言拆成下面一句:
谁(用户/角色/公司/组织)
在什么时间和来源下
对什么模型的哪些记录
能执行哪种 CRUD 或业务 capability
能访问哪些字段
入口如何展示
失败和变更如何解释、审计与回收
然后按固定顺序落地:
原生组 -> ACL 能力上限 -> 全局规则 -> 组规则
-> 字段 groups -> 业务方法 -> UI 裁剪
-> 多公司/sudo/controller 复核 -> 自动化测试 -> 审计解释
只要始终区分“能力、范围、字段、业务动作、入口”这五个维度,Odoo 权限系统就不再是一堆零散勾选,而是一套可以推导、测试和逐步增强的授权模型。
老赵解读
实现路线应从可验证的最小闭环开始:选一个模型、少量角色和几种典型数据范围,做出授权、执行、解释、到期和审计,再进入真实场景验证。只有同类问题在多个客户中重复出现,边界、测试和维护责任都清楚后,才适合产品化。权限增强的竞争力最终不是配置项多,而是让管理员看得懂、业务跑得通、审计说得清。
目前没有任何评论。