diff --git a/core/tauri-utils/src/acl/mod.rs b/core/tauri-utils/src/acl/mod.rs index c37102310..b33a3e644 100644 --- a/core/tauri-utils/src/acl/mod.rs +++ b/core/tauri-utils/src/acl/mod.rs @@ -200,6 +200,10 @@ pub enum ExecutionContext { #[cfg(feature = "build")] mod build_ { + use std::convert::identity; + + use crate::{literal_struct, tokens::*}; + use super::*; use proc_macro2::TokenStream; use quote::{quote, ToTokens, TokenStreamExt}; @@ -219,4 +223,57 @@ mod build_ { }); } } + + impl ToTokens for Commands { + fn to_tokens(&self, tokens: &mut TokenStream) { + let allow = vec_lit(&self.allow, str_lit); + let deny = vec_lit(&self.deny, str_lit); + literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny) + } + } + + impl ToTokens for Scopes { + fn to_tokens(&self, tokens: &mut TokenStream) { + let allow = opt_vec_lit(self.allow.as_ref(), identity); + let deny = opt_vec_lit(self.deny.as_ref(), identity); + literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny) + } + } + + impl ToTokens for Permission { + fn to_tokens(&self, tokens: &mut TokenStream) { + let version = opt_lit_owned(self.version.as_ref().map(|v| { + let v = v.get(); + quote!(::core::num::NonZeroU64::new(#v).unwrap()) + })); + let identifier = str_lit(&self.identifier); + let description = opt_str_lit(self.description.as_ref()); + let commands = &self.commands; + let scope = &self.scope; + literal_struct!( + tokens, + ::tauri::utils::acl::Permission, + version, + identifier, + description, + commands, + scope + ) + } + } + + impl ToTokens for PermissionSet { + fn to_tokens(&self, tokens: &mut TokenStream) { + let identifier = str_lit(&self.identifier); + let description = str_lit(&self.description); + let permissions = vec_lit(&self.permissions, str_lit); + literal_struct!( + tokens, + ::tauri::utils::acl::PermissionSet, + identifier, + description, + permissions + ) + } + } } diff --git a/core/tauri-utils/src/acl/plugin.rs b/core/tauri-utils/src/acl/plugin.rs index 9f4258a97..9f427651b 100644 --- a/core/tauri-utils/src/acl/plugin.rs +++ b/core/tauri-utils/src/acl/plugin.rs @@ -36,9 +36,6 @@ pub struct PermissionFile { #[serde(default)] pub set: Vec, - /// Test something!! - pub test: Option, - /// A list of inlined permissions #[serde(default)] pub permission: Vec, @@ -110,3 +107,63 @@ impl Manifest { manifest } } + +#[cfg(feature = "build")] +mod build { + use proc_macro2::TokenStream; + use quote::{quote, ToTokens, TokenStreamExt}; + use std::convert::identity; + + use super::*; + use crate::{literal_struct, tokens::*}; + + impl ToTokens for DefaultPermission { + fn to_tokens(&self, tokens: &mut TokenStream) { + let version = opt_lit_owned(self.version.as_ref().map(|v| { + let v = v.get(); + quote!(::core::num::NonZeroU64::new(#v).unwrap()) + })); + let description = opt_str_lit(self.description.as_ref()); + let permissions = vec_lit(&self.permissions, str_lit); + literal_struct!( + tokens, + ::tauri::utils::acl::plugin::DefaultPermission, + version, + description, + permissions + ) + } + } + + impl ToTokens for Manifest { + fn to_tokens(&self, tokens: &mut TokenStream) { + let default_permission = opt_lit(self.default_permission.as_ref()); + + let permissions = map_lit( + quote! { ::std::collections::BTreeMap }, + &self.permissions, + str_lit, + identity, + ); + + let permission_sets = map_lit( + quote! { ::std::collections::BTreeMap }, + &self.permission_sets, + str_lit, + identity, + ); + + let global_scope_schema = + opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit)); + + literal_struct!( + tokens, + ::tauri::utils::acl::plugin::Manifest, + default_permission, + permissions, + permission_sets, + global_scope_schema + ) + } + } +} diff --git a/core/tauri-utils/src/acl/resolved.rs b/core/tauri-utils/src/acl/resolved.rs index 07589e32b..5fdd8c2ab 100644 --- a/core/tauri-utils/src/acl/resolved.rs +++ b/core/tauri-utils/src/acl/resolved.rs @@ -6,6 +6,7 @@ use std::{ collections::{hash_map::DefaultHasher, BTreeMap, HashSet}, + fmt, hash::{Hash, Hasher}, }; @@ -22,15 +23,37 @@ use super::{ /// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`]. pub type ScopeKey = usize; +/// Metadata for what referenced a [`ResolvedCommand`]. +#[cfg(debug_assertions)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct ResolvedCommandReference { + /// Identifier of the capability. + pub capability: String, + /// Identifier of the permission. + pub permission: String, +} + /// A resolved command permission. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default, Clone, PartialEq, Eq)] pub struct ResolvedCommand { - /// The list of window label patterns that is allowed to run this command. + /// The list of capability/permission that referenced this command. + #[cfg(debug_assertions)] + pub referenced_by: Vec, + /// The list of window label patterns that was resolved for this command. pub windows: Vec, /// The reference of the scope that is associated with this command. See [`Resolved#structfield.scopes`]. pub scope: Option, } +impl fmt::Debug for ResolvedCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ResolvedCommand") + .field("windows", &self.windows) + .field("scope", &self.scope) + .finish() + } +} + /// A resolved scope. Merges all scopes defined for a single command. #[derive(Debug, Default)] pub struct ResolvedScope { @@ -51,8 +74,11 @@ pub struct CommandKey { } /// Resolved access control list. -#[derive(Debug)] +#[derive(Default)] pub struct Resolved { + /// ACL plugin manifests. + #[cfg(debug_assertions)] + pub acl: BTreeMap, /// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`]. pub allowed_commands: BTreeMap, /// The commands that are denied. Map each command with its context to a [`ResolvedCommand`]. @@ -63,6 +89,17 @@ pub struct Resolved { pub global_scope: BTreeMap, } +impl fmt::Debug for Resolved { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Resolved") + .field("allowed_commands", &self.allowed_commands) + .field("denied_commands", &self.denied_commands) + .field("command_scope", &self.command_scope) + .field("global_scope", &self.global_scope) + .finish() + } +} + impl Resolved { /// Resolves the ACL for the given plugin permissions and app capabilities. pub fn resolve( @@ -139,6 +176,8 @@ impl Resolved { format!("plugin:{plugin_name}|{allowed_command}"), capability, scope_id, + #[cfg(debug_assertions)] + permission, ); } @@ -148,6 +187,8 @@ impl Resolved { format!("plugin:{plugin_name}|{denied_command}"), capability, scope_id, + #[cfg(debug_assertions)] + permission, ); } } @@ -205,12 +246,16 @@ impl Resolved { .collect(); let resolved = Self { + #[cfg(debug_assertions)] + acl, allowed_commands: allowed_commands .into_iter() .map(|(key, cmd)| { Ok(( key, ResolvedCommand { + #[cfg(debug_assertions)] + referenced_by: cmd.referenced_by, windows: parse_window_patterns(cmd.windows)?, scope: cmd.resolved_scope_key, }, @@ -223,6 +268,8 @@ impl Resolved { Ok(( key, ResolvedCommand { + #[cfg(debug_assertions)] + referenced_by: cmd.referenced_by, windows: parse_window_patterns(cmd.windows)?, scope: cmd.resolved_scope_key, }, @@ -247,6 +294,8 @@ fn parse_window_patterns(windows: HashSet) -> Result, #[derive(Debug, Default)] struct ResolvedCommandTemp { + #[cfg(debug_assertions)] + pub referenced_by: Vec, pub windows: HashSet, pub scope: Vec, pub resolved_scope_key: Option, @@ -257,6 +306,7 @@ fn resolve_command( command: String, capability: &Capability, scope_id: Option, + #[cfg(debug_assertions)] permission: &Permission, ) { let contexts = match &capability.context { CapabilityContext::Local => { @@ -279,6 +329,12 @@ fn resolve_command( }) .or_default(); + #[cfg(debug_assertions)] + resolved.referenced_by.push(ResolvedCommandReference { + capability: capability.identifier.clone(), + permission: permission.identifier.clone(), + }); + resolved.windows.extend(capability.windows.clone()); if let Some(id) = scope_id { resolved.scope.push(id); @@ -347,37 +403,63 @@ mod build { use std::convert::identity; use super::*; - use crate::tokens::*; - - /// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`. - /// - /// All fields must represent a binding of the same name that implements `ToTokens`. - macro_rules! literal_struct { - ($tokens:ident, $struct:ident, $($field:ident),+) => { - $tokens.append_all(quote! { - ::tauri::utils::acl::resolved::$struct { - $($field: #$field),+ - } - }) - }; - } + use crate::{literal_struct, tokens::*}; impl ToTokens for CommandKey { fn to_tokens(&self, tokens: &mut TokenStream) { let name = str_lit(&self.name); let context = &self.context; - literal_struct!(tokens, CommandKey, name, context) + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::CommandKey, + name, + context + ) + } + } + + #[cfg(debug_assertions)] + impl ToTokens for ResolvedCommandReference { + fn to_tokens(&self, tokens: &mut TokenStream) { + let capability = str_lit(&self.capability); + let permission = str_lit(&self.permission); + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::ResolvedCommandReference, + capability, + permission + ) } } impl ToTokens for ResolvedCommand { fn to_tokens(&self, tokens: &mut TokenStream) { + #[cfg(debug_assertions)] + let referenced_by = vec_lit(&self.referenced_by, identity); + let windows = vec_lit(&self.windows, |window| { let w = window.as_str(); quote!(#w.parse().unwrap()) }); let scope = opt_lit(self.scope.as_ref()); - literal_struct!(tokens, ResolvedCommand, windows, scope) + + #[cfg(debug_assertions)] + { + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::ResolvedCommand, + referenced_by, + windows, + scope + ) + } + #[cfg(not(debug_assertions))] + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::ResolvedCommand, + windows, + scope + ) } } @@ -385,12 +467,25 @@ mod build { fn to_tokens(&self, tokens: &mut TokenStream) { let allow = vec_lit(&self.allow, identity); let deny = vec_lit(&self.deny, identity); - literal_struct!(tokens, ResolvedScope, allow, deny) + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::ResolvedScope, + allow, + deny + ) } } impl ToTokens for Resolved { fn to_tokens(&self, tokens: &mut TokenStream) { + #[cfg(debug_assertions)] + let acl = map_lit( + quote! { ::std::collections::BTreeMap }, + &self.acl, + str_lit, + identity, + ); + let allowed_commands = map_lit( quote! { ::std::collections::BTreeMap }, &self.allowed_commands, @@ -419,9 +514,22 @@ mod build { identity, ); + #[cfg(debug_assertions)] + { + literal_struct!( + tokens, + ::tauri::utils::acl::resolved::Resolved, + acl, + allowed_commands, + denied_commands, + command_scope, + global_scope + ) + } + #[cfg(not(debug_assertions))] literal_struct!( tokens, - Resolved, + ::tauri::utils::acl::resolved::Resolved, allowed_commands, denied_commands, command_scope, diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 919bc307b..b51925ee6 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -2141,24 +2141,11 @@ fn default_build() -> BuildConfig { #[cfg(feature = "build")] mod build { use super::*; - use crate::tokens::*; + use crate::{literal_struct, tokens::*}; use proc_macro2::TokenStream; use quote::{quote, ToTokens, TokenStreamExt}; use std::convert::identity; - /// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`. - /// - /// All fields must represent a binding of the same name that implements `ToTokens`. - macro_rules! literal_struct { - ($tokens:ident, $struct:ident, $($field:ident),+) => { - $tokens.append_all(quote! { - ::tauri::utils::config::$struct { - $($field: #$field),+ - } - }) - }; - } - impl ToTokens for WebviewUrl { fn to_tokens(&self, tokens: &mut TokenStream) { let prefix = quote! { ::tauri::utils::config::WebviewUrl }; @@ -2200,7 +2187,14 @@ mod build { let radius = opt_lit(self.radius.as_ref()); let color = opt_lit(self.color.as_ref()); - literal_struct!(tokens, WindowEffectsConfig, effects, state, radius, color) + literal_struct!( + tokens, + ::tauri::utils::config::WindowEffectsConfig, + effects, + state, + radius, + color + ) } } @@ -2309,7 +2303,7 @@ mod build { literal_struct!( tokens, - WindowConfig, + ::tauri::utils::config::WindowConfig, label, url, user_agent, @@ -2415,7 +2409,13 @@ mod build { let pubkey = str_lit(&self.pubkey); let windows = &self.windows; - literal_struct!(tokens, UpdaterConfig, active, pubkey, windows); + literal_struct!( + tokens, + ::tauri::utils::config::UpdaterConfig, + active, + pubkey, + windows + ); } } @@ -2437,7 +2437,7 @@ mod build { let rpm = quote!(Default::default()); let dmg = quote!(Default::default()); let macos = quote!(Default::default()); - let external_bin = opt_vec_str_lit(self.external_bin.as_ref()); + let external_bin = opt_vec_lit(self.external_bin.as_ref(), str_lit); let windows = &self.windows; let ios = quote!(Default::default()); let android = quote!(Default::default()); @@ -2445,7 +2445,7 @@ mod build { literal_struct!( tokens, - BundleConfig, + ::tauri::utils::config::BundleConfig, active, identifier, publisher, @@ -2500,7 +2500,7 @@ mod build { literal_struct!( tokens, - BuildConfig, + ::tauri::utils::config::BuildConfig, runner, dev_path, dist_dir, @@ -2528,7 +2528,11 @@ mod build { impl ToTokens for UpdaterWindowsConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let install_mode = &self.install_mode; - literal_struct!(tokens, UpdaterWindowsConfig, install_mode); + literal_struct!( + tokens, + ::tauri::utils::config::UpdaterWindowsConfig, + install_mode + ); } } @@ -2596,7 +2600,7 @@ mod build { literal_struct!( tokens, - RemoteDomainAccessScope, + ::tauri::utils::config::RemoteDomainAccessScope, scheme, domain, windows, @@ -2615,7 +2619,7 @@ mod build { literal_struct!( tokens, - SecurityConfig, + ::tauri::utils::config::SecurityConfig, csp, dev_csp, freeze_prototype, @@ -2635,7 +2639,7 @@ mod build { let tooltip = opt_str_lit(self.tooltip.as_ref()); literal_struct!( tokens, - TrayIconConfig, + ::tauri::utils::config::TrayIconConfig, id, icon_path, icon_as_template, @@ -2683,7 +2687,7 @@ mod build { literal_struct!( tokens, - TauriConfig, + ::tauri::utils::config::TauriConfig, pattern, windows, bundle, @@ -2711,7 +2715,12 @@ mod build { let product_name = opt_str_lit(self.product_name.as_ref()); let version = opt_str_lit(self.version.as_ref()); - literal_struct!(tokens, PackageConfig, product_name, version); + literal_struct!( + tokens, + ::tauri::utils::config::PackageConfig, + product_name, + version + ); } } @@ -2723,7 +2732,15 @@ mod build { let build = &self.build; let plugins = &self.plugins; - literal_struct!(tokens, Config, schema, package, tauri, build, plugins); + literal_struct!( + tokens, + ::tauri::utils::config::Config, + schema, + package, + tauri, + build, + plugins + ); } } } diff --git a/core/tauri-utils/src/tokens.rs b/core/tauri-utils/src/tokens.rs index 81e9740d8..7c0dcaba1 100644 --- a/core/tauri-utils/src/tokens.rs +++ b/core/tauri-utils/src/tokens.rs @@ -11,6 +11,20 @@ use quote::{quote, ToTokens}; use serde_json::Value as JsonValue; use url::Url; +/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`. +/// +/// All fields must represent a binding of the same name that implements `ToTokens`. +#[macro_export] +macro_rules! literal_struct { + ($tokens:ident, $struct:path, $($field:ident),+) => { + $tokens.append_all(quote! { + $struct { + $($field: #$field),+ + } + }) + }; +} + /// Create a `String` constructor `TokenStream`. /// /// e.g. `"Hello World" -> String::from("Hello World"). @@ -28,14 +42,28 @@ pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream { } } +/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type. +pub fn opt_lit_owned(item: Option) -> TokenStream { + match item { + None => quote! { ::core::option::Option::None }, + Some(item) => quote! { ::core::option::Option::Some(#item) }, + } +} + /// Helper function to combine an `opt_lit` with `str_lit`. pub fn opt_str_lit(item: Option>) -> TokenStream { opt_lit(item.map(str_lit).as_ref()) } /// Helper function to combine an `opt_lit` with a list of `str_lit` -pub fn opt_vec_str_lit(item: Option>>) -> TokenStream { - opt_lit(item.map(|list| vec_lit(list, str_lit)).as_ref()) +pub fn opt_vec_lit( + item: Option>, + map: impl Fn(Raw) -> Tokens, +) -> TokenStream +where + Tokens: ToTokens, +{ + opt_lit(item.map(|list| vec_lit(list, map)).as_ref()) } /// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s. diff --git a/core/tauri/src/command/authority.rs b/core/tauri/src/command/authority.rs index 314655ee5..f3a26fc96 100644 --- a/core/tauri/src/command/authority.rs +++ b/core/tauri/src/command/authority.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::{collections::BTreeMap, ops::Deref}; use serde::de::DeserializeOwned; @@ -21,6 +21,8 @@ use super::{CommandArg, CommandItem}; /// The runtime authority used to authorize IPC execution based on the Access Control List. pub struct RuntimeAuthority { + #[cfg(debug_assertions)] + acl: BTreeMap, allowed_commands: BTreeMap, denied_commands: BTreeMap, pub(crate) scope_manager: ScopeManager, @@ -37,6 +39,15 @@ pub enum Origin { }, } +impl Display for Origin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Local => write!(f, "local"), + Self::Remote { domain } => write!(f, "remote: {domain}"), + } + } +} + impl Origin { fn matches(&self, context: &ExecutionContext) -> bool { match (self, context) { @@ -53,30 +64,160 @@ impl Origin { } impl RuntimeAuthority { - pub(crate) fn new(acl: Resolved) -> Self { - let command_cache = acl + pub(crate) fn new(resolved_acl: Resolved) -> Self { + let command_cache = resolved_acl .command_scope .keys() .map(|key| (*key, ::new())) .collect(); Self { - allowed_commands: acl.allowed_commands, - denied_commands: acl.denied_commands, + #[cfg(debug_assertions)] + acl: resolved_acl.acl, + allowed_commands: resolved_acl.allowed_commands, + denied_commands: resolved_acl.denied_commands, scope_manager: ScopeManager { - command_scope: acl.command_scope, - global_scope: acl.global_scope, + command_scope: resolved_acl.command_scope, + global_scope: resolved_acl.global_scope, command_cache, global_scope_cache: Default::default(), }, } } + #[cfg(debug_assertions)] + pub(crate) fn resolve_access_message( + &self, + plugin: &str, + command_name: &str, + window: &str, + origin: &Origin, + ) -> String { + fn print_references(resolved: &ResolvedCommand) -> String { + resolved + .referenced_by + .iter() + .map(|r| format!("capability: {}, permission: {}", r.capability, r.permission)) + .collect::>() + .join(" || ") + } + + fn has_permissions_allowing_command( + manifest: &crate::utils::acl::plugin::Manifest, + set: &crate::utils::acl::PermissionSet, + command: &str, + ) -> bool { + for permission_id in &set.permissions { + if permission_id == "default" { + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command) { + return true; + } + } + } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) { + if has_permissions_allowing_command(manifest, ref_set, command) { + return true; + } + } else if let Some(permission) = manifest.permissions.get(permission_id) { + if permission.commands.allow.contains(&command.into()) { + return true; + } + } + } + false + } + + let command = format!("plugin:{plugin}|{command_name}"); + if let Some((_cmd, resolved)) = self + .denied_commands + .iter() + .find(|(cmd, _)| cmd.name == command && origin.matches(&cmd.context)) + { + format!( + "{plugin}.{command_name} denied on origin {origin}, referenced by: {}", + print_references(resolved) + ) + } else { + let command_matches = self + .allowed_commands + .iter() + .filter(|(cmd, _)| cmd.name == command) + .collect::>(); + + if let Some((_cmd, resolved)) = command_matches + .iter() + .find(|(cmd, _)| origin.matches(&cmd.context)) + { + if resolved.windows.iter().any(|w| w.matches(window)) { + "allowed".to_string() + } else { + format!("{plugin}.{command_name} not allowed on window {window}, expected one of {}, referenced by {}", resolved.windows.iter().map(|w| w.as_str()).collect::>().join(", "), print_references(resolved)) + } + } else { + let permission_error_detail = if let Some(manifest) = self.acl.get(plugin) { + let mut permissions_referencing_command = Vec::new(); + + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command_name) { + permissions_referencing_command.push("default".into()); + } + } + for set in manifest.permission_sets.values() { + if has_permissions_allowing_command(manifest, set, command_name) { + permissions_referencing_command.push(set.identifier.clone()); + } + } + for permission in manifest.permissions.values() { + if permission.commands.allow.contains(&command_name.into()) { + permissions_referencing_command.push(permission.identifier.clone()); + } + } + + permissions_referencing_command.sort(); + + format!( + "Permissions associated with this command: {}", + permissions_referencing_command + .iter() + .map(|p| format!("{plugin}:{p}")) + .collect::>() + .join(", ") + ) + } else { + "Plugin did not define its manifest".to_string() + }; + + if command_matches.is_empty() { + format!("{plugin}.{command_name} not allowed. {permission_error_detail}") + } else { + format!( + "{plugin}.{command_name} not allowed on origin [{}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}", + origin, + command_matches + .iter() + .map(|(cmd, resolved)| { + let context = match &cmd.context { + ExecutionContext::Local => "[local]".to_string(), + ExecutionContext::Remote { domain } => format!("[remote: {}]", domain.as_str()), + }; + format!( + "- context: {context}, referenced by: {}", + print_references(resolved) + ) + }) + .collect::>() + .join("\n") + ) + } + } + } + } + /// Checks if the given IPC execution is allowed and returns the [`ResolvedCommand`] if it is. pub fn resolve_access( &self, command: &str, window: &str, - origin: Origin, + origin: &Origin, ) -> Option<&ResolvedCommand> { if self .denied_commands @@ -89,8 +230,8 @@ impl RuntimeAuthority { .allowed_commands .iter() .find(|(cmd, _)| cmd.name == command && origin.matches(&cmd.context)) - .map(|(_cmd, allowed)| allowed) - .filter(|allowed| allowed.windows.iter().any(|w| w.matches(window))) + .map(|(_cmd, resolved)| resolved) + .filter(|resolved| resolved.windows.iter().any(|w| w.matches(window))) } } } @@ -329,7 +470,7 @@ mod tests { let resolved_cmd = ResolvedCommand { windows: vec![Pattern::new(window).unwrap()], - scope: None, + ..Default::default() }; let allowed_commands = [(command.clone(), resolved_cmd.clone())] .into_iter() @@ -337,16 +478,14 @@ mod tests { let authority = RuntimeAuthority::new(Resolved { allowed_commands, - denied_commands: Default::default(), - command_scope: Default::default(), - global_scope: Default::default(), + ..Default::default() }); assert_eq!( authority.resolve_access( &command.name, &window.replace('*', "something"), - Origin::Local + &Origin::Local ), Some(&resolved_cmd) ); @@ -366,6 +505,7 @@ mod tests { let resolved_cmd = ResolvedCommand { windows: vec![Pattern::new(window).unwrap()], scope: None, + ..Default::default() }; let allowed_commands = [(command.clone(), resolved_cmd.clone())] .into_iter() @@ -373,16 +513,14 @@ mod tests { let authority = RuntimeAuthority::new(Resolved { allowed_commands, - denied_commands: Default::default(), - command_scope: Default::default(), - global_scope: Default::default(), + ..Default::default() }); assert_eq!( authority.resolve_access( &command.name, window, - Origin::Remote { + &Origin::Remote { domain: domain.into() } ), @@ -404,6 +542,7 @@ mod tests { let resolved_cmd = ResolvedCommand { windows: vec![Pattern::new(window).unwrap()], scope: None, + ..Default::default() }; let allowed_commands = [(command.clone(), resolved_cmd.clone())] .into_iter() @@ -411,16 +550,14 @@ mod tests { let authority = RuntimeAuthority::new(Resolved { allowed_commands, - denied_commands: Default::default(), - command_scope: Default::default(), - global_scope: Default::default(), + ..Default::default() }); assert_eq!( authority.resolve_access( &command.name, window, - Origin::Remote { + &Origin::Remote { domain: domain.replace('*', "studio") } ), @@ -439,6 +576,7 @@ mod tests { let resolved_cmd = ResolvedCommand { windows: vec![Pattern::new(window).unwrap()], scope: None, + ..Default::default() }; let allowed_commands = [(command.clone(), resolved_cmd.clone())] .into_iter() @@ -446,16 +584,14 @@ mod tests { let authority = RuntimeAuthority::new(Resolved { allowed_commands, - denied_commands: Default::default(), - command_scope: Default::default(), - global_scope: Default::default(), + ..Default::default() }); assert!(authority .resolve_access( &command.name, window, - Origin::Remote { + &Origin::Remote { domain: "tauri.app".into() } ) @@ -474,7 +610,7 @@ mod tests { command.clone(), ResolvedCommand { windows: windows.clone(), - scope: None, + ..Default::default() }, )] .into_iter() @@ -483,7 +619,7 @@ mod tests { command.clone(), ResolvedCommand { windows: windows.clone(), - scope: None, + ..Default::default() }, )] .into_iter() @@ -492,12 +628,11 @@ mod tests { let authority = RuntimeAuthority::new(Resolved { allowed_commands, denied_commands, - command_scope: Default::default(), - global_scope: Default::default(), + ..Default::default() }); assert!(authority - .resolve_access(&command.name, window, Origin::Local) + .resolve_access(&command.name, window, &Origin::Local) .is_none()); } } diff --git a/core/tauri/src/test/mod.rs b/core/tauri/src/test/mod.rs index 2d69e2ba7..e48fc88a1 100644 --- a/core/tauri/src/test/mod.rs +++ b/core/tauri/src/test/mod.rs @@ -125,6 +125,8 @@ pub fn mock_context(assets: A) -> crate::Context { _info_plist: (), pattern: Pattern::Brownfield(std::marker::PhantomData), resolved_acl: Resolved { + #[cfg(debug_assertions)] + acl: Default::default(), allowed_commands: Default::default(), denied_commands: Default::default(), command_scope: Default::default(), diff --git a/core/tauri/src/webview/mod.rs b/core/tauri/src/webview/mod.rs index 1e93ef270..d8d44ac47 100644 --- a/core/tauri/src/webview/mod.rs +++ b/core/tauri/src/webview/mod.rs @@ -1093,22 +1093,19 @@ fn main() { request.headers, ); + let acl_origin = if is_local { + Origin::Local + } else { + Origin::Remote { + domain: current_url + .domain() + .map(|d| d.to_string()) + .unwrap_or_default(), + } + }; let resolved_acl = manager .runtime_authority - .resolve_access( - &request.cmd, - &message.webview.webview.label, - if is_local { - Origin::Local - } else { - Origin::Remote { - domain: current_url - .domain() - .map(|d| d.to_string()) - .unwrap_or_default(), - } - }, - ) + .resolve_access(&request.cmd, &message.webview.webview.label, &acl_origin) .cloned(); let mut invoke = Invoke { @@ -1117,20 +1114,33 @@ fn main() { acl: resolved_acl, }; - if request.cmd.starts_with("plugin:") { + if let Some((plugin, command_name)) = request.cmd.strip_prefix("plugin:").map(|raw_command| { + let mut tokens = raw_command.split('|'); + // safe to unwrap: split always has a least one item + let plugin = tokens.next().unwrap(); + let command = tokens.next().map(|c| c.to_string()).unwrap_or_default(); + (plugin, command) + }) { if request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND && invoke.acl.is_none() { - invoke.resolver.reject("NOT ALLOWED"); + #[cfg(debug_assertions)] + { + invoke + .resolver + .reject(manager.runtime_authority.resolve_access_message( + plugin, + &command_name, + &invoke.message.webview.webview.label, + &acl_origin, + )); + } + #[cfg(not(debug_assertions))] + invoke + .resolver + .reject(format!("Command {} not allowed by ACL", request.cmd)); return; } - let command = invoke.message.command.replace("plugin:", ""); - let mut tokens = command.split('|'); - // safe to unwrap: split always has a least one item - let plugin = tokens.next().unwrap(); - invoke.message.command = tokens - .next() - .map(|c| c.to_string()) - .unwrap_or_else(String::new); + invoke.message.command = command_name; let command = invoke.message.command.clone();