跳至内容

第7章 菜单、动作、视图与按钮权限

这一章的关键不是记住多少 XML 属性,而是判断每个机制到底在“执行前阻止”,还是只在“界面上不显示”。

7.1 菜单权限 ir.ui.menu

重要字段定义于 odoo/addons/base/models/ir_ui_menu.py:19-44

字段 作用
name 菜单名称
active 是否启用
sequence 排序
parent_id / parent_path 菜单树
groups_id 哪些组可看到入口
action 菜单打开的动作
web_icon 应用图标

菜单加载的大致链路:

/web/webclient/load_menus
  -> Web controller
      -> web 扩展模型 load_web_menus()
          -> base ir.ui.menu.load_menus()
          -> search()/search_fetch()
              -> _filter_visible_menus()
                  -> _visible_menu_ids()

_visible_menu_ids() 的逻辑在 ir_ui_menu.py:76-130

  1. 先 sudo 读取菜单元数据。
  2. 若菜单配置 groups_id,用户拥有其中任一组才保留。
  3. 非 debug 会话排除 base.group_no_one
  4. 对窗口、报表、服务器动作,检查动作目标模型的 read ACL。
  5. 保留可见叶子菜单及其祖先文件夹。

菜单过滤只检查模型 ACL,不检查记录规则是否最终能返回任何记录。菜单可见也不代表某个具体文档可读。

XML 示例:

<menuitem id="menu_demo_document"
          name="Documents"
          parent="menu_demo_root"
          action="action_demo_document"
          groups="demo.group_user,demo.group_manager"/>

groups="a,b" 是任一组即可。菜单转换器中的 -group_xmlid 语法表示从 Many2many 关系中解除这个组,不是通用的“属于该组就排除”条件,见 odoo/tools/convert.py:263-320。不要把视图节点的 !group 语义机械套到 <menuitem>

安全结论:菜单是导航。用户即使看不到菜单,也可能通过收藏 URL、消息链接、动作 ID、RPC 或自定义控制器到达同一功能;目标模型和方法仍必须自带授权。

7.2 动作的元数据配置权与业务使用权

普通用户通常没有通用 ORM 权限读取或维护完整动作、视图元数据。Web 控制器会通过专用入口 sudo 读取元数据,再返回白名单字段。例如 /web/action/load 位于 addons/web/controllers/action.py:22-51,窗口动作还通过 _get_action_dict() 清洗,见 ir_actions.py:218-252

所以:

没有 ir.actions.* 元数据 read ACL
    ≠
不能使用一个由 Web 专用入口安全加载的业务动作

反过来也一样:能加载动作元数据不等于能读动作指向的业务记录。

动作基类的重要字段在 odoo/addons/base/models/ir_actions.py:52-73

字段 作用
name / type 动作名称和类型
path Web URL 路径
binding_model_id 将动作绑定到哪个模型工具栏
binding_type Action 或 Report
binding_view_types 在 list/form 等哪些视图显示

binding_* 决定工具栏入口,不是执行授权。

7.3 窗口动作 ir.actions.act_window

重要字段见 ir_actions.py:302-327

字段 作用
res_model 目标模型
view_mode / view_id / views 视图选择
domain 默认筛选 domain
context 默认上下文
res_id 直接打开的记录
target 当前窗口、弹窗等
groups_id 绑定动作可见组
search_view_id 搜索视图

groups_id 会在模型工具栏的 get_bindings() 中过滤,源码 ir_actions.py:149-215。但是 /web/action/load 对窗口动作采用 sudo 加载,并不把该字段作为通用的直达执行授权;菜单可见算法也不统一检查窗口动作自己的 groups_id

domain 也不是安全规则。它只定义从这个入口打开时的默认过滤,用户可以直接 RPC 搜索同一模型并省略该 domain。最终仍由模型 ACL 和 read 记录规则约束。

正确定位:窗口动作定义“从哪个角度打开哪个工作台”,不定义“用户在数据库中到底能访问什么”。

7.4 服务器动作 ir.actions.server

服务器动作是普通动作中的重要例外。关键字段位于 ir_actions.py:503-588

  • model_id / model_name
  • state
  • code
  • child_ids
  • crud_model_id
  • groups_id
  • 更新字段等配置

code 字段本身受 base.group_system 字段组保护。run() 的执行链在 ir_actions.py:936-1011

  1. sudo 读取动作配置。
  2. 若配置 groups_id,显式检查当前用户是否在允许组。
  3. 若没配置组,要求目标模型 write ACL。
  4. 若有活动记录,对这些记录调用 check_access('write'),包括记录规则。
  5. 通过后才执行 runner。

因此服务器动作的 groups 不只是显示属性。但仍要注意:

  • 配了组时会走组分支,不能据此忽略动作内部代码审计。
  • 没有 active records 的代码型动作可能没有记录级检查对象。
  • 动作代码中的 sudo()、原始 SQL、跨模型写入仍可能扩张权限。

