11.1 固定的排障顺序
权限问题不要从“再加一条规则试试”开始。按这个顺序查:
1. 当前 env:uid、su、allowed_company_ids
2. 用户类型:Internal / Portal / Public
3. 有效组:包括物化的 implied groups
4. ACL:目标模型和 operation 是否至少一条 grant
5. 全局规则:每一条是否匹配目标记录
6. 组规则:当前用户匹配哪些,OR 后是否通过
7. 字段 groups:错误是否其实来自字段
8. 业务方法:状态、额度、职责分离、公司校验
9. UI 入口:菜单/按钮为什么显示或隐藏
10. sudo、SQL、controller、缓存:是否绕过或复用旧值
先确认失败在哪一层,再修改最小范围。盲目增加一条 TRUE 组规则常常会修好一个用户,却意外放宽同组所有人。
11.2 在 Odoo shell 中观察
以下仅用于专用开发/测试库:
user = env.ref("base.user_demo")
Model = env["permission.lab.document"].with_user(user)
user.groups_id.mapped("full_name")
Model.browse().has_access("read")
Model.browse().has_access("write")
domain = env["ir.rule"].with_user(user)._compute_domain(
"permission.lab.document", "read"
)
domain
Model.search([]).ids
env['ir.rule']._compute_domain() 是内部方法,适合源码学习和调试,不应成为业务模块长期依赖的公开 API。观察错误时注意普通用户和 debug 内部用户看到的规则细节不同;源码会避免向 portal/public 泄露敏感记录名与规则信息。
11.3 有效权限矩阵不要只列 CRUD
建议用下面的列设计测试和未来权限驾驶舱:
| 角色 | 入口 | 模型 CRUD | 数据范围 | 字段读 | 字段写 | 业务能力 | 公司 | 有效期 | 来源 |
|---|---|---|---|---|---|---|---|---|---|
| 销售员 | 销售订单 | R/W/C | 本人及未分配 | 报价字段 | 草稿字段 | 确认限额内订单 | C1 | 长期 | 岗位 |
| 销售经理 | 销售订单 | R/W/C/U | 本部门及下级 | 全部 | 除审计字段 | 审批,不可自批 | C1/C2 | 兼岗三个月 | 授权单 |
只列“有/无菜单”或四个 CRUD,会漏掉最有业务价值的差异。
11.4 自动化测试的基本结构
推荐使用 new_test_user()、with_user()、with_context() 和明确的 AccessError 断言。配套测试见 myaddons/odoo_permission_lab/tests/test_permission_lab.py。
from odoo.exceptions import AccessError
from odoo.tests.common import TransactionCase, new_test_user
class TestSecurity(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = new_test_user(
cls.env,
login="security_user",
groups="my_module.group_user",
)
def test_user_cannot_read_other_record(self):
record = self.env["my.model"].sudo().create({...})
with self.assertRaises(AccessError):
record.with_user(self.user).read(["name"])
def test_public_method_checks_group(self):
record = self.env["my.model"].sudo().create({...})
with self.assertRaises(AccessError):
record.with_user(self.user).action_approve()
不要只断言按钮不存在。模型方法测试更接近恶意或误用的直接 RPC。对自定义 controller 再补 HttpCase,验证真实路由、token、状态码和响应脱敏。
11.5 必须覆盖的测试矩阵
身份和组
- 内部、门户、公共用户。
- 直接组、隐含组、多条继承路径。
- 管理员组与真正 sudo 的差异。
- 增加、撤销和用户降级。
CRUD 和记录范围
- read、write、create、unlink 分开测。
- search 静默过滤与显式 browse 抛错。
- 单记录和混合允许/禁止的批量 recordset。
- 全局规则 AND、组规则 OR、无适用组规则。
- create 后验检查、write 前态检查。
字段与业务动作
fields_get、read、write、create payload、search domain。- 直接调用公开方法,不通过按钮。
- 状态迁移、金额边界、自己审批自己、重复审批。
- import/export/report/attachment 等旁路。
多公司
- 单公司、启用多公司、只允许但未启用的公司。
- 非法
allowed_company_ids。 company_id=False的共享语义。_check_company的跨公司关联。- sudo 与非 sudo 回归。
缓存与生命周期
- 同事务改组后立即生效。
- 新请求和跨 worker 生效。
- 用户调部门、换岗位。
- 临时授权刚到期的边界。
- 规则修改和模块升级。
- sudo 预取后降权。
11.6 权限测试应包含“允许”和“拒绝”两面
只测拒绝容易把业务全部锁死仍然通过;只测允许容易遗漏越权。每条策略至少写:
一个应允许的用户 × 一个应允许的记录/操作
一个应拒绝的用户 × 同一记录/操作
一个相同用户 × 边界外记录/状态
例如“部门经理可审批下属费用”至少要测:直属下属、下级部门、其他部门、经理本人、已审批状态和超额度。
11.7 性能分析
记录规则最终进入业务 SQL,测试数据量必须接近真实规模。重点观察:
- 负责人、公司、部门、状态字段是否有索引。
child_of是否依赖正确的parent_path和索引。- 多层关系 domain 是否产生大量 JOIN/EXISTS。
- 多条组规则 OR 是否破坏索引选择。
- 是否把数万个部门/用户 ID 永久展开成
IN (...)。 - search、read_group、报表和导出是否一致变慢。
在脱敏的测试环境使用 SQL 日志、查询计数和:
EXPLAIN (ANALYZE, BUFFERS) ...
不要在繁忙生产库随意运行带 ANALYZE 的重查询。先用 EXPLAIN,再在隔离环境复现。
11.8 原生审计能力的边界
_log_access 主要保留:
create_uid/create_date- 最后一次
write_uid/write_date
它不是完整变更历史。res.users.log 还会清理为每用户最近登录记录,见 res_users.py:299-315。原生用户-组 M2M 也没有“谁因为什么授权、审批单、前后值、何时到期”的不可变事件链。
权限增强模块若要满足内部控制与追责,需要独立审计事件,并考虑失败操作随事务回滚的问题。拒绝事件可写结构化服务器日志或外部审计接收端,不能简单在同一失败事务里 create 一条日志后再抛异常。
老赵解读
权限排障最有效的方法是固定顺序,不凭感觉乱改规则:先复现身份和公司上下文,再查 ACL、字段、记录规则、业务方法,最后检查 sudo、控制器和缓存。测试也必须成对出现,既证明该放行的人能完成工作,也证明不该放行的人确实被拒绝;只有成功用例的权限测试几乎没有安全价值。
目前没有任何评论。