Skip to content

Commit

Permalink
refactor: core plugin permissions are now prefixed core:, closes #10359
Browse files Browse the repository at this point in the history
… (#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>
  • Loading branch information
lucasfernog and tweidinger authored Jul 30, 2024
1 parent a0841d5 commit 758d28c
Show file tree
Hide file tree
Showing 27 changed files with 461 additions and 437 deletions.
9 changes: 9 additions & 0 deletions .changes/core-plugin-namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"tauri": patch:breaking
"tauri-plugin": patch:breaking
"@tauri-apps/cli": patch:breaking
"tauri-cli": patch:breaking
---

Core plugin permissions are now prefixed with `core:`, the `core:default` permission set can now be used and the `core` plugin name is reserved.
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:`.
4 changes: 2 additions & 2 deletions core/tauri-acl-schema/capability-schema.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Capability",
"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 ```",
"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 ```",
"type": "object",
"required": [
"identifier",
Expand Down Expand Up @@ -48,7 +48,7 @@
}
},
"permissions": {
"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 ```",
"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 ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
Expand Down
11 changes: 7 additions & 4 deletions core/tauri-build/src/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,13 @@ pub fn validate_capabilities(

for permission_entry in &capability.permissions {
let permission_id = permission_entry.identifier();
let (key, permission_name) = permission_id
.get()
.split_once(':')
.unwrap_or_else(|| (APP_ACL_KEY, permission_id.get()));

let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
let permission_name = permission_id.get_base();

if key == "core" && permission_name == "default" {
continue;
}

let permission_exists = acl_manifests
.get(key)
Expand Down
4 changes: 2 additions & 2 deletions core/tauri-config-schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@
]
},
"Capability": {
"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 ```",
"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 ```",
"type": "object",
"required": [
"identifier",
Expand Down Expand Up @@ -1129,7 +1129,7 @@
}
},
"permissions": {
"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 ```",
"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 ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
Expand Down
5 changes: 5 additions & 0 deletions core/tauri-plugin/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use serde::de::DeserializeOwned;

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

const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];

pub fn plugin_config<T: DeserializeOwned>(name: &str) -> Option<T> {
let config_env_var_name = format!(
"TAURI_{}_PLUGIN_CONFIG",
Expand Down Expand Up @@ -93,6 +95,9 @@ impl<'a> Builder<'a> {
if name.contains('_') {
anyhow::bail!("plugin names cannot contain underscores");
}
if RESERVED_PLUGIN_NAMES.contains(&name.as_str()) {
anyhow::bail!("plugin name `{name}` is reserved");
}

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

Expand Down
11 changes: 2 additions & 9 deletions core/tauri-utils/src/acl/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl<'de> Deserialize<'de> for PermissionEntry {
/// "main"
/// ],
/// "permissions": [
/// "path:default",
/// "core:default",
/// "dialog:open",
/// {
/// "identifier": "fs:allow-write-text-file",
Expand Down Expand Up @@ -174,14 +174,7 @@ pub struct Capability {
///
/// ```json
/// [
/// "path:default",
/// "event:default",
/// "window:default",
/// "app:default",
/// "image:default",
/// "resources:default",
/// "menu:default",
/// "tray:default",
/// "core:default",
/// "shell:allow-open",
/// "dialog:open",
/// {
Expand Down
12 changes: 8 additions & 4 deletions core/tauri-utils/src/acl/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use thiserror::Error;

const IDENTIFIER_SEPARATOR: u8 = b':';
const PLUGIN_PREFIX: &str = "tauri-plugin-";
const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";

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

let mut bytes = value.bytes();
if bytes.len() > MAX_LEN_IDENTIFIER {
return Err(Self::Error::Humongous(bytes.len()));
if value.len() > MAX_LEN_IDENTIFIER {
return Err(Self::Error::Humongous(value.len()));
}

let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);

let mut bytes = value.bytes();

// grab the first byte only before parsing the rest
let mut prev = bytes
.next()
Expand All @@ -175,7 +179,7 @@ impl TryFrom<String> for Identifier {
None => return Err(Self::Error::InvalidFormat),
Some(next @ ValidByte::Byte(_)) => prev = next,
Some(ValidByte::Separator) => {
if separator.is_none() {
if separator.is_none() || is_core_identifier {
// safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
separator = Some(idx.try_into().unwrap());
prev = ValidByte::Separator
Expand Down
34 changes: 32 additions & 2 deletions core/tauri-utils/src/acl/resolved.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ use super::{
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
pub type ScopeKey = u64;

const CORE_PLUGINS: &[&str] = &[
"core:app",
"core:event",
"core:image",
"core:menu",
"core:path",
"core:resources",
"core:tray",
"core:webview",
"core:window",
];

/// Metadata for what referenced a [`ResolvedCommand`].
#[cfg(debug_assertions)]
#[derive(Default, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -80,7 +92,7 @@ impl Resolved {
/// Resolves the ACL for the given plugin permissions and app capabilities.
pub fn resolve(
acl: &BTreeMap<String, Manifest>,
capabilities: BTreeMap<String, Capability>,
mut capabilities: BTreeMap<String, Capability>,
target: Target,
) -> Result<Self, Error> {
let mut allowed_commands = BTreeMap::new();
Expand All @@ -91,7 +103,7 @@ impl Resolved {
let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();

// resolve commands
for capability in capabilities.values() {
for capability in capabilities.values_mut() {
if !capability
.platforms
.as_ref()
Expand All @@ -101,6 +113,20 @@ impl Resolved {
continue;
}

if let Some(core_default_index) = capability.permissions.iter().position(|permission| {
matches!(
permission,
PermissionEntry::PermissionRef(i) if i.get() == "core:default"
)
}) {
capability.permissions.remove(core_default_index);
for plugin in CORE_PLUGINS {
capability.permissions.push(PermissionEntry::PermissionRef(
format!("{plugin}:default").try_into().unwrap(),
));
}
}

with_resolved_permissions(
capability,
acl,
Expand Down Expand Up @@ -134,6 +160,8 @@ impl Resolved {
&mut allowed_commands,
if key == APP_ACL_KEY {
allowed_command.to_string()
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{allowed_command}")
} else {
format!("plugin:{key}|{allowed_command}")
},
Expand All @@ -149,6 +177,8 @@ impl Resolved {
&mut denied_commands,
if key == APP_ACL_KEY {
denied_command.to_string()
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{denied_command}")
} else {
format!("plugin:{key}|{denied_command}")
},
Expand Down
26 changes: 15 additions & 11 deletions core/tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ use std::{
static CHECKED_FEATURES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
const PLUGINS: &[(&str, &[(&str, bool)])] = &[
// (plugin_name, &[(command, enabled-by_default)])
// note that when adding new core plugins, they must be added to the ACL resolver aswell
(
"path",
"core:path",
&[
("resolve_directory", true),
("resolve", true),
Expand All @@ -32,7 +33,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"event",
"core:event",
&[
("listen", true),
("unlisten", true),
Expand All @@ -41,7 +42,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"window",
"core:window",
&[
("create", false),
// getters
Expand Down Expand Up @@ -114,7 +115,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"webview",
"core:webview",
&[
("create_webview", false),
("create_webview_window", false),
Expand All @@ -134,7 +135,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"app",
"core:app",
&[
("version", true),
("name", true),
Expand All @@ -145,7 +146,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"image",
"core:image",
&[
("new", true),
("from_bytes", true),
Expand All @@ -154,9 +155,9 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("size", true),
],
),
("resources", &[("close", true)]),
("core:resources", &[("close", true)]),
(
"menu",
"core:menu",
&[
("new", true),
("append", true),
Expand All @@ -183,7 +184,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
],
),
(
"tray",
"core:tray",
&[
("new", true),
("get_by_id", true),
Expand Down Expand Up @@ -328,7 +329,8 @@ fn define_permissions(out_dir: &Path) {
";

for (plugin, commands) in PLUGINS {
let permissions_out_dir = out_dir.join("permissions").join(plugin);
let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin);
let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name);
let autogenerated =
permissions_out_dir.join(tauri_utils::acl::build::AUTOGENERATED_FOLDER_NAME);
let commands_dir = autogenerated.join("commands");
Expand Down Expand Up @@ -375,7 +377,9 @@ permissions = [{default_permissions}]
)
.unwrap_or_else(|e| panic!("failed to define permissions for {plugin}: {e}"));

let docs_out_dir = Path::new("permissions").join(plugin).join("autogenerated");
let docs_out_dir = Path::new("permissions")
.join(plugin_directory_name)
.join("autogenerated");
create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory");
tauri_utils::acl::build::generate_docs(
&permissions,
Expand Down
Loading

0 comments on commit 758d28c

Please sign in to comment.