Rust in Everything
基于 tauri 2.12.5

Command greet not allowed by ACL

主线位置:调用主线上 Webview::on_message 比对完 invoke key 之后的 ACL 检查。检查查什么、拒绝信息怎么拼,见运行时检查;能力怎么在编译期展开成表,见编译期解析。本页从「看到这句报错」出发,把几种常见原因各复现一遍。

flowchart TB
  rej["invoke 被拒"] --> build{"构建类型"}
  build -->|release| one["Command x not allowed by ACL"]
  one --> dbg["换 debug 构建再看一次"]
  build -->|debug| head{"拒绝信息开头"}
  dbg --> head
  head -->|"not allowed on window"| win["能力的 windows 没有这个标签"]
  head -->|"Command not found"| typo["命令名拼错或没进清单"]
  head -->|"Permissions associated"| perm["插件命令没授权"]
  head -->|"explicitly denied"| deny["某份能力里有 deny"]

实验 crate exp-tauri/pit3-acl:从模板 base 复制,tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口。build.rs 用 AppManifest::new().commands(&["greet", "report"]) 声明了应用清单,所以应用命令也要过 ACL(没有应用清单时本地页面调应用命令不查,见运行时检查)。窗口不写在配置里,由 setup 按环境变量 WINDOWS 建。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

两份能力。main 给 main 窗口 core:default 和 allow-greet;report 给所有窗口 allow-report,好让每个页面都能把结果报回来:

{
  "identifier": "main",
  "windows": ["main"],
  "permissions": ["core:default", "allow-greet"]
}
{
  "identifier": "report",
  "windows": ["*"],
  "remote": { "urls": ["http://127.0.0.1:19010"] },
  "permissions": ["allow-report"]
}

应用开了 main 和一个后来加的 settings 窗口,两个窗口加载同一个页面,各调三次:

  const tries = [
    ['greet', { name: 'rie' }],
    ['gret', { name: 'rie' }],
    ['plugin:window|set_title', { value: 'hi' }],
  ];
  const out = [];
  for (const [cmd, args] of tries) {
    try { out.push(`${cmd} -> ok ${JSON.stringify(await T.invoke(cmd, args))}`); }
    catch (e) { out.push(`${cmd} -> err ${e}`); }
  }

WINDOWS="main=acl.html,settings=acl.html",release 构建:

