Skip to content

Commit 758d28c

Browse files
refactor: core plugin permissions are now prefixed core:, closes #10359 (#10390)
* refactor: core plugin permissions are now prefixed core:, closes #10359 * code review * expand reserved plugin names * fix * add core:default permission set * fix permission usage --------- Co-authored-by: Tillmann <28728469+tweidinger@users.noreply.github.com>
1 parent a0841d5 commit 758d28c

File tree

27 files changed

+461
-437
lines changed

27 files changed

+461
-437
lines changed

.changes/core-plugin-namespace.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"tauri": patch:breaking
3+
"tauri-plugin": patch:breaking
4+
"@tauri-apps/cli": patch:breaking
5+
"tauri-cli": patch:breaking
6+
---
7+
8+
Core plugin permissions are now prefixed with `core:`, the `core:default` permission set can now be used and the `core` plugin name is reserved.
9+
The `tauri migrate` tool will automate the migration process, which involves prefixing all `app`, `event`, `image`, `menu`, `path`, `resources`, `tray`, `webview` and `window` permissions with `core:`.

core/tauri-acl-schema/capability-schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"title": "Capability",
4-
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\n It controls application windows fine grained access to the Tauri core, application, or plugin commands.\n If a window is not matching any capability then it has no access to the IPC layer at all.\n\n This can be done to create groups of windows, based on their required system access, which can reduce\n impact of frontend vulnerabilities in less privileged windows.\n Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.\n A Window can have none, one, or multiple associated capabilities.\n\n ## Example\n\n ```json\n {\n \"identifier\": \"main-user-files-write\",\n \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\",\n \"windows\": [\n \"main\"\n ],\n \"permissions\": [\n \"path:default\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n },\n \"platforms\": [\"macOS\",\"windows\"]\n }\n ```",
4+
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\n It controls application windows fine grained access to the Tauri core, application, or plugin commands.\n If a window is not matching any capability then it has no access to the IPC layer at all.\n\n This can be done to create groups of windows, based on their required system access, which can reduce\n impact of frontend vulnerabilities in less privileged windows.\n Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.\n A Window can have none, one, or multiple associated capabilities.\n\n ## Example\n\n ```json\n {\n \"identifier\": \"main-user-files-write\",\n \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\",\n \"windows\": [\n \"main\"\n ],\n \"permissions\": [\n \"core:default\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n },\n \"platforms\": [\"macOS\",\"windows\"]\n }\n ```",
55
"type": "object",
66
"required": [
77
"identifier",
@@ -48,7 +48,7 @@
4848
}
4949
},
5050
"permissions": {
51-
"description": "List of permissions attached to this capability.\n\n Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.\n For commands directly implemented in the application itself only `${permission-name}`\n is required.\n\n ## Example\n\n ```json\n [\n \"path:default\",\n \"event:default\",\n \"window:default\",\n \"app:default\",\n \"image:default\",\n \"resources:default\",\n \"menu:default\",\n \"tray:default\",\n \"shell:allow-open\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n }\n ```",
51+
"description": "List of permissions attached to this capability.\n\n Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.\n For commands directly implemented in the application itself only `${permission-name}`\n is required.\n\n ## Example\n\n ```json\n [\n \"core:default\",\n \"shell:allow-open\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n }\n ```",
5252
"type": "array",
5353
"items": {
5454
"$ref": "#/definitions/PermissionEntry"

core/tauri-build/src/acl.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,10 +473,13 @@ pub fn validate_capabilities(
473473

474474
for permission_entry in &capability.permissions {
475475
let permission_id = permission_entry.identifier();
476-
let (key, permission_name) = permission_id
477-
.get()
478-
.split_once(':')
479-
.unwrap_or_else(|| (APP_ACL_KEY, permission_id.get()));
476+
477+
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
478+
let permission_name = permission_id.get_base();
479+
480+
if key == "core" && permission_name == "default" {
481+
continue;
482+
}
480483

481484
let permission_exists = acl_manifests
482485
.get(key)

core/tauri-config-schema/schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,7 @@
10821082
]
10831083
},
10841084
"Capability": {
1085-
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\n It controls application windows fine grained access to the Tauri core, application, or plugin commands.\n If a window is not matching any capability then it has no access to the IPC layer at all.\n\n This can be done to create groups of windows, based on their required system access, which can reduce\n impact of frontend vulnerabilities in less privileged windows.\n Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.\n A Window can have none, one, or multiple associated capabilities.\n\n ## Example\n\n ```json\n {\n \"identifier\": \"main-user-files-write\",\n \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\",\n \"windows\": [\n \"main\"\n ],\n \"permissions\": [\n \"path:default\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n },\n \"platforms\": [\"macOS\",\"windows\"]\n }\n ```",
1085+
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\n It controls application windows fine grained access to the Tauri core, application, or plugin commands.\n If a window is not matching any capability then it has no access to the IPC layer at all.\n\n This can be done to create groups of windows, based on their required system access, which can reduce\n impact of frontend vulnerabilities in less privileged windows.\n Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.\n A Window can have none, one, or multiple associated capabilities.\n\n ## Example\n\n ```json\n {\n \"identifier\": \"main-user-files-write\",\n \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\",\n \"windows\": [\n \"main\"\n ],\n \"permissions\": [\n \"core:default\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n },\n \"platforms\": [\"macOS\",\"windows\"]\n }\n ```",
10861086
"type": "object",
10871087
"required": [
10881088
"identifier",
@@ -1129,7 +1129,7 @@
11291129
}
11301130
},
11311131
"permissions": {
1132-
"description": "List of permissions attached to this capability.\n\n Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.\n For commands directly implemented in the application itself only `${permission-name}`\n is required.\n\n ## Example\n\n ```json\n [\n \"path:default\",\n \"event:default\",\n \"window:default\",\n \"app:default\",\n \"image:default\",\n \"resources:default\",\n \"menu:default\",\n \"tray:default\",\n \"shell:allow-open\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n }\n ```",
1132+
"description": "List of permissions attached to this capability.\n\n Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.\n For commands directly implemented in the application itself only `${permission-name}`\n is required.\n\n ## Example\n\n ```json\n [\n \"core:default\",\n \"shell:allow-open\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n }\n ```",
11331133
"type": "array",
11341134
"items": {
11351135
"$ref": "#/definitions/PermissionEntry"

core/tauri-plugin/src/build/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use serde::de::DeserializeOwned;
1313

1414
use std::{env::var, io::Cursor};
1515

16+
const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
17+
1618
pub fn plugin_config<T: DeserializeOwned>(name: &str) -> Option<T> {
1719
let config_env_var_name = format!(
1820
"TAURI_{}_PLUGIN_CONFIG",
@@ -93,6 +95,9 @@ impl<'a> Builder<'a> {
9395
if name.contains('_') {
9496
anyhow::bail!("plugin names cannot contain underscores");
9597
}
98+
if RESERVED_PLUGIN_NAMES.contains(&name.as_str()) {
99+
anyhow::bail!("plugin name `{name}` is reserved");
100+
}
96101

97102
let out_dir = PathBuf::from(build_var("OUT_DIR")?);
98103

core/tauri-utils/src/acl/capability.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ impl<'de> Deserialize<'de> for PermissionEntry {
9494
/// "main"
9595
/// ],
9696
/// "permissions": [
97-
/// "path:default",
97+
/// "core:default",
9898
/// "dialog:open",
9999
/// {
100100
/// "identifier": "fs:allow-write-text-file",
@@ -174,14 +174,7 @@ pub struct Capability {
174174
///
175175
/// ```json
176176
/// [
177-
/// "path:default",
178-
/// "event:default",
179-
/// "window:default",
180-
/// "app:default",
181-
/// "image:default",
182-
/// "resources:default",
183-
/// "menu:default",
184-
/// "tray:default",
177+
/// "core:default",
185178
/// "shell:allow-open",
186179
/// "dialog:open",
187180
/// {

core/tauri-utils/src/acl/identifier.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use thiserror::Error;
1010

1111
const IDENTIFIER_SEPARATOR: u8 = b':';
1212
const PLUGIN_PREFIX: &str = "tauri-plugin-";
13+
const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";
1314

1415
// https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field
1516
const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len();
@@ -156,11 +157,14 @@ impl TryFrom<String> for Identifier {
156157
return Err(Self::Error::Empty);
157158
}
158159

159-
let mut bytes = value.bytes();
160-
if bytes.len() > MAX_LEN_IDENTIFIER {
161-
return Err(Self::Error::Humongous(bytes.len()));
160+
if value.len() > MAX_LEN_IDENTIFIER {
161+
return Err(Self::Error::Humongous(value.len()));
162162
}
163163

164+
let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
165+
166+
let mut bytes = value.bytes();
167+
164168
// grab the first byte only before parsing the rest
165169
let mut prev = bytes
166170
.next()
@@ -175,7 +179,7 @@ impl TryFrom<String> for Identifier {
175179
None => return Err(Self::Error::InvalidFormat),
176180
Some(next @ ValidByte::Byte(_)) => prev = next,
177181
Some(ValidByte::Separator) => {
178-
if separator.is_none() {
182+
if separator.is_none() || is_core_identifier {
179183
// safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
180184
separator = Some(idx.try_into().unwrap());
181185
prev = ValidByte::Separator

core/tauri-utils/src/acl/resolved.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ use super::{
1717
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
1818
pub type ScopeKey = u64;
1919

20+
const CORE_PLUGINS: &[&str] = &[
21+
"core:app",
22+
"core:event",
23+
"core:image",
24+
"core:menu",
25+
"core:path",
26+
"core:resources",
27+
"core:tray",
28+
"core:webview",
29+
"core:window",
30+
];
31+
2032
/// Metadata for what referenced a [`ResolvedCommand`].
2133
#[cfg(debug_assertions)]
2234
#[derive(Default, Clone, PartialEq, Eq)]
@@ -80,7 +92,7 @@ impl Resolved {
8092
/// Resolves the ACL for the given plugin permissions and app capabilities.
8193
pub fn resolve(
8294
acl: &BTreeMap<String, Manifest>,
83-
capabilities: BTreeMap<String, Capability>,
95+
mut capabilities: BTreeMap<String, Capability>,
8496
target: Target,
8597
) -> Result<Self, Error> {
8698
let mut allowed_commands = BTreeMap::new();
@@ -91,7 +103,7 @@ impl Resolved {
91103
let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
92104

93105
// resolve commands
94-
for capability in capabilities.values() {
106+
for capability in capabilities.values_mut() {
95107
if !capability
96108
.platforms
97109
.as_ref()
@@ -101,6 +113,20 @@ impl Resolved {
101113
continue;
102114
}
103115

116+
if let Some(core_default_index) = capability.permissions.iter().position(|permission| {
117+
matches!(
118+
permission,
119+
PermissionEntry::PermissionRef(i) if i.get() == "core:default"
120+
)
121+
}) {
122+
capability.permissions.remove(core_default_index);
123+
for plugin in CORE_PLUGINS {
124+
capability.permissions.push(PermissionEntry::PermissionRef(
125+
format!("{plugin}:default").try_into().unwrap(),
126+
));
127+
}
128+
}
129+
104130
with_resolved_permissions(
105131
capability,
106132
acl,
@@ -134,6 +160,8 @@ impl Resolved {
134160
&mut allowed_commands,
135161
if key == APP_ACL_KEY {
136162
allowed_command.to_string()
163+
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
164+
format!("plugin:{core_plugin_name}|{allowed_command}")
137165
} else {
138166
format!("plugin:{key}|{allowed_command}")
139167
},
@@ -149,6 +177,8 @@ impl Resolved {
149177
&mut denied_commands,
150178
if key == APP_ACL_KEY {
151179
denied_command.to_string()
180+
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
181+
format!("plugin:{core_plugin_name}|{denied_command}")
152182
} else {
153183
format!("plugin:{key}|{denied_command}")
154184
},

core/tauri/build.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ use std::{
1818
static CHECKED_FEATURES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
1919
const PLUGINS: &[(&str, &[(&str, bool)])] = &[
2020
// (plugin_name, &[(command, enabled-by_default)])
21+
// note that when adding new core plugins, they must be added to the ACL resolver aswell
2122
(
22-
"path",
23+
"core:path",
2324
&[
2425
("resolve_directory", true),
2526
("resolve", true),
@@ -32,7 +33,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
3233
],
3334
),
3435
(
35-
"event",
36+
"core:event",
3637
&[
3738
("listen", true),
3839
("unlisten", true),
@@ -41,7 +42,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
4142
],
4243
),
4344
(
44-
"window",
45+
"core:window",
4546
&[
4647
("create", false),
4748
// getters
@@ -114,7 +115,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
114115
],
115116
),
116117
(
117-
"webview",
118+
"core:webview",
118119
&[
119120
("create_webview", false),
120121
("create_webview_window", false),
@@ -134,7 +135,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
134135
],
135136
),
136137
(
137-
"app",
138+
"core:app",
138139
&[
139140
("version", true),
140141
("name", true),
@@ -145,7 +146,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
145146
],
146147
),
147148
(
148-
"image",
149+
"core:image",
149150
&[
150151
("new", true),
151152
("from_bytes", true),
@@ -154,9 +155,9 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
154155
("size", true),
155156
],
156157
),
157-
("resources", &[("close", true)]),
158+
("core:resources", &[("close", true)]),
158159
(
159-
"menu",
160+
"core:menu",
160161
&[
161162
("new", true),
162163
("append", true),
@@ -183,7 +184,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
183184
],
184185
),
185186
(
186-
"tray",
187+
"core:tray",
187188
&[
188189
("new", true),
189190
("get_by_id", true),
@@ -328,7 +329,8 @@ fn define_permissions(out_dir: &Path) {
328329
";
329330

330331
for (plugin, commands) in PLUGINS {
331-
let permissions_out_dir = out_dir.join("permissions").join(plugin);
332+
let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin);
333+
let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name);
332334
let autogenerated =
333335
permissions_out_dir.join(tauri_utils::acl::build::AUTOGENERATED_FOLDER_NAME);
334336
let commands_dir = autogenerated.join("commands");
@@ -375,7 +377,9 @@ permissions = [{default_permissions}]
375377
)
376378
.unwrap_or_else(|e| panic!("failed to define permissions for {plugin}: {e}"));
377379

378-
let docs_out_dir = Path::new("permissions").join(plugin).join("autogenerated");
380+
let docs_out_dir = Path::new("permissions")
381+
.join(plugin_directory_name)
382+
.join("autogenerated");
379383
create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory");
380384
tauri_utils::acl::build::generate_docs(
381385
&permissions,

0 commit comments

Comments
 (0)