diff --git a/core/tauri-build/src/acl.rs b/core/tauri-build/src/acl.rs index 86ea9e22a..97e124358 100644 --- a/core/tauri-build/src/acl.rs +++ b/core/tauri-build/src/acl.rs @@ -132,8 +132,9 @@ pub fn validate_capabilities( continue; } - for permission in &capability.permissions { - if let Some((plugin_name, permission_name)) = permission.get().split_once(':') { + for permission_entry in &capability.permissions { + let permission_id = permission_entry.identifier(); + if let Some((plugin_name, permission_name)) = permission_id.get().split_once(':') { let permission_exists = plugin_manifests .get(plugin_name) .map(|manifest| { @@ -162,7 +163,7 @@ pub fn validate_capabilities( anyhow::bail!( "Permission {} not found, expected one of {}", - permission.get(), + permission_id.get(), available_permissions.join(", ") ); } diff --git a/core/tauri-utils/src/acl/capability.rs b/core/tauri-utils/src/acl/capability.rs index 5d163175e..dfe234d69 100644 --- a/core/tauri-utils/src/acl/capability.rs +++ b/core/tauri-utils/src/acl/capability.rs @@ -7,11 +7,37 @@ use crate::{acl::Identifier, platform::Target}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +use super::Scopes; -/// A set of direct capabilities grouped together under a new name. -pub struct CapabilitySet { - inner: Vec, +/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] +/// or an object that references a permission and extends its scope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub enum PermissionEntry { + /// Reference a permission or permission set by identifier. + PermissionRef(Identifier), + /// Reference a permission or permission set by identifier and extends its scope. + ExtendedPermission { + /// Identifier of the permission or permission set. + identifier: Identifier, + /// Scope to append to the existing permission scope. + #[serde(default, flatten)] + scope: Scopes, + }, +} + +impl PermissionEntry { + /// The identifier of the permission referenced in this entry. + pub fn identifier(&self) -> &Identifier { + match self { + Self::PermissionRef(identifier) => identifier, + Self::ExtendedPermission { + identifier, + scope: _, + } => identifier, + } + } } /// a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime. @@ -36,7 +62,7 @@ pub struct Capability { /// List of windows that uses this capability. Can be a glob pattern. pub windows: Vec, /// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. - pub permissions: Vec, + pub permissions: Vec, /// Target platforms this capability applies. By default all platforms applies. #[serde(default = "default_platforms")] pub platforms: Vec, diff --git a/core/tauri-utils/src/acl/resolved.rs b/core/tauri-utils/src/acl/resolved.rs index dee874c98..07589e32b 100644 --- a/core/tauri-utils/src/acl/resolved.rs +++ b/core/tauri-utils/src/acl/resolved.rs @@ -14,7 +14,7 @@ use glob::Pattern; use crate::platform::Target; use super::{ - capability::{Capability, CapabilityContext}, + capability::{Capability, CapabilityContext, PermissionEntry}, plugin::Manifest, Error, ExecutionContext, Permission, PermissionSet, Scopes, Value, }; @@ -83,24 +83,48 @@ impl Resolved { continue; } - for permission_id in &capability.permissions { + for permission_entry in &capability.permissions { + let permission_id = permission_entry.identifier(); let permission_name = permission_id.get_base(); if let Some(plugin_name) = permission_id.get_prefix() { let permissions = get_permissions(plugin_name, permission_name, &acl)?; for permission in permissions { + let scope = match permission_entry { + PermissionEntry::PermissionRef(_) => permission.scope.clone(), + PermissionEntry::ExtendedPermission { + identifier: _, + scope, + } => { + let mut merged = permission.scope.clone(); + if let Some(allow) = scope.allow.clone() { + merged + .allow + .get_or_insert_with(Default::default) + .extend(allow); + } + if let Some(deny) = scope.deny.clone() { + merged + .deny + .get_or_insert_with(Default::default) + .extend(deny); + } + merged + } + }; + if permission.commands.allow.is_empty() && permission.commands.deny.is_empty() { // global scope global_scope .entry(plugin_name.to_string()) .or_default() - .push(permission.scope.clone()); + .push(scope.clone()); } else { - let has_scope = permission.scope.allow.is_some() || permission.scope.deny.is_some(); + let has_scope = scope.allow.is_some() || scope.deny.is_some(); if has_scope { current_scope_id += 1; - command_scopes.insert(current_scope_id, permission.scope.clone()); + command_scopes.insert(current_scope_id, scope.clone()); } let scope_id = if has_scope { diff --git a/core/tests/acl/fixtures/capabilities/scope-extended/cap.json b/core/tests/acl/fixtures/capabilities/scope-extended/cap.json new file mode 100644 index 000000000..e0eebc73a --- /dev/null +++ b/core/tests/acl/fixtures/capabilities/scope-extended/cap.json @@ -0,0 +1,40 @@ +{ + "identifier": "run-app", + "description": "app capability", + "windows": [ + "main" + ], + "permissions": [ + { + "identifier": "fs:read", + "allow": [ + { + "path": "$HOME/.config/**" + } + ] + }, + "fs:deny-home", + { + "identifier": "fs:allow-read-resources", + "deny": [ + { + "path": "$RESOURCE/**/*.key" + } + ] + }, + "fs:allow-move-temp", + { + "identifier": "fs:allow-app", + "allow": [ + { + "path": "$APP/**" + } + ], + "deny": [ + { + "path": "$APP/*.db" + } + ] + } + ] +} \ No newline at end of file diff --git a/core/tests/acl/fixtures/capabilities/scope-extended/required-plugins.json b/core/tests/acl/fixtures/capabilities/scope-extended/required-plugins.json new file mode 100644 index 000000000..9ab323181 --- /dev/null +++ b/core/tests/acl/fixtures/capabilities/scope-extended/required-plugins.json @@ -0,0 +1 @@ +["fs"] diff --git a/core/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap b/core/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap new file mode 100644 index 000000000..1e04fac83 --- /dev/null +++ b/core/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap @@ -0,0 +1,212 @@ +--- +source: core/tests/acl/src/lib.rs +assertion_line: 59 +expression: resolved +--- +Resolved { + allowed_commands: { + CommandKey { + name: "plugin:fs|move", + context: Local, + }: ResolvedCommand { + windows: [ + Pattern { + original: "main", + tokens: [ + Char( + 'm', + ), + Char( + 'a', + ), + Char( + 'i', + ), + Char( + 'n', + ), + ], + is_recursive: false, + }, + ], + scope: Some( + 792017965103506125, + ), + }, + CommandKey { + name: "plugin:fs|read_dir", + context: Local, + }: ResolvedCommand { + windows: [ + Pattern { + original: "main", + tokens: [ + Char( + 'm', + ), + Char( + 'a', + ), + Char( + 'i', + ), + Char( + 'n', + ), + ], + is_recursive: false, + }, + ], + scope: Some( + 5856262838373339618, + ), + }, + CommandKey { + name: "plugin:fs|read_file", + context: Local, + }: ResolvedCommand { + windows: [ + Pattern { + original: "main", + tokens: [ + Char( + 'm', + ), + Char( + 'a', + ), + Char( + 'i', + ), + Char( + 'n', + ), + ], + is_recursive: false, + }, + ], + scope: Some( + 10252531491715478446, + ), + }, + }, + denied_commands: {}, + command_scope: { + 792017965103506125: ResolvedScope { + allow: [ + Map( + { + "path": String( + "$TEMP/*", + ), + }, + ), + ], + deny: [], + }, + 5856262838373339618: ResolvedScope { + allow: [ + Map( + { + "path": String( + "$HOME/.config/**", + ), + }, + ), + Map( + { + "path": String( + "$RESOURCE/**", + ), + }, + ), + Map( + { + "path": String( + "$RESOURCE", + ), + }, + ), + ], + deny: [ + Map( + { + "path": String( + "$RESOURCE/**/*.key", + ), + }, + ), + ], + }, + 10252531491715478446: ResolvedScope { + allow: [ + Map( + { + "path": String( + "$HOME/.config/**", + ), + }, + ), + Map( + { + "path": String( + "$RESOURCE/**", + ), + }, + ), + Map( + { + "path": String( + "$RESOURCE", + ), + }, + ), + ], + deny: [ + Map( + { + "path": String( + "$RESOURCE/**/*.key", + ), + }, + ), + ], + }, + }, + global_scope: { + "fs": ResolvedScope { + allow: [ + Map( + { + "path": String( + "$APP", + ), + }, + ), + Map( + { + "path": String( + "$APP/**", + ), + }, + ), + ], + deny: [ + Map( + { + "path": String( + "$HOME", + ), + }, + ), + Map( + { + "path": String( + "$APP/*.db", + ), + }, + ), + ], + }, + }, +} diff --git a/core/tests/acl/src/lib.rs b/core/tests/acl/src/lib.rs index 25d899b4a..03abe3dae 100644 --- a/core/tests/acl/src/lib.rs +++ b/core/tests/acl/src/lib.rs @@ -50,7 +50,7 @@ mod tests { .expect("required-plugins.json is not a valid JSON"); let manifests = load_plugins(&fixture_plugins); - let capabilities = parse_capabilities(&format!("{}/*.toml", fixture_entry.path().display())) + let capabilities = parse_capabilities(&format!("{}/cap*", fixture_entry.path().display())) .expect("failed to parse capabilities"); let resolved = Resolved::resolve(manifests, capabilities, Target::current())