Browse Source

feat(ipc): improve error message when plugin command is not allowed (#8681)

* feat(ipc): improve error message when plugin command is not allowed

* reuse literal_struct macro
Lucas Fernandes Nogueira 1 year ago
parent
commit
9af90ca7f3

+ 57 - 0
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
+      )
+    }
+  }
 }

+ 60 - 3
core/tauri-utils/src/acl/plugin.rs

@@ -36,9 +36,6 @@ pub struct PermissionFile {
   #[serde(default)]
   pub set: Vec<PermissionSet>,
 
-  /// Test something!!
-  pub test: Option<PermissionSet>,
-
   /// A list of inlined permissions
   #[serde(default)]
   pub permission: Vec<Permission>,
@@ -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
+      )
+    }
+  }
+}

+ 129 - 21
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<ResolvedCommandReference>,
+  /// The list of window label patterns that was resolved for this command.
   pub windows: Vec<glob::Pattern>,
   /// The reference of the scope that is associated with this command. See [`Resolved#structfield.scopes`].
   pub scope: Option<ScopeKey>,
 }
 
+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<String, Manifest>,
   /// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`].
   pub allowed_commands: BTreeMap<CommandKey, ResolvedCommand>,
   /// 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<String, ResolvedScope>,
 }
 
+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<String>) -> Result<Vec<glob::Pattern>,
 
 #[derive(Debug, Default)]
 struct ResolvedCommandTemp {
+  #[cfg(debug_assertions)]
+  pub referenced_by: Vec<ResolvedCommandReference>,
   pub windows: HashSet<String>,
   pub scope: Vec<usize>,
   pub resolved_scope_key: Option<usize>,
@@ -257,6 +306,7 @@ fn resolve_command(
   command: String,
   capability: &Capability,
   scope_id: Option<usize>,
+  #[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,

+ 44 - 27
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
+      );
     }
   }
 }

+ 30 - 2
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<impl ToTokens>) -> 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<impl AsRef<str>>) -> 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<impl IntoIterator<Item = impl AsRef<str>>>) -> TokenStream {
-  opt_lit(item.map(|list| vec_lit(list, str_lit)).as_ref())
+pub fn opt_vec_lit<Raw, Tokens>(
+  item: Option<impl IntoIterator<Item = Raw>>,
+  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.

+ 167 - 32
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<String, crate::utils::acl::plugin::Manifest>,
   allowed_commands: BTreeMap<CommandKey, ResolvedCommand>,
   denied_commands: BTreeMap<CommandKey, ResolvedCommand>,
   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, <TypeMap![Send + Sync]>::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::<Vec<_>>()
+        .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::<BTreeMap<_, _>>();
+
+      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::<Vec<_>>().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::<Vec<_>>()
+              .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::<Vec<_>>()
+              .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());
   }
 }

+ 2 - 0
core/tauri/src/test/mod.rs

@@ -125,6 +125,8 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
     _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(),

+ 34 - 24
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();