REPORT [main tauri://localhost/acl.html]
greet -> ok "hello, rie"
gret -> err Command gret not allowed by ACL
plugin:window|set_title -> err Command plugin:window|set_title not allowed by ACL
REPORT [settings tauri://localhost/acl.html]
greet -> err Command greet not allowed by ACL
gret -> err Command gret not allowed by ACL
plugin:window|set_title -> err Command plugin:window|set_title not allowed by ACL

五次拒绝,同一句话。同一份代码,debug 构建:

REPORT [main tauri://localhost/acl.html]
greet -> ok "hello, rie"
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed. Permissions associated with this command: core:window:allow-set-title
REPORT [settings tauri://localhost/acl.html]
greet -> err greet not allowed on window "settings", webview "settings", URL: local

allowed on: [windows: "main", URL: local]

referenced by: capability: main, permission: allow-greet
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed. Permissions associated with this command: core:window:allow-set-title

三种原因,三种措辞:

  • 新窗口不在能力里。settings 调 greet:allow-greet 存在,但只给了 windows: ["main"]。信息列出所有允许条目(allowed on)和它们的出处(referenced by)。
  • 命令名拼错。gret 不在任何清单里,debug 说 Command not found。有应用清单时这句话来自 ACL,不是来自 generate_handler!:请求在找命令之前就被拦了。
  • 插件命令没授权。core:default 里的 core:window:default 几乎全是 title、inner_size 这类读取命令,set_title 要单独加 core:window:allow-set-title。信息直接列出能放行它的权限。

还有一种不改任何能力文件就会出现的情况。配置里写了 app > security > capabilities:

    "security": { "capabilities": ["main", "report"] }

然后照「新窗口加一份能力」的做法新建 capabilities/settings.json(内容见下面的修法)。构建没有任何警告,settings 窗口的输出和上面的 debug 输出一字不差:greet not allowed on window "settings",allowed on 里只有 main,新文件根本没参与。

出处

拦人的是 on_message 里的这个条件:插件命令、有应用清单、来源是远程,三者之一成立,并且 resolve_access 返回 None,就拒绝。debug 下拒绝信息由 resolve_access_message 现场重算,release 下是一句固定的 format!("Command {} not allowed by ACL", request.cmd)。所以 release 的报错没有诊断价值,它只告诉你「被 ACL 拦了」;原因要回 debug 构建看。几种措辞的完整对照表在运行时检查的「拒绝时说什么」。

能力的 windows 只是一张标签的 glob 列表,代码里 WebviewWindowBuilder::new(app, "settings", ..) 多建一个窗口,这张表不会自己长出新标签。

配置列了能力时只认列出的那些,选择在 get_capabilities:

  let mut capabilities = if config.app.security.capabilities.is_empty() {
    capabilities_from_files
  } else {
    let mut capabilities = BTreeMap::new();
    for capability_entry in &config.app.security.capabilities {
      match capability_entry {
        CapabilityEntry::Inlined(capability) => {
          capabilities.insert(capability.identifier.clone(), capability.clone());
        }
        CapabilityEntry::Reference(id) => {
          let capability = capabilities_from_files
            .remove(id)
            .with_context(|| format!("capability with identifier {id} not found"))?;
          capabilities.insert(id.clone(), capability);
        }
      }
    }
    capabilities
  };

列表非空时,capabilities_from_files 只用来按标识符查;没被引用的文件照样被解析、照样写进 gen/schemas/capabilities.json(见 tauri-build),但不进 Resolved。所以看 gen/schemas/capabilities.json 也看不出它没生效。

修法

给 settings 窗口一份能力,把要用的插件权限也加上:

{
  "identifier": "settings",
  "windows": ["settings"],
  "permissions": ["core:default", "allow-greet", "core:window:allow-set-title"]
}

配置里没有 app > security > capabilities 时(或者把 "settings" 加进那个列表),debug 构建:

REPORT [main tauri://localhost/acl.html]
greet -> ok "hello, rie"
gret -> err gret not allowed. Command not found
plugin:window|set_title -> err window.set_title not allowed on window "main", webview "main", URL: local

allowed on: [windows: "settings", URL: local]

referenced by: capability: settings, permission: allow-set-title
REPORT [settings tauri://localhost/acl.html]
greet -> ok "hello, rie"
gret -> err gret not allowed. Command not found
plugin:window|set_title -> ok null
  • settings 的 greet、set_title 都通过了。main 的 set_title 换了一种拒绝措辞:现在有了允许条目,只是不在 main 上。
  • 运行时按需建、标签带编号的窗口,用 glob:"windows": ["main", "settings-*"]。
  • gret 只能改代码。有应用清单的项目里,「命令不存在」和「没授权」在 release 下是同一句话,拼写错误不会以「not found」的形式出现。

怎么确认没中招

  • 开发时用 debug 构建跑,并且把 invoke 的 catch 打出来:拒绝的 Promise 以一个字符串 reject,不打出来就只是「按钮没反应」。
  • 搜代码里所有 WebviewWindowBuilder::new / WebviewBuilder::new 的标签,逐个对照能力文件的 windows / webviews。
  • 配置里有 app > security > capabilities 时,新建能力文件后要同时把标识符加进去;或者干脆删掉这个列表,让 capabilities/ 下的文件全部生效。
  • 插件命令的权限名按插件那一页的规则推:plugin:window|set_title 对应 core:window:allow-set-title;debug 的拒绝信息也会直接列出来。

小结

  • release 下所有 ACL 拒绝都是 Command <cmd> not allowed by ACL;debug 下按原因分成「not allowed on window」「Command not found」「Permissions associated with this command」「explicitly denied」几种。
  • 新建的窗口要出现在某份能力的 windows 里;运行时编号的标签用 glob。
  • core:default 只给插件的读取类命令,写操作(set_title 等)要单独授权。
  • 有应用清单时,拼错的命令名也是「not allowed」。
  • 配置里列了 app > security > capabilities,没列进去的能力文件被静默忽略。

💬 关联讨论

发起讨论
加载中...