权限策略¶
v1.6.0 新增
- 已知限制 10 已解决,只剩一种情况。 在 Odoo 10 至 12 上,受守卫的会话会用模型的字段来判定按位置传递给业务方法的对象中的键:既不是模型字段、也不是允许的上下文键的键会被拒绝(规则
context);无法读取字段时,调用会被拒绝(规则unverified)。剩下的情况:与模型某个字段同名的上下文键会被当作值来读取。参见 已知限制。 - 新的摘要:
readonly、standard和builder会多拒绝一些调用,参与计算摘要的层级数据版本也变了,因此每个配置档的摘要都会改变(没有策略文件时standard为21524d1933f3)。full的行为不变。仍然是 28 个工具。 odoo_version会点名作出响应的 odoo-tools 副本(plugin块),策略被锁定时也是如此。没有policy的响应来自早于 1.5.0 的副本,它没有权限策略。参见 工具报告的内容。
v1.5.1 新增
- 在 Odoo 10 至 12 上按位置传递的上下文: 这些版本系列还会把紧跟在方法参数之后的那个位置参数当作调用的上下文。上下文规则 现在也适用于这个参数:在 A 层对所有版本系列适用,在 10 至 12 上还会在受守卫的会话中再检查一次。当时仍未解决的部分是 已知限制 10,v1.6.0 已将其解决,只剩一种情况。
- 新的摘要:
readonly、standard和builder会多拒绝一些调用,参与计算摘要的层级数据版本也变了,因此每个配置档的摘要都会改变(没有策略文件时standard为9f3166860347)。full的行为不变。仍然是 28 个工具。 - 会话开始的 hook 不再读取策略选项:它根据服务器的本地诊断,在上一次服务器启动被锁定时连同 E 代码提示你。
v1.5.0 新增
- 权限策略: MCP 服务器在 Odoo 用户自身的访问权限之上应用一个权限配置档(profile)。默认为
standard:可以读取、创建、更新并运行业务方法,但不删除、不做 Studio 风格的改动、不发送邮件或短信(内部备注可以)、不更改用户、权限、模块、设置、银行账户、财务序列或 chatter 历史,也不读取已保存的密钥。 - 两个新选项: Permission profile(
odoo_policy)和 Permission policy file(odoo_policy_file),两者都有默认值。 - 审计日志: 每次工具调用(包括被拒绝的调用)写一行 JSON,不含任何值或密钥。
- 要保留 1.4 的行为,请选择配置档
full。
odoo-tools 的 MCP 服务器在 Odoo 用户自身的访问权限之上应用一套权限策略。它是一个上限:它从不授予 Odoo 拒绝的权限,只会收回权限,并且从不弱于 Read-only mode。它在 Community、Enterprise 和 Odoo Online 上的表现相同,因为它运行在 MCP 服务器中,不需要任何 Odoo 模块。Odoo 的访问规则、记录规则和字段组始终照常生效。
权威参考是仓库中的
docs/policy.md;本页对其进行概述和解释。
两个选项¶
| 插件选项 | 环境变量 | 默认值 | 含义 |
|---|---|---|---|
Permission profile(odoo_policy) |
ODOO_POLICY |
standard |
readonly、standard、builder 或 full |
Permission policy file(odoo_policy_file) |
ODOO_POLICY_FILE |
空 | 可选的绝对路径,指向一个 JSON 文件,在配置档之上按模型添加规则 |
在 /plugin → odoo-tools → Configure options 中修改它们。在容器或独立(standalone)服务器中,使用环境变量。
要保留 1.4 的行为,请选择 full
默认配置档在这个次版本中收紧了。如果你希望智能体保留 Odoo 用户拥有的全部权限(例如在你自己公司的实例上),请把 Permission profile 设为 full(在容器或独立运行中为 ODOO_POLICY=full)。full 仍然拒绝简短的 NEVER 列表(更改 API keys 和卸载模块),并保留执行该列表所依赖的两项参数检查(参见 配置档)。
策略只在服务器启动时读取一次。修改后需要重新连接服务器(在 Claude Code 中运行 /mcp);之后新的摘要(digest)会出现在 odoo_version 和审计日志的启动行中。
它保护什么,不保护什么¶
在本地(stdio)模式下,权限策略限制的是 Claude 通过 odoo-tools 的 MCP 工具 能做什么。它防范的是错误,以及隐藏在 Claude 所读数据中的指令。它不限制持有 API key 的人,也不是一个沙箱,无法约束在同一台机器上拥有不受限 shell 访问权限的智能体。对所有人都有效的边界是 Odoo 用户本身:为智能体提供一个专用的、最小权限的 Odoo 用户(不是管理员),以及一个带过期时间的 API key。在未来的远程模式中,服务器会保管 Odoo 凭据,届时同一套策略将成为真正的权限强制。
它也不在记录级别工作(记录规则属于 Odoo),也不是业务方法在 Odoo 内部所做之事的沙箱(参见 已知限制)。当 Odoo 用户是管理员(base.group_system)时,odoo_version 会发出警告(W10):此时 Odoo 的访问权限不做任何限制,本策略是唯一起限制作用的一层。
操作¶
| 操作 | 含义 | 来源 |
|---|---|---|
read |
记录数据:搜索、读取、计数、分组、导出、翻译、一条记录的附件 | 读取工具;通过 odoo_execute 调用的读取类方法 |
create |
创建记录,包括 copy、name_create 以及插入行的导入 |
odoo_create、导入工具、创建类方法 |
write |
修改已有记录,包括归档、翻译以及更新行的导入 | odoo_write、odoo_translate_set、导入工具、写入类方法 |
unlink |
删除记录,包括删除明细行的 x2many 命令 | odoo_unlink、通过 odoo_execute 调用的 unlink、x2many 命令 |
call |
通过 odoo_execute 运行模型方法,可限定为一个方法列表 |
仅 odoo_execute |
schema |
对模型做 Studio 风格的改动(新的 x_ 字段、模型上的自动化) |
odoo_add_field、odoo_add_automation |
inspect |
一个模型的字段元数据和访问检查 | odoo_fields_get、odoo_api_catalog、odoo_access_check,以及各工具自身的 fields_get 调用 |
inspect从不由规则授予:只要某个模型上允许任何其他操作,该模型上就允许inspect。所有操作都被拒绝的模型(secrets层级)也会隐藏它的字段列表。- 导出属于
read:一次导出返回的内容与odoo_search_read相同。 - 导入同时需要
create和write,因为 Odoo 的load()既插入也更新。
配置档¶
| 按模型类别划分的操作 | readonly |
standard(默认) |
builder |
full |
|---|---|---|---|---|
| read,业务模型 | 是 | 是 | 是 | 是 |
| create、write,业务模型 | 否 | 是 | 是 | 是 |
| unlink,业务模型 | 否 | 否 | 否 | 是 |
| call,业务方法 | 否 | 是 | 是 | 是 |
| 外发方法的 call | 否 | 仅内部备注 | 仅内部备注 | 是 |
| schema,业务模型 | 否 | 否 | 是 | 是 |
读取 security、outbound 和 customization 模型 |
是 | 是 | 是 | 是 |
对 security 和 outbound 模型的任何更改 |
否 | 否 | 否 | 是 |
对 customization 模型的任何更改 |
否 | 否 | 是 | 是 |
对 secrets 模型的任何操作 |
否 | 否 | 否 | 是 |
| 类凭据字段 | 屏蔽 | 屏蔽 | 屏蔽(自动化的代码字段除外) | 返回 |
| 附件、身份、委托链接和上下文规则 | 开启 | 开启 | 开启 | 关闭 |
| NEVER、名称检查和守卫(guard)的边界检查 | 开启 | 开启 | 开启 | 开启 |
参数形态和关联模型查询(规则 shape 和 unverified) |
开启 | 开启 | 开启 | 开启 |
需要牢记两点:
- 在机密性和完整性方面,
builder等同于full。 它允许智能体创建服务器动作和自动化:在 Odoo 服务器上运行的 Python,可以使用sudo(),之后还可能以触发它的管理员身份运行。这样的代码可以读取一个系统参数,并把它复制到一个可读的字段中。builder之下的层级能阻止通过通用工具犯下的错误,但阻止不了编写自动化代码的智能体。请只在沙箱和预发布(staging)环境中使用builder。加载它会产生警告 W6。 full就是 1.4 的行为,但 NEVER、名称检查、守卫的边界检查(它只拦截 odoo-tools 自身的缺陷)以及 NEVER 和策略文件所依赖的两项参数检查除外。因此,少数 1.4 能发送的调用需要换一种写法:- 策略无法确定读懂的参数会被拒绝(规则
shape),例如通过odoo_execute向read_group传入三个以上的位置参数(后面几个位置在不同的 Odoo 系列之间不同),或在读取或写入方法上使用它不认识的关键字(业务方法的参数不会被解析)。请按名称传参,或使用专门的工具; - 带点的路径背后的字段定义,以及可能是 x2many 命令的值(写入中的列表、
false或null)背后的字段定义,会先用fields_get读取:res.users上的api_key_ids命令会到达res.users.apikeys,而 NEVER 关闭了该模型。如果 Odoo 拒绝这次查询,调用就会被拒绝(规则unverified)。普通字段名或普通值不需要查询。
- 策略无法确定读懂的参数会被拒绝(规则
standard 拒绝 unlink,但允许 call:删除是唯一不可逆的通用动词(Odoo 没有回收站,归档才是拒绝信息所建议的可逆途径),而拒绝 call 会破坏 action_confirm、action_post 和各种向导流程。因此关闭的是外发方法。对于使用管理员用户的生产环境,加固示例 把 call 限定为方法列表。
每个配置档列出的工具¶
tools/list 会隐藏策略在所有模型上都禁止的工具,就像 Read-only mode 隐藏写入工具一样。隐藏只是宽松的近似:一个被列出的工具仍可能在某个具体模型上被拒绝;而按名称调用一个被隐藏的工具,会返回一个说明原因的错误(绝不会是“未知工具”)。Read-only mode(odoo_readonly、ODOO_READONLY=1)在所有配置档中都会隐藏 9 个写入工具。
| 配置档 | 不列出的工具 | 列出数量 |
|---|---|---|
readonly |
odoo_create、odoo_write、odoo_unlink、odoo_execute、odoo_translate_set、odoo_import_preview、odoo_import、odoo_add_field、odoo_add_automation |
19 |
standard |
odoo_unlink、odoo_add_field、odoo_add_automation |
25 |
builder |
无 | 28 |
full |
无 | 28 |
| 任意配置档,且 Read-only mode 开启 | odoo_create、odoo_write、odoo_unlink、odoo_execute、odoo_translate_set、odoo_import_preview、odoo_import、odoo_add_field、odoo_add_automation |
19 |
策略文件还可以隐藏更多:列在 tools.disabled 中的工具不再被列出。
服务器指令会多出一行,写明配置档、它的摘要和被隐藏的工具,并说明点名权限策略的拒绝是最终结果。在 full 之外,每个接收模型参数的工具,其描述末尾都会附上一段简短的配置档提示,例如
[Policy 'standard': no deletes or secrets; users, rights, mail-out and customization read-only. See odoo_version.]。
敏感层级¶
默认关闭四个层级(tiers)。一个模型通过精确名称或 前缀.* 模式(匹配前缀之下的名称,而不包括前缀本身)、通过它的别名以及通过它的委托父模型归入某个层级。当一个模型的任何层级扣留某项操作时,该操作即被拒绝,拒绝信息会按下表顺序点名第一个层级。在某个版本中不存在的名称从不匹配;标注为 best-effort 的名称会在真实数据库上确认。各列说明每个配置档在该层级内允许什么;full 允许一切。
| 层级 | 模型 | readonly |
standard |
builder |
原因 |
|---|---|---|---|---|---|
secrets |
ir.config_parameter、res.config、res.config.*、ir.mail_server、fetchmail.server、payment.provider、payment.acquirer、payment.token、iap.account、res.users.apikeys.show、res.company.ldap、auth_totp.device(best-effort)、l10n_mx_edi.certificate(best-effort)、certificate.certificate(best-effort)、certificate.key(best-effort)、account.online.link(best-effort)、account.online.account(best-effort)、mail.mail |
无 | 无 | 无 | 它们保存凭据,或起凭据作用的值(系统参数、邮件服务器和 LDAP 的密码、支付服务商的密钥和令牌(token)、IAP 令牌、新建的 API key、银行同步令牌、CFDI 证书和私钥、排队中的重置密码邮件)。读取它们本身就已造成损害。 |
security |
res.users、res.users.*、res.groups、res.groups.*、ir.model.access、ir.rule、ir.module.module、base.module.*、change.password.*、portal.wizard、portal.wizard.*、auth_totp.wizard、auth.oauth.provider、res.partner.bank、base.partner.merge.automatic.wizard、mail.message、mail.tracking.value、ir.sequence、ir.sequence.date_range |
读取 | 读取 | 读取 | 身份与权限、登录重定向、银行账户、联系人合并、chatter 历史和财务序列:读取保持开放;更改需要配置档 full,或一个精确点名该模型的条目。 |
outbound |
mail.compose.message、mail.followers、mail.template、sms.sms、sms.composer、sms.template、portal.share、mailing.*(best-effort)、snailmail.letter、account.move.send、account.move.send.*(best-effort)、account.invoice.send、mail.mail |
读取 | 读取 | 读取 | 它们发送邮件、短信、信件或分享链接,或决定发送什么。 |
customization |
ir.*、base.automation、base.action.rule、studio.*、base_import.import、base.language.install、base.language.import,ir.attachment 除外 |
读取 | 读取 | 全部 | 服务器动作、自动化、计划任务、视图、assets、手动字段、默认值以及其他 ir.* 记录会运行代码或改变 Odoo 之后的行为,批量导入引擎和语言引擎会跨模型插入、更新或安装;它们需要配置档 builder。 |
为什么关闭 customization 模型:
ir.actions.server、ir.cron和base.automation会在之后的触发器上运行 Python,以其他用户的身份,并可以使用sudo();ir.server.object.lines保存 Python 表达式。ir.ui.view的 QWeb 会被编译成代码(在公开网站上保存一段脚本只差一次写入);ir.asset会注入 JavaScript 和 CSS。- 手动的
ir.model.fields可以携带compute代码;ir.model.data会改指外部标识符;ir.default可以在任何模型上设置默认值;ir.property(至 17 为止)保存按公司区分的值,例如默认科目。 ir.filters、ir.embedded.actions、ir.ui.menu、ir.actions.*(URL 重定向、打印报表)和ir.translation(至 15 为止)。作为前缀,ir.*也覆盖了没有人列出的模型。base_import.import(批量导入引擎)和语言向导属于这个层级,只能通过它们自己的工具访问,因此odoo_execute无法把导入器的load()驱动到一个已关闭的模型上。
这些重叠是有意的:ir.config_parameter 和 ir.mail_server 同时属于 ir.* 和 secrets,因此 builder 仍让它们保持关闭;ir.model.access、ir.rule、ir.module.module 和 ir.sequence 同时属于 ir.* 和 security,因此 builder 无法更改权限、安装模块或为发票重新编号。
v1.5.0 中的行为:account.journal
在 v1.5.0 中,account.journal(及其银行账户和序列)是一个业务模型:它的写入不属于任何层级。这些写入是否会加入某个层级,将在用 v1.5.0 对 CFDI 开票做实时验证之后决定。
NEVER 列表¶
任何配置档和任何规则都不允许以下操作,full 也不例外。加载器会拒绝试图允许它们的文件(E18)。
| 模型 | 操作 | 原因 |
|---|---|---|
res.users.apikeys |
create、write、unlink、call | odoo-tools 从不创建、更改或撤销 API keys |
res.users.apikeys.description |
read、create、write、unlink、call、schema | 创建 API key 的向导 |
ir.module.module |
call(button_immediate_uninstall、button_uninstall、module_uninstall) |
卸载模块会删除它的数据 |
base.module.uninstall |
read、create、write、unlink、call、schema | 卸载向导 |
私有方法不需要条目:方法名检查(^[A-Za-z][A-Za-z0-9_]{0,127}$)在所有传输方式上都会拒绝以 _ 开头的名称,与 Odoo 一致。模型名必须符合 ^[a-z0-9_.]{1,128}$。
内置规则¶
配置档 full 会关闭下面的委托链接、ir.attachment、身份、外发方法和上下文规则。有两部分在所有配置档中都保持开启,因为 NEVER 和策略文件的规则在 full 下同样适用:委托子模型仍然采用其父模型的决定,关联记录也仍然会被检查。
委托(_inherits)¶
委托给一个已关闭父模型的子模型,会写入父模型的字段:website.page 委托给 ir.ui.view,因此写入它的 arch 就是写入一个 QWeb 视图。所以,除 read 之外的每项操作,子模型都继承其父模型的层级:写入 website.page 需要 builder,而读取保持开放。这些子模型是 website.page 和 website.controller.page(ir.ui.view)、11-16 上的 ir.cron 和 base.automation(ir.actions.server),以及 product.document 和 documents.document(ir.attachment)。
v1.5.0 中的行为:父模型的决定包括文件中的规则
父模型的完整决定也适用于子模型,包括文件中的规则:针对 website.page(或 ir.cron)的一条精确规则本身并不会开放它的写入。请同时开放父模型(ir.ui.view 或 ir.actions.server),或使用配置档 builder。
子模型的链接字段(view_id、ir_attachment_id 等)不能通过通用工具设置(规则 delegation-link):把它指向一条已有记录,就会通过子模型暴露或更改那条记录。
ir.attachment 固定到其父记录¶
ir.attachment 保存其他模型的二进制字段,包括证书私钥,因此对它的通用读取可能从后门到达 secrets 层级。对 ir.attachment 的通用调用必须点名它的父模型:
- 一次搜索(所有接收顶层域的模型读取方法:
search、search_read、search_count、read_group、formatted_read_group、web_search_read、web_read_group、name_search、web_name_search、read_grouping_sets、formatted_read_grouping_sets、search_fetch,以及透视表和面板读取器read_progress_bar、search_panel_select_range、search_panel_select_multi_range)需要一个顶层的["res_model", "=", X]或["res_model", "in", [...]],其中的模型都必须允许read,并且不能有针对res_field的叶子条件。守卫会追加["res_field", "=", False],因此搜索永远看不到保存在字段中的文件。search_panel_*读取器不带顶层域参数,所以在ir.attachment上它永远无法点名父模型,会被拒绝。 - 按 id 的调用 会先读取这些 id 的
res_model和res_field(一次智能体收不到的内部读取)。每个 id 都不能有res_field,每个res_model都必须允许read(读取时)或write(更改时)。 - 一次创建(或写入这些值)不能是
public,也不能设置res_field;它的res_model必须允许write。当写入通过委托子模型到达ir.attachment时同样如此:product.document和documents.document暴露了父模型的public、res_field和datas,因此通过它们创建或切换出一个任何人都能下载的公开附件会被拒绝,与直接在ir.attachment上操作完全一样。
字段附件仍可通过其父模型读取(在 product.template 上用 odoo_read 读取 image_1920),odoo_record_documents 也通过它自己的固定内部调用继续工作。
身份(用户的联系邮箱)¶
用户的重置密码链接会发送到该用户联系人(partner)的邮箱,因此通过 res.partner 更改这个邮箱就等于接管账户。对 res.partner 设置 email 的 write,会让守卫先读取这些联系人的 user_ids;如果其中有用户(包括门户用户),该调用还需要对 res.users 的 write,而 security 层级扣留了它。
v1.5.0 中的行为:更新联系人的导入
向 res.partner 的导入如果带有 email 列以及 id 或 .id 列(即更新已有联系人),会被拒绝,拒绝信息会指向 odoo_write,后者会逐个检查联系人。
外发方法¶
业务方法可以把数据发送出去:带有智能体所选联系人的 message_post 可以给任何人发邮件,message_subscribe 会转发之后的每一条消息。这些方法作为 call 会被拒绝,除非一个精确点名该模型的条目列出了该方法,并附有理由:
action_send_and_print、action_send_mail、message_mail_with_source、message_notify、message_post、message_post_with_source、message_post_with_template、message_post_with_view、message_subscribe、send_and_print_action、send_mail
内部备注是允许的。 当 message_post 的关键字参数带有 subtype_xmlid "mail.mt_note"(13+)或 subtype "mail.mt_note"(10-12)、没有 partner_ids、message_type 缺省或为 "comment",并且没有其他位置参数时,它作为普通 call 通过。内部备注不会通过邮件通知任何人。
上下文键¶
一次调用只能携带以下上下文键,以及在未被屏蔽的字段上带普通值(文本、数字、布尔值或 null)的 default_<字段>:
active_id、active_ids、active_model、active_test、allowed_company_ids、bin_size、button_validate_picking_ids、lang、location、mail_create_nosubscribe、mo_ids_to_backorder、picking_ids_not_to_backorder、read_group_expand、skip_backorder、skip_immediate、tz、uid、validate_analytic、warehouse
库存和制造的验证向导需要其中一些键:例如没有 button_validate_picking_ids 或 skip_backorder 时,向导会返回 True 而不验证任何东西。发票的确认/过账按钮会发送 validate_analytic(16+),account.move._post 读取它来强制校验分析分配;Odoo 网页客户端会在每次调用中把已登录用户的 id 放进 uid,它只是信息性的,因为执行操作的用户是已认证的会话,而不是 context['uid']。允许的键中没有任何一个会关闭 chatter 的跟踪记录。tracking_disable、mail_notrack 和 mail_create_nolog 是有意缺席的:它们会让 security 层级作为证据保护的 chatter 跟踪记录沉默。
v1.5.0 中的行为:上下文中的 default_<字段> 必须是普通值
在 readonly、standard 和 builder 下,值为列表或对象的 default_<字段> 会被拒绝(规则 context),在向 Odoo 发送任何内容之前,对所有工具、方法和模型都是如此,无论上下文位于何处(kwargs["context"]、嵌套在读取规格中的上下文,或嵌套在 onchange 的 fields_spec 中的上下文):x2many 命令([(0, 0, {...})]、[(4, id)]、[(6, 0, ids)])、id 列表、嵌套值,甚至空列表。Odoo 会把上下文默认值应用到调用运行期间创建的每一条记录上:调用点名的记录、它的 x2many 命令创建的明细行,以及业务方法、向导或导入所创建的其他模型的记录(action_confirm、验证向导、load)。这样的值无法对照调用所点名的那一个模型进行检查。拒绝信息会点名该键,从不显示它的值。请把这些值放在记录的 values 中传入,在那里每条命令都会针对它所到达的模型做出决定;或者使用配置档 full。例如,在 values 中带上明细行来创建制造欠单(backorder)向导:
{"model": "mrp.production.backorder", "method": "create",
"args": [{"mrp_production_ids": [[6, 0, [5]]],
"mrp_production_backorder_line_ids": [[0, 0, {"mrp_production_id": 5,
"to_backorder": true}]]}]}
而不是使用其动作的 default_mrp_production_ids 和 default_mrp_production_backorder_line_ids 键;库存欠单向导则在 values 中使用 pick_ids: [(4, id)],而不是 default_pick_ids。
此外,在可能创建记录的调用上(读取调用不会,因为默认值在读取中不填充任何内容),点名一个结构性字段的普通 default_<字段> 会被按构造规则拒绝:父模型敏感的委托链接字段(default_ir_attachment_id、default_view_id,以及其他指向 ir.attachment、某个层级或 NEVER 模型的链接),以及 default_public 或 default_res_field。无害的默认值(default_move_type: "out_invoice")照常运行,除非它的字段被屏蔽。default_res_model 和 default_res_id 仍然允许,因为活动安排和邮件编辑器会传入它们(参见已知限制 9)。full 不变。
自 v1.5.1 起,这些规则也适用于按位置传递的对象,Odoo 10 至 12 会把它当作上下文;自 v1.6.0 起,受守卫的会话还会用模型的字段来判定这类对象的其他键:参见 已知限制 10。
关联记录:x2many 命令与字段路径¶
一次写入会针对它更改的每一条记录进行检查,而不仅仅是它点名的模型。在 values 中(create、write、copy 的默认值、web_save):
| 命令 | one2many | many2many |
|---|---|---|
(0, _, vals) |
在关联模型上 create,然后检查其 vals |
相同 |
(1, id, vals) |
在关联模型上 write,然后检查其 vals |
相同 |
(2, id) |
在关联模型上 unlink | 相同 |
(3, id)、(5,)、(6, _, ids)、id 列表、False |
在 write 中:在关联模型上 unlink(当反向字段为 ondelete='cascade' 时,Odoo 会删除被移除的明细行);在 create 中:在关联模型上 write |
当某个层级、文件规则或 NEVER 在关联模型上拒绝 write 时,视为在关联模型上 write;否则视为 read |
(4, id) |
在关联模型上 write(它会更换明细行的父记录) | 同上 |
| 其他任何形式、深度超过 4,或一个 Odoo 不报告却携带命令的字段 | 拒绝(shape) |
相同 |
因此,在 res.partner 上,user_ids: [(1, uid, {"groups_id": ...})] 是对 res.users 和 res.groups 的写入,bank_ids 的更改是对 res.partner.bank 的写入,而 order_line: [(5,)] 在 standard 下是一次删除。
v1.5.0 中的行为:指向用户的 many2many 链接
指向一个写入被扣留的模型的 many2many 链接,会被算作对该模型的更改,因此在 standard 下会被拒绝,例如用户的组(res.users.groups_id)或公司的用户(res.company.user_ids)。有两个只引用用户的链接被算作对 res.users 的读取,可以在 standard 下运行:任务的指派人(project.task.user_ids)和 helpdesk 团队的成员(helpdesk.team.member_ids)。其他任何指向 res.users 的 many2many 仍然算作写入。
读取会沿着它们的路径进行检查:域叶子条件的每一跳(包括 any 和 not any)、web_read 和 web_search_read 的嵌套 specification、export_data 的路径、导入列,以及 groupby 和 order 中带点的项。到达的每个模型都必须允许 read,并且任何一段都不能是被屏蔽的字段。在导入列中,指向 one2many 的路径(order_line/product_id)会在关联模型上创建并写入明细行。当无法读取这些检查所需的字段元数据时,调用会被拒绝(规则 unverified),在所有配置档中都是如此。
类凭据字段¶
在 full 之外,名称匹配
(^|_)(password|passwd|pass|pwd|secret|token|apikey|api_key|private_key|privkey|credentials?)($|_),
或名称为 signup_url、smtp_pass 或 totp_secret 的字段会被屏蔽。表示令牌关系或令牌计数、而非令牌值的名称不会被屏蔽:精确名称 payment_token_id、payment_token_ids 和 token_count,以及任何以 token_id、token_ids 或 token_count 结尾的名称(因此 token_id、suitable_payment_token_ids 和 payment_token_count 也不会)。它们只引用 secrets 层级的 payment.token,而后者自己的值仍然是封闭的。ir.actions.server、ir.cron 和 base.automation 的 code、服务器动作的 webhook_url 以及自动化的 webhook_uuid 也会被屏蔽,builder 下除外(集成常常把令牌硬编码在那里)。
被屏蔽的字段无论出现在哪里都会被拒绝:字段列表、翻译工具的 field、values 的键(包括嵌套命令中的值)、导入列和导出路径(每一段)、任意深度的 specification 键、groupby、聚合的源字段、order,以及域路径的每一段。对密钥的过滤会让人一次猜一点地把它泄露出来。读取结果会在任意深度上丢掉被屏蔽的键,工具结果会在 policy_withheld_fields 中点名它们(例如 ["res.company.l10n_mx_edi_pac_password"]),因此不会有任何东西悄无声息地消失。
每个读取或过滤记录数据的公开方法都要经过这些检查,而不仅仅是 search/read:search_fetch、fetch、onchange、read_progress_bar、search_panel_select_range、search_panel_select_multi_range、copy_data、网页客户端的 web_name_search、透视表读取器 read_grouping_sets 和 formatted_read_grouping_sets,以及 filtered_domain 都是读取,因此它们的域、分组集合、字段和规格都会像其他读取一样被解析和拒绝。read_progress_bar 按 progress_bar["field"] 分桶(并对 sum_field 求和),这是第二个分组维度,会像它的 group_by 一样被检查;它的结果不做隐去处理,因为其中的键是这些(已经检查过的)分组字段的不同取值,而不是字段名。
仍然作为普通 call 的方法(例如 action_confirm 这样的业务动词,或 odoo-tools 任何版本都未列出的方法)有两道兜底。它的结果会在任意深度上被隐去,与读取结果相同。它的参数在发送前会用模型的字段元数据进行扫描:一个作为该模型字段路径的字符串,会被当作对该路径的读取来检查(例如,指向支付服务商的 mapped("provider_id.name") 或 signup_token:count 会被拒绝);键为字段的对象会被当作写入值来检查(onchange 除外,它是一个不写入的模拟);形似域的参数会经过读取路径分析。不是该模型字段的字符串(消息正文、主题、附件文件名、状态词)会被放行,因此内部备注和带普通文字参数的业务动词仍然可以运行。
策略文件可以修改这个列表:fields.withhold 添加名称(模型.字段 或 *.字段,用于数据最小化),fields.allow 释放精确的 模型.字段 名称,每个都需要附上理由。withhold 优先于 allow,allow 优先于内置列表。
优先级¶
decide(op, model, method) 按顺序运行以下步骤,第一个做出决定的步骤即为最终决定:
invalid-name:模型或方法没有通过名称检查。never:<id>:匹配到一个 NEVER 条目。readonly:Read-only mode 开启,且操作不是read或inspect。locked:策略无法加载。tool-disabled:工具位于tools.disabled中(按工具检查)。file:<模型>:一个设置了该操作的、精确且未过期的条目。对于call,false表示拒绝,列表按成员关系决定(并为它列出的方法开放层级和外发方法),true为call开放该模型的层级,但不开放外发方法,外发方法仍然需要列表。tier:<名称>:在该配置档下,模型的某个层级(它自己的、某个别名的,或某个委托父模型的)扣留了该操作。outbound-method:对外发方法的call,且不是内部备注。file:<模式>:设置了该操作的最具体的模式(先是最长的前缀.*,然后是*)。profile:<名称>:配置档的默认值。
第 7 步和第 8 步从不考虑模式所允许的内容,因此模式永远无法开放一个层级或一个外发方法:只有精确点名该模型并附有理由的条目才能做到。添加一个 false 永远不会把拒绝变成允许。
别名。 针对一对被重命名或合并的名称中任一名称的规则,在所有版本上都同时适用于两者:account.invoice 和 account.move、它们的 .line 模型、stock.quant.package 和 stock.package、base.automation 和 base.action.rule,以及 兼容性 映射表中的其他重命名。对其中任一名称的拒绝都会胜出。在 Odoo 10 至 12 上,针对 account.move(日记账分录)的规则因此也会限制 account.invoice,反之亦然。
策略文件¶
{
"version": 1,
"comment": "Acme production: confirm and post only, no imports",
"models": {
"*": {"call": false},
"sale.order": {"call": ["action_confirm", "action_cancel"]},
"account.move": {"call": ["action_post", "message_post"],
"reason": "Invoices are sent from the chatter by the billing agent"},
"res.partner.bank": {"create": true, "write": true,
"reason": "Ticket 4312: supplier onboarding", "expires": "2026-11-15"},
"hr.*": {"read": false, "create": false, "write": false, "call": false}
},
"fields": {
"withhold": ["hr.employee.private_email", "*.vat"],
"allow": [{"field": "sale.order.access_token", "reason": "Portal links for the sales agent"}]
},
"tools": {"disabled": ["odoo_import", "odoo_import_preview"]}
}
该文件在选项所选的配置档之上叠加规则,本身不携带配置档。模式(schema)是严格的:任何未知的键都是错误,因此一个拼写错误不会悄悄丢掉一条拒绝规则。仓库中有一份供编辑器使用的模式:
docs/policy.schema.json
(服务器在代码中进行校验)。
| 键 | 规则 |
|---|---|
| 顶层 | version(必需,1)、models、fields、tools、comment |
models |
键为精确模型名、前缀.* 或 *;最多 1000 个条目 |
| 一个条目 | read、create、write、unlink、schema(true 或 false);call(true、false,或由 1 到 200 个公开方法名组成的列表);reason(10 到 200 个字符);expires(YYYY-MM-DD);comment |
fields |
withhold:模型.字段 或 *.字段 的列表;allow:{"field": "模型.字段", "reason": "..."} 的列表 |
tools |
{"disabled": [工具名称]} |
| 解析 | 最大 256 KiB,UTF-8(接受开头的 BOM);重复的键是错误 |
- 一个条目只设置它点名的操作。方法列表不会合并:由设置了
call的最具体条目决定。 - 开放需要理由。 开放某个层级操作、或列出某个外发方法的精确条目,需要一个至少 10 个字符的
reason(E15)。reason和expires只适用于精确条目(E19)。 - 过期即关闭。 过了日期之后(UTC 当天结束时),条目的允许项(
true值和call的方法列表都一样)会被忽略,只有它的false拒绝仍然有效,因此该操作会回落到配置档。过期的方法列表不会变成对call的全面拒绝:它点名的业务动词在standard下仍然可用,过期的条目允许的内容永远不会多于去掉允许项后的同一个文件。开放项会显示在odoo_version(opened_sensitive)、--check-policy和审计日志的启动行中。 - 摘要是对配置档、规范化后的规则、字段规则、被禁用的工具、Read-only mode 标志以及层级数据版本计算的 SHA-256,截取为 12 个十六进制字符。注释、理由和键的顺序不会改变它;任何操作值的变化都会改变它。
文件存放位置¶
- 只接受显式的绝对路径。 服务器不会在任何默认位置查找,因为智能体可能在那里创建一个文件并让它被悄悄加载。
- 推荐位置:macOS 和 Linux 上为
~/.config/odoo-tools/policy.json,Windows 上为%APPDATA%\odoo-tools\policy.json。请把它放在所有项目文件夹之外,并确保只有你能写入它(否则会出现警告 W3)。 - 在容器中:
-v ~/.odoo-tools/policy.json:/policy/policy.json:ro -e ODOO_POLICY_FILE=/policy/policy.json(参见 本地部署)。
保护文件和选项¶
这些都不是必需的,而且在 stdio 下,它们都拦不住一个拥有不受限 shell 的智能体(参见 它保护什么,不保护什么);它们只是让安全带更难被解开:
- 像
{"permissions": {"deny": ["Edit(~/.config/odoo-tools/**)", "Write(~/.config/odoo-tools/**)"]}}这样的 Claude Code 权限规则,覆盖 Claude Code 的文件工具以及它在 Bash 中识别出的文件命令,但不覆盖任意子进程。请保持 Bash 的确认提示开启。 - 来自项目文件的插件选项会被忽略:Claude Code 只从用户设置或托管设置中读取插件选项,因此克隆下来的仓库无法把配置档设为
full。 - 服务器指令会告诉智能体:策略的拒绝是最终结果,不得通过其他工具或 shell 寻找绕行办法。
加载错误与警告¶
服务器从不因为一份有问题的策略而崩溃。未知的配置档、相对路径或无效的文件会锁定(locked)服务器:tools/list 只列出 odoo_version,它在不联系 Odoo 的情况下返回带有错误的 policy 块;其他任何工具都会被拒绝,拒绝文本会点名错误和修正它的选项,并说明没有向 Odoo 发送任何内容;启动日志和指令行会带上该错误,审计日志的启动行会带上它的 E 代码。服务器的本地诊断(server.jsonl)也会记下这个 E 代码,在 Claude Code 中,插件的 SessionStart hook 就从那里读取它:当上一次启动被锁定时,它每天最多一次把 E 代码告诉你(它既不读取策略选项,也不读取策略文件)。参见
故障排查。
错误和警告文本按服务器输出的原样以英文显示:
| 代码 | 文本 |
|---|---|
| E1 | unknown permission profile '{value}' (use readonly, standard, builder, full) |
| E2 | the policy file path must be absolute (got '{value}') |
| E3 | policy file {path}: cannot be read ({strerror}) |
| E4 | policy file {path}: is larger than 262144 bytes |
| E5 | policy file {path}: is not valid JSON (line {line}, column {column}: {msg}) |
| E6 | policy file {path}: duplicate key '{key}' (a repeated entry could hide a rule) |
| E7 | policy file {path}: the top level must be an object with "version": 1 |
| E8 | policy file {path}: unknown key '{key}' at the top level (use version, models, fields, tools, comment) |
| E9 | policy file {path}: models['{key}']: not a model name, 'prefix.*' or '*' |
| E10 | policy file {path}: models['{key}']: unknown key '{op}' (use read, create, write, unlink, call, schema, reason, expires, comment) |
| E11 | policy file {path}: models['{key}'].{op}: must be true or false |
| E12 | policy file {path}: models['{key}'].call: must be true, false or a list of method names |
| E13 | policy file {path}: models['{key}'].call: '{method}' is not a public method name |
| E14 | policy file {path}: tools.disabled: unknown tool '{name}' |
| E15 | policy file {path}: models['{key}'] opens {ops} on {article} {tier}-tier model and needs "reason" (at least 10 characters): it is shown in odoo_version and written to the audit log |
| E16 | policy file {path}: models['{key}'].expires: '{value}' is not a date (YYYY-MM-DD) |
| E17 | policy file {path}: fields.{list}[{index}]: '{value}' is not model.field (withhold also accepts *.field) |
| E18 | policy file {path}: models['{key}'].{op}: cannot be granted: {reason} |
| E19 | policy file {path}: models['{key}']: "reason" and "expires" only apply to an entry that names one model exactly |
| E20 | policy file {path}: fields.allow[{index}] needs "reason" (at least 10 characters) |
整个内容为 JSON 值 null 的文件同样会以 E7 锁定。
警告不会锁定服务器。它们出现在 odoo_version.policy.warnings、--check-policy 和审计日志的启动行中。W10 由 odoo_version 在运行时发出,条件是 Odoo 用户为管理员。
| 代码 | 文本 |
|---|---|
| W1 | models['{key}'] refuses create/write/unlink but allows call: business methods (e.g. action_post) can still change these records; set "call": false or list the allowed methods |
| W2 | models['{model}'] opens {ops} on '{model}' ({tier} tier){until}: {reason} |
| W3 | the policy file can be changed by other users of this machine (mode {octal}) |
| W4 | models['{key}'] expired on {date}: its allowances are ignored, its refusals still apply |
| W5 | models['{key}'] sets {op} on a pattern; models of the {tiers} tiers are not affected: name a model exactly to open one |
| W6 | profile 'builder' lets the agent create automations and server actions, which run Python on the Odoo server with sudo() available: for confidentiality it equals 'full'. Use it on sandboxes and staging. |
| W7 | fields.allow releases {field}: {reason} |
| W10 | the Odoo user of this connection is an administrator (base.group_system): Odoo's access rights narrow nothing, so this policy is the only narrowing layer. Prefer a dedicated user with the fewest groups that work. |
离线检查文件:--check-policy¶
python3 "<plugin>/server/run_stdio.py" --check-policy 文件 [--profile standard] \
[--explain 模型:操作[:方法] ...] [--json]
它像服务器那样加载文件(Read-only mode 处于关闭状态),从不联系 Odoo,并打印配置档和摘要、规范化后的规则、带理由和过期时间的开放项、警告、被隐藏的工具,以及每个 --explain 对应的一行:
explain: res.users write: REFUSED (tier:security)
explain: sale.order call action_confirm: ALLOWED (file:sale.order)
explain: account.move read: REFUSED (file:account.invoice, alias of account.move)
explain: website.page write: REFUSED (tier:customization, through ir.ui.view, which website.page delegates to)
退出码:文件有效时为 0,文件有错误时为 1(服务器将以锁定状态启动),用法错误时为 2。相对路径的 文件 在检查时按当前文件夹解析(选项本身必须是绝对路径),空的 文件(--check-policy "")只检查配置档。--json 以 JSON 输出同一份报告。摘要涵盖 Read-only mode 标志,因此以 ODOO_READONLY=1 运行的服务器,对同一文件和配置档会报告另一个摘要。团队的策略仓库可以在 CI 中运行它。在插件内部,<plugin> 就是 ${CLAUDE_PLUGIN_ROOT}。
工具报告的内容¶
odoo_version 返回一个 policy 块:
"policy": {"profile": "standard", "file": null, "digest": "21524d1933f3", "readonly": false,
"hidden_tools": ["odoo_unlink", "odoo_add_field", "odoo_add_automation"],
"opened_sensitive": [], "released_fields": [], "warnings": [],
"error": null, "error_code": null}
opened_sensitive 中的一个条目形如:{"model": "res.partner.bank", "ops": ["create", "write"], "tier": "security", "reason": "...", "expires": "2026-11-15"}。每条警告都是一个 {"code": ..., "text": ...} 对象。策略被锁定时,odoo_version 只返回 plugin 块、带有 error 和 error_code 的 policy 块以及一条说明,不联系 Odoo。
自 v1.6.0 起,odoo_version 还会返回一个 plugin 块:作出响应的 odoo-tools 版本,以及在 Claude Code 中它的已安装副本(plugin@marketplace)。没有 policy 的响应来自早于 1.5.0 的副本,它没有权限策略:引导路线和 /odoo-tools:odoo-doctor 会在那里停止(参见 两个 odoo-tools 副本)。
odoo_access_check在 Odoo 自己的回答旁边返回policy({操作: true 或 false}),当 Odoo 允许而策略拒绝时附上一条说明。odoo_list_models会略去策略在其上不允许任何操作的模型,并给出数量(hidden_by_policy)。odoo_online_profile会把策略扣留的部分报告在unavailable中,而不是失败。
拒绝¶
拒绝是一个工具错误。它点名操作、模型、配置档以及做出决定的规则;然后说明调用在哪里停止、给出补救办法,并要求智能体不要通过其他工具达成同样的结果。它只会复述经过校验的名称(模型、字段、方法、操作和上下文键的名称),从不复述参数值。
- A 层,登录之前(大多数拒绝):根据工具声明的访问和参数做出决定,此时会话尚不存在。它以
Nothing was sent to Odoo.结尾,并且不计为一次工具调用。不在允许列表中的上下文键,以及值为列表或对象的default_<字段>,都在这里被拒绝(规则context)。 - B 层,登录之后(守卫):工具发出的每一个 Odoo 调用在发送时都会连同其值和路径再检查一次。这样的拒绝以
The call was stopped before Odoo ran it: nothing was changed and no record data was returned.结尾。服务器可能为了做出决定而读取了字段定义。在 Odoo 10 至 12 上,按位置传递给业务方法的对象中,既不是模型字段、也不是允许的上下文键的键会在这里被拒绝(规则context,参见 已知限制 10)。 - 超出工具自身声明映射的调用,会被当作 odoo-tools 的缺陷拦下(规则
envelope),在所有配置档中都是如此;请把它报告给 dev@transgenia.org。
拒绝信息以如下形式开头,后面接着补救办法(这里是用 odoo_write {"active": false} 归档):
the odoo-tools permission policy refuses 'unlink' on 'res.partner' (profile 'standard',
rule profile:standard). Nothing was sent to Odoo.
智能体必须把点名权限策略的拒绝视为最终结果:它会告诉你,而你可以更改配置档或策略文件。
审计日志¶
每次工具调用(包括被拒绝的调用)都会留下一行 JSON:工具、它触及的模型和操作名称(op:模型[:方法])、拒绝发生的层和规则、计数、耗时、错误的异常类名以及策略摘要。它从不保存参数值、域、字段值、id、文件内容、URL、令牌、密钥、密码或错误消息文本。Odoo 的数据库和登录名只出现在服务器启动时写入的 start 行中,该行还带有配置档、文件路径、警告(代码)以及带理由的开放项。
{"v":1,"ts":"2026-10-21T15:02:11Z","seq":42,"prev":"9c1e0b7d2a44f1e3","event":"call",
"tool":"odoo_write","layer":"B","decision":"deny","rule":"relational",
"touches":["write:res.partner"],"effective":["write:res.users"],"n":3,"status":"refused",
"ms":41,"policy":"a4f1c0d2e9b7"}
键有 v、ts、seq、prev、event(start、call、chain_reset)、tool、layer(tool、A 或 B)、decision、rule、touches、effective、n、withheld、status(ok、refused、error、invalid)、error、ms、policy,启动行上还有 profile、readonly、version、pid、source、db、login、opened 和 warnings。
ODOO_AUDIT_DIR |
日志行写到哪里 |
|---|---|
一个文件夹(插件设为 ${CLAUDE_PLUGIN_DATA}/audit) |
每个 UTC 日一个文件,odoo-tools-audit-YYYY-MM-DD.jsonl,文件夹 0700,文件 0600,从不经过符号链接 |
未设置或 stderr |
stderr 上的 odoo_mcp.audit 日志记录器(在容器中即 docker logs) |
off |
不写入 |
ODOO_AUDIT_RETENTION_DAYS(默认 90;0 表示永久保留文件)会在服务器启动时以及 UTC 午夜之后的第一次写入时清理旧的每日文件;只有带审计文件名的普通文件才会被删除。插件固定为 90 天。当插件从其最后一个作用域中卸载时,Claude Code 会删除插件的数据文件夹。写入失败从不会中断工具调用。
每一行的 prev 是同一文件中上一行的 SHA-256 的前 16 个十六进制字符(第一行为 genesis)。要检查这条链:
链完整时退出码为 0;在第一行内容被编辑、顺序被改变或被插入的行处,或最后一行不完整时(会点名该行),退出码为 1;文件无法读取时为 2。由于每一行只链接到前一行,单独检查一个文件本身无法发现从某天末尾删掉的整行,也无法发现被整个删除的某天文件:幸存的前缀仍然是连续的链。在本地,这是证据而不是防范:能写入该文件夹的人就能改写文件并重新计算链。跨文件和重启锚定链的接收端属于远程模式的新增功能,这里不提供。
示例¶
仓库在
docs/policy-examples/
中提供了四个文件,CI 会逐一加载:
| 文件 | 配置档 | 适用于 |
|---|---|---|
production-hardened.json |
standard |
使用管理员用户的生产环境,直到有了最小权限用户为止:只允许精选的动词,不访问人力资源数据,不读取 chatter,不更改网站,不导入 |
sales-team.json |
standard |
销售智能体:CRM 和报价单,在 chatter 中回复客户,不做会计更改 |
read-only-analyst.json |
readonly |
报表:聚合和导出,不含人力资源数据、chatter 或联系方式 |
sandbox-builder.json |
builder |
培训沙箱:Studio 风格的改动、删除和测试用户 |
加固文件,用于使用管理员用户的生产环境:
{
"version": 1,
"comment": "Production hardening: business reads and writes, curated verbs only, no imports",
"models": {
"*": {"call": false},
"res.partner": {"call": ["message_post"], "reason": "Agents reply to customers from the chatter"},
"crm.lead": {"call": ["action_set_won", "action_set_lost"]},
"sale.order": {"call": ["action_confirm", "action_cancel"]},
"account.move": {"call": ["action_post"]},
"hr.*": {"read": false, "create": false, "write": false, "call": false},
"mail.message": {"read": false},
"mail.activity":{"read": false},
"website.*": {"write": false, "create": false}
},
"fields": {"withhold": ["*.vat", "hr.employee.private_email"]},
"tools": {"disabled": ["odoo_import", "odoo_import_preview"]}
}
"*": {"call": false}让odoo_execute只剩下精选的动词;它的读取方法也会被拒绝,因为它们需要call。- 对
hr.*、mail.message和mail.activity的拒绝,让人力资源数据也无法通过 chatter 触及(参见已知限制)。 - 对
website.*的拒绝关闭了已发布内容这条渠道。
使用方法:把文件复制到所有项目之外(例如 ~/.config/odoo-tools/policy.json),用 --check-policy 检查它,然后把它的绝对路径填入 Permission policy file。
工具映射¶
每个工具都会声明它触及什么,没有映射的工具无法被注册、列出或调用。A 层 在会话存在之前根据参数对声明的内容做出决定。工具发出的固定机制性调用是内部条目,每一个都由针对实际调用的谓词固定,并附有理由;守卫(B 层)会拒绝任何既未声明、也不是谓词成立的内部调用。
| 工具 | 只读 | 声明(A 层) | 在 readonly / standard / builder 中隐藏 |
|---|---|---|---|
odoo_access_check |
是 | 对所请求模型的 inspect | 否 / 否 / 否 |
odoo_add_automation |
否 | 对所请求模型的 schema;create base.automation;create ir.actions.server |
是 / 是 / 否 |
odoo_add_field |
否 | 对所请求模型的 schema;给出 relation 时,对其模型的 inspect |
是 / 是 / 否 |
odoo_api_catalog |
是 | 对所请求模型的 inspect | 否 / 否 / 否 |
odoo_connections |
是 | 无模型 | 否 / 否 / 否 |
odoo_create |
否 | 对所请求模型的 create | 是 / 否 / 否 |
odoo_execute |
否 | 对所请求方法的 call,加上其类别(例如 unlink) | 是 / 否 / 否 |
odoo_export_records_csv |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_export_records_json |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_fields_get |
是 | 对所请求模型的 inspect | 否 / 否 / 否 |
odoo_import |
否 | 对所请求模型的 create 和 write;fields 各列的路径 |
是 / 否 / 否 |
odoo_import_preview |
否 | 对所请求模型的 create 和 write;fields 各列的路径 |
是 / 否 / 否 |
odoo_list_models |
是 | read ir.model |
否 / 否 / 否 |
odoo_module_info |
是 | read ir.module.module |
否 / 否 / 否 |
odoo_online_profile |
是 | read res.users、res.company、res.users.apikeys 和 ir.module.module;inspect res.partner;被拒绝的部分会被报告,工具照常运行 |
否 / 否 / 否 |
odoo_read |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_read_group |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_record_documents |
是 | 对所请求模型的 read;read ir.attachment |
否 / 否 / 否 |
odoo_report |
是 | 从不调用 Odoo | 否 / 否 / 否 |
odoo_search |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_search_count |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_search_read |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_telemetry_preview |
是 | 无模型 | 否 / 否 / 否 |
odoo_translate_get |
是 | 对所请求模型的 read | 否 / 否 / 否 |
odoo_translate_set |
否 | 对所请求模型的 write | 是 / 否 / 否 |
odoo_unlink |
否 | 对所请求模型的 unlink | 是 / 是 / 否 |
odoo_version |
是 | 无模型 | 否 / 否 / 否 |
odoo_write |
否 | 对所请求模型的 write | 是 / 否 / 否 |
要向服务器添加工具,参见 贡献。
已知限制¶
- 方法内部的副作用。 当 Odoo 如此配置时,
action_confirm可能会发送确认邮件,已有的自动化也会在被允许的写入上运行。这些由 Odoo 针对已登录用户来管理。加固文件把call限定为方法列表。 - 方法返回的字符串不会被隐去,例如一个带有令牌的门户 URL。
ir.attachment以外的指针模型(mail.message、mail.activity、mail.followers、rating.rating),在文件拒绝读取业务模型时才变得重要;加固示例会把它们与hr.*一起拒绝。- 名称无害的关联字段和计算字段,以及显示名称,可能携带被屏蔽模型的数据。在这方面,Odoo 的字段组仍然是控制手段。
- many2one 链接不作为触及项检查。 把
partner_bank_id设为一个已有的银行账户,只算作对父记录的写入。 - 面向公众的字段在效果上是一条外发渠道,例如网站上发布的描述。加固文件可以拒绝对
website.*和product.template的写入。 - 无法阻止通过计数和聚合进行推断。
- 在 stdio 下,拥有 shell 的智能体可以绕过策略,并且在机密性方面
builder等同于full。 - 嵌套附件可以通过上下文默认值绑定到某条记录。
default_res_model和default_res_id仍然允许,因为活动安排和邮件编辑器会传入它们,所以一个创建嵌套ir.attachment的调用(直接创建,或通过product.document这样的委托子模型)可以从上下文中设置该附件的res_model和res_id。直接用这些值创建附件仍然会被检查(父模型必须允许write);只有嵌套创建这条路径被接受,因为任何针对调用顶层模型的检查都无法跟踪它。public和res_field作为默认值会被拒绝,因此这只是绑定一个附件,而不会暴露它。 -
在 Odoo 10 至 12 上按位置传递的上下文。 这些版本系列还会把紧跟在方法自身参数之后的那个位置参数当作调用的上下文;从 13 起只读取
kwargs["context"]。策略把这样的参数当作上下文来读取:在 A 层对所有版本系列适用,在发送任何内容之前,通过odoo_execute调用时无论是否带ids都是如此;在 10 至 12 上还会在受守卫的会话中再检查一次。这些版本系列用dict()构建上下文,因此由[键, 值]对组成的列表在下文中也算作对象,按它构建出的上下文来读取;字段名、id 或域条件组成的列表则不会构建出上下文。- 对于策略了解其参数的方法(
search_read、create、write以及它读取的其他方法),位于参数之后的对象,或者位于一个从不承载对象的参数位置上的对象(search_count的limit,Odoo 10 至 12 没有这个参数),都要满足所有上下文规则。当上下文规则放行时,在参数之后还带有参数的调用仍会像以前一样被拒绝(规则shape)。 - 对于其他任何方法(例如
action_confirm这样的业务方法),策略无法知道参数在哪里结束,因此每个按位置传递的对象都可能是上下文,也可能是该方法的某个值。它的default_<字段>键在所有版本系列上都要满足上下文规则(列表或对象、在可能创建记录的调用上的结构性字段、被屏蔽的字段),而像{"default_code": "A-1"}这样的值对象仍然照常运行。自 v1.6.0 起,在 Odoo 10 至 12 上,受守卫的会话还会用模型的字段来判定它的其他键:是模型字段的键属于该方法的值,会像该方法的其他参数一样被检查(参见 类凭据字段);允许的上下文键可以通过;其他任何键(例如tracking_disable)只可能是上下文键,会被拒绝(规则context)。这些字段就是受守卫的会话为其他检查读取的定义(fields_get,带缓存),只在出现这类键时才读取;如果 Odoo 拒绝这次查询,调用会被拒绝(规则unverified)。不是文本的键只可能出现在键值对列表中(例如[4, 5]这样的 x2many 链接),它不对应 Odoo 读取的任何上下文键,因此不做处理。所以,一个由文本键值对组成、且第一项既不是模型字段也不是允许的上下文键的列表(选择项的键值对、一行两列的数据),按位置传递时也会在那里被拒绝:请按名称传递该方法的参数。
这些拒绝(规则
context)会点名该键,从不显示它的值,并提示按名称在kwargs["context"]中传递上下文、按名称传递该方法的其他参数,或者把这些值放在记录的values中。剩下的情况:在 Odoo 10 至 12 上,这类对象中是模型字段的键会被当作值来读取,因此与模型某个字段同名的上下文键不受允许清单的约束。允许清单有意排除的键(tracking_disable、mail_notrack、mail_create_nolog)会被拒绝,除非模型有同名的字段。请按名称在kwargs["context"]中传递上下文。 - 对于策略了解其参数的方法(