7.5 报表动作 ir.actions.report

重要字段位于 odoo/addons/base/models/ir_actions_report.py:141-176

字段 作用
model 文档模型
report_type QWeb HTML/PDF 等
report_name / report_file 模板和文件标识
groups_id Print 入口的可见组
multi 是否用于多记录
paperformat_id 纸张格式
print_report_name 动态文件名
attachment_use / attachment 附件缓存
domain 报表入口的适用性

groups_id 会过滤 Print 绑定入口,但标准报表路由 /report/<converter>/<reportname>/<docids> 只要求 auth='user',然后按报表名称 sudo 查元数据,相关源码为:

  • addons/web/controllers/report.py:23-50
  • ir_actions_report.py:623-657

标准模板中的 docs 通常以当前用户环境 browse,真正读取字段时会触发业务模型 ACL 和记录规则,见 ir_actions_report.py:1082-1097;但报表 groups_id 本身不是可靠的直达路由授权。自定义 report model、模板中的 sudo 和自定义控制器都需要单独审计。

PDF 缓存更需要警惕:准备 PDF stream 时先取得 sudo 的 report,再由它查找并读取已存在的附件,见 ir_actions_report.py:248-261,774-814。若目标 docid 的 PDF 已全部命中缓存,路径可能不再渲染 docs 来间接触发文档读取。因此授权检查必须发生在读取缓存之前。

敏感报表增强的建议入口逻辑:

report = request.env["ir.actions.report"]._get_report_from_name(report_name)
if not report:
    raise NotFound()
if report.groups_id and not report.groups_id & request.env.user.groups_id:
    raise AccessError("Not allowed to print this report")

records = request.env[report.model].browse(docids)
records.check_access("read")
# 通过检查后,才进入渲染或读取缓存附件。

实际实现还要处理空 docids、上下文报表、自定义 abstract report、token 和 HTTP 错误映射。

7.6 URL、Client 和嵌入式动作

  • ir.actions.act_urlir.actions.client 没有统一的通用业务授权语义,主要依赖菜单、按钮和实际目标服务。
  • Odoo 18 的 ir.embedded.actions.groups_ids/domain_compute_is_visible 中计算可见性,见 odoo/addons/base/models/ir_embedded_actions.py:8-29,71-91;最终业务调用仍要由模型和方法保护。

把所有动作总结成一张表:

动作类型 组字段主要作用 直达执行是否统一强制组 最终依赖
Window 绑定入口可见 目标模型 ACL/规则
Report Print 入口可见 文档访问、报表入口显式校验
Server 入口 + run() 检查 配置组时强制组;空组退回模型 write 活动记录 write、动作代码
URL/Client 外围入口 无统一语义 目标 URL/客户端调用的服务端授权
Embedded 可见性 后续动作与模型授权

7.7 视图记录 ir.ui.view

重要字段见 odoo/addons/base/models/ir_ui_view.py:143-206

字段 作用
model / type 目标模型与 form/list/search 等类型
priority 匹配和继承顺序
arch_db XML 架构
inherit_id 继承的父视图
mode primary 或 extension
groups_id 视图记录组条件,适用范围有限
active 是否启用

扩展视图记录禁止设置 groups_id,约束明确要求把 groups 放到 XML 节点,见 ir_ui_view.py:422-428

<record id="view_demo_form_manager_extension" model="ir.ui.view">
    <field name="inherit_id" ref="demo.view_demo_form"/>
    <field name="arch" type="xml">
        <xpath expr="//group[@name='details']" position="inside">
            <field name="manager_note" groups="demo.group_manager"/>
        </xpath>
    </field>
</record>

普通后台的视图组合会 sudo 选择和合并元数据,再针对当前用户后处理。当前源码中 ir.ui.view.groups_id_check_view_access() 主要出现在 public asset 和 website restricted visibility 路径,并不是普通后台的通用数据安全边界。后台差异优先用架构节点 groups,数据安全仍由 ORM 保证。

7.8 视图节点 groups

最终架构后处理 _postprocess_access_rights() 位于 ir_ui_view.py:989-1060

  1. 继承架构合并完成后,_postprocess_view() 把节点组条件编码为 __groups_key__
  2. 缓存保留包含所有组分支的后处理架构。
  3. 返回给具体用户前,根据有效组删除不匹配节点。
  4. 根据模型 ACL 给适用的视图根节点及嵌套关系视图补 create=Falseedit=Falsedelete=False
  5. 给关系字段补 can_createcan_write 提示。

组表达式编码见 ir_ui_view.py:1062-1131,全组版本缓存见 ir_ui_view.py:2706-2711,按用户删除节点见 ir_ui_view.py:989-1015

