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,没列进去的能力文件被静默忽略。