节点语法:

<!-- 任一正组满足,并且不属于任何负组 -->
<button name="action_sensitive"
        type="object"
        groups="demo.group_manager,!demo.group_auditor"/>

base.group_no_one 只有开发者模式会话才有效,不是业务权限。视图过滤见 ir_ui_view.py:1000-1015,用户组判断见 res_users.py:1142-1190

根节点自动关闭 create/edit/delete 是客户端能力提示。如果手工写 create="1",也不能给没有 create ACL 的用户授予权限;反过来 create="0" 只隐藏这个视图的创建入口,不能阻止直接 RPC create。

7.9 按钮权限

type="object" 按钮的大致调用链:

用户点击按钮
  -> Web client POST /web/dataset/call_button
      -> controller 检查方法名
          -> call_kw(model, method, args, kwargs)
              -> 调用公开 Python 方法

关键源码:

  • 控制器:addons/web/controllers/dataset.py:32-43
  • 方法名检查:odoo/models.py:151-158
  • call_kwodoo/api.py:496-524

方法名检查只禁止下划线开头的方法和 init。它不会回头读取按钮 XML,也不会检查那个节点上的 groups、invisible 或 confirm。因此知道模型名和公开方法名的用户可以直接发 RPC。

type="action" 按钮只是加载动作 ID,窗口动作或报表动作的显示组同样不能代替目标模型授权。

不安全示例:

def action_approve(self):
    self.sudo().write({"state": "approved"})

它既没有检查组、状态、记录范围,也用 sudo 抹掉了调用者边界。

较完整的公开方法模板:

def action_approve(self):
    self.ensure_one()
    self.check_access("write")

    if not self.env.user.has_group("demo.group_approver"):
        raise AccessError(_("Only approvers can approve."))
    if self.company_id not in self.env.companies:
        raise AccessError(_("The company is not enabled."))
    if self.state != "submitted":
        raise UserError(_("Only submitted records can be approved."))
    if self.owner_id == self.env.user:
        raise AccessError(_("You cannot approve your own document."))

    return self._set_state("approved")

批量方法不要盲目 ensure_one();应明确是整批原子失败、过滤允许子集,还是逐条报告。安全行为必须成为方法契约,而不是偶然结果。

7.10 QWeb、前端组判断和 Controller

服务端 QWeb 节点的 groups/t-groups 会编译为 env.user.has_groups(...) 条件,见 odoo/addons/base/models/ir_qweb.py:1847-1864。它能避免渲染某段 HTML,但不能阻止另一 API 读取同一数据。

顶层 <template groups="..."> 是另一种机制:XML 导入时会把它转换为 ir.ui.view.groups_id,见 odoo/tools/convert.py:450-514,尤其是 :496-500。它不会作为普通 QWeb 节点条件原样留在 arch 中;其记录级适用边界受前述 _check_view_access() 调用路径限制,不能与节点 t-groups 混为一谈。

前端 user.hasGroup() 会 RPC 到 res.users.has_group 并缓存结果,源码 addons/web/static/src/core/user.js:51-78,106-110。这只是前端决策。

Controller 的 auth 也不是业务权限:

auth 含义
user 必须登录,以当前用户环境执行
public 可匿名;未登录时使用 Public user
none 可在没有数据库的阶段调用

@route(auth='user') 不支持自动声明 Odoo 组,也不会自动检查 docid。Controller 必须显式做组、公司、记录或 token 校验。

Portal/token 的正确顺序是:先尝试当前用户的非 sudo 记录访问;若失败,再恒时比较有效 token;只有授权通过后,才以必要范围 sudo 返回记录。参考 addons/portal/controllers/portal.py:373-392

CSRF 防止跨站请求伪造,不是对象权限。HTTP 路由的 CSRF 默认开启、JSON 路由默认关闭,仍然必须做业务授权。

路由的 readonly=True 只是选择只读事务/副本,不是授权声明,也不会自动检查记录。

7.11 界面层安全复核清单

看到任何 groups=invisible= 时,继续问:

  1. 用户能否直接调用公开方法?
  2. 动作 ID 能否被直接加载?
  3. 报表 URL 能否直达?
  4. Controller 是否只验证登录,没有验证 docid?
  5. 目标模型是否有 ACL 和记录规则?
  6. 关联行、附件、分析模型是否也有规则?
  7. 方法内部是否过早 sudo?

老赵解读

菜单、动作、视图和按钮首先服务于“让谁看到什么工作台”,它们很重要,但不能成为唯一防线。评审任何按钮时都要追问:方法能否通过 RPC 直接调用?报表 URL 能否绕过菜单直达?控制器是否重新检查目标记录?只要答案涉及“可以直接访问”,安全检查就必须在服务端落地。

额定值
0 0

目前没有任何评论。

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