瀏覽代碼

feat(codegen): allow defining additional capabilities, closes #8798 (#8802)

* refactor(core): capabilities must be referenced on the Tauri config file

* add all capabilities by default

* feat(codegen): allow defining additional capabilities, closes #8798

* undo example

* lint

* move add_capability to runtime authority

* add change files

* go through code review

* fix tests

* remove tokens option
Lucas Fernandes Nogueira 1 年之前
父節點
當前提交
8d16a80d2f

+ 6 - 0
.changes/codegen-capabilities-attribute.md

@@ -0,0 +1,6 @@
+---
+"tauri-macros": patch:enhance
+"tauri-codegen": patch:enhance
+---
+
+The `generate_context` proc macro now accepts a `capabilities` attribute where the value is an array of file paths that can be [conditionally compiled](https://doc.rust-lang.org/reference/conditional-compilation.html). These capabilities are added to the application along the capabilities defined in the Tauri configuration file.

+ 5 - 0
.changes/context-runtime-authority.md

@@ -0,0 +1,5 @@
+---
+"tauri-utils": patch:enhance
+---
+
+The `Context` struct now includes the runtime authority instead of the resolved ACL. This does not impact most applications.

+ 5 - 0
.changes/tauri-build-codegen-capabilities.md

@@ -0,0 +1,5 @@
+---
+"tauri-build": patch:enhance
+---
+
+Added `CodegenContext::capability` to include a capability file dynamically.

+ 5 - 0
.changes/tauri-utils-capability-refactor.md

@@ -0,0 +1,5 @@
+---
+"tauri-utils": patch:enhance
+---
+
+Refactored the capability types and resolution algorithm.

+ 1 - 0
Cargo.lock

@@ -4149,6 +4149,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
+ "syn 2.0.49",
  "tauri-utils",
  "thiserror",
  "time",

+ 4 - 1
core/tauri-build/src/acl.rs

@@ -17,7 +17,10 @@ use schemars::{
   schema_for,
 };
 use tauri_utils::{
-  acl::{build::CapabilityFile, capability::Capability, plugin::Manifest},
+  acl::{
+    capability::{Capability, CapabilityFile},
+    plugin::Manifest,
+  },
   platform::Target,
 };
 

+ 14 - 1
core/tauri-build/src/codegen/context.rs

@@ -7,7 +7,7 @@ use std::{
   env::var,
   fs::{create_dir_all, File},
   io::{BufWriter, Write},
-  path::PathBuf,
+  path::{Path, PathBuf},
 };
 use tauri_codegen::{context_codegen, ContextData};
 use tauri_utils::config::FrontendDist;
@@ -20,6 +20,7 @@ pub struct CodegenContext {
   dev: bool,
   config_path: PathBuf,
   out_file: PathBuf,
+  capabilities: Option<Vec<PathBuf>>,
 }
 
 impl Default for CodegenContext {
@@ -28,6 +29,7 @@ impl Default for CodegenContext {
       dev: false,
       config_path: PathBuf::from("tauri.conf.json"),
       out_file: PathBuf::from("tauri-build-context.rs"),
+      capabilities: None,
     }
   }
 }
@@ -74,6 +76,16 @@ impl CodegenContext {
     self
   }
 
+  /// Adds a capability file to the generated context.
+  #[must_use]
+  pub fn capability<P: AsRef<Path>>(mut self, path: P) -> Self {
+    self
+      .capabilities
+      .get_or_insert_with(Default::default)
+      .push(path.as_ref().to_path_buf());
+    self
+  }
+
   /// Generate the code and write it to the output file - returning the path it was saved to.
   ///
   /// Unless you are doing something special with this builder, you don't need to do anything with
@@ -125,6 +137,7 @@ impl CodegenContext {
       // it's very hard to have a build script for unit tests, so assume this is always called from
       // outside the tauri crate, making the ::tauri root valid.
       root: quote::quote!(::tauri),
+      capabilities: self.capabilities,
     })?;
 
     // get the full output file path

+ 2 - 1
core/tauri-codegen/Cargo.toml

@@ -17,7 +17,8 @@ sha2 = "0.10"
 base64 = "0.21"
 proc-macro2 = "1"
 quote = "1"
-serde = { version = "1", features = ["derive"] }
+syn = "2"
+serde = { version = "1", features = [ "derive" ] }
 serde_json = "1"
 tauri-utils = { version = "2.0.0-beta.1", path = "../tauri-utils", features = [ "build" ] }
 thiserror = "1"

+ 38 - 4
core/tauri-codegen/src/context.rs

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: MIT
 
 use std::collections::BTreeMap;
+use std::convert::identity;
 use std::path::{Path, PathBuf};
 use std::{ffi::OsStr, str::FromStr};
 
@@ -11,7 +12,7 @@ use proc_macro2::TokenStream;
 use quote::quote;
 use sha2::{Digest, Sha256};
 
-use tauri_utils::acl::capability::Capability;
+use tauri_utils::acl::capability::{Capability, CapabilityFile};
 use tauri_utils::acl::plugin::Manifest;
 use tauri_utils::acl::resolved::Resolved;
 use tauri_utils::assets::AssetKey;
@@ -20,6 +21,7 @@ use tauri_utils::html::{
   inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
 };
 use tauri_utils::platform::Target;
+use tauri_utils::tokens::{map_lit, str_lit};
 
 use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
 
@@ -32,6 +34,8 @@ pub struct ContextData {
   pub config: Config,
   pub config_parent: PathBuf,
   pub root: TokenStream,
+  /// Additional capabilities to include.
+  pub capabilities: Option<Vec<PathBuf>>,
 }
 
 fn map_core_assets(
@@ -126,6 +130,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     config,
     config_parent,
     root,
+    capabilities: additional_capabilities,
   } = data;
 
   let target = std::env::var("TARGET")
@@ -390,7 +395,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     Default::default()
   };
 
-  let capabilities = if config.app.security.capabilities.is_empty() {
+  let mut capabilities = if config.app.security.capabilities.is_empty() {
     capabilities_from_files
   } else {
     let mut capabilities = BTreeMap::new();
@@ -410,7 +415,36 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     capabilities
   };
 
-  let resolved_acl = Resolved::resolve(acl, capabilities, target).expect("failed to resolve ACL");
+  let acl_tokens = map_lit(
+    quote! { ::std::collections::BTreeMap },
+    &acl,
+    str_lit,
+    identity,
+  );
+
+  if let Some(paths) = additional_capabilities {
+    for path in paths {
+      let capability = CapabilityFile::load(&path)
+        .unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
+      match capability {
+        CapabilityFile::Capability(c) => {
+          capabilities.insert(c.identifier.clone(), c);
+        }
+        CapabilityFile::List {
+          capabilities: capabilities_list,
+        } => {
+          capabilities.extend(
+            capabilities_list
+              .into_iter()
+              .map(|c| (c.identifier.clone(), c)),
+          );
+        }
+      }
+    }
+  }
+
+  let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
+  let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
 
   Ok(quote!({
     #[allow(unused_mut, clippy::let_and_return)]
@@ -422,7 +456,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
       #package_info,
       #info_plist,
       #pattern,
-      #resolved_acl
+      #runtime_authority
     );
     #with_tray_icon_code
     context

+ 81 - 38
core/tauri-macros/src/context.rs

@@ -4,11 +4,11 @@
 
 use proc_macro2::{Ident, Span, TokenStream};
 use quote::{quote, ToTokens};
-use std::{env::VarError, path::PathBuf};
+use std::path::PathBuf;
 use syn::{
   parse::{Parse, ParseBuffer},
   punctuated::Punctuated,
-  LitStr, PathArguments, PathSegment, Token,
+  Expr, ExprLit, Lit, LitStr, Meta, PathArguments, PathSegment, Token,
 };
 use tauri_codegen::{context_codegen, get_config, ContextData};
 use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Target};
@@ -16,6 +16,7 @@ use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Targe
 pub(crate) struct ContextItems {
   config_file: PathBuf,
   root: syn::Path,
+  capabilities: Option<Vec<PathBuf>>,
 }
 
 impl Parse for ContextItems {
@@ -26,51 +27,92 @@ impl Parse for ContextItems {
       .map(Target::from_triple)
       .unwrap_or_else(|_| Target::current());
 
-    let config_file = if input.is_empty() {
-      std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join("tauri.conf.json"))
-    } else {
-      let raw: LitStr = input.parse()?;
+    let mut root = None;
+    let mut capabilities = None;
+    let config_file = input.parse::<LitStr>().ok().map(|raw| {
+      let _ = input.parse::<Token![,]>();
       let path = PathBuf::from(raw.value());
       if path.is_relative() {
-        std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join(path))
+        std::env::var("CARGO_MANIFEST_DIR")
+          .map(|m| PathBuf::from(m).join(path))
+          .map_err(|e| e.to_string())
       } else {
         Ok(path)
       }
-    }
-    .map_err(|error| match error {
-      VarError::NotPresent => "no CARGO_MANIFEST_DIR env var, this should be set by cargo".into(),
-      VarError::NotUnicode(_) => "CARGO_MANIFEST_DIR env var contained invalid utf8".into(),
-    })
-    .and_then(|path| {
-      if does_supported_file_name_exist(target, &path) {
-        Ok(path)
-      } else {
-        Err(format!(
-          "no file at path {} exists, expected tauri config file",
-          path.display()
-        ))
-      }
-    })
-    .map_err(|e| input.error(e))?;
+      .and_then(|path| {
+        if does_supported_file_name_exist(target, &path) {
+          Ok(path)
+        } else {
+          Err(format!(
+            "no file at path {} exists, expected tauri config file",
+            path.display()
+          ))
+        }
+      })
+    });
 
-    let context_path = if input.is_empty() {
-      let mut segments = Punctuated::new();
-      segments.push(PathSegment {
-        ident: Ident::new("tauri", Span::call_site()),
-        arguments: PathArguments::None,
-      });
-      syn::Path {
-        leading_colon: Some(Token![::](Span::call_site())),
-        segments,
+    while let Ok(meta) = input.parse::<Meta>() {
+      match meta {
+        Meta::Path(p) => {
+          root.replace(p);
+        }
+        Meta::NameValue(v) => {
+          if *v.path.require_ident()? == "capabilities" {
+            if let Expr::Array(array) = v.value {
+              capabilities.replace(
+                array
+                  .elems
+                  .into_iter()
+                  .map(|e| {
+                    if let Expr::Lit(ExprLit {
+                      attrs: _,
+                      lit: Lit::Str(s),
+                    }) = e
+                    {
+                      Ok(s.value().into())
+                    } else {
+                      Err(syn::Error::new(
+                        input.span(),
+                        "unexpected expression for capability",
+                      ))
+                    }
+                  })
+                  .collect::<Result<Vec<_>, syn::Error>>()?,
+              );
+            } else {
+              return Err(syn::Error::new(
+                input.span(),
+                "unexpected value for capabilities",
+              ));
+            }
+          }
+        }
+        Meta::List(_) => {
+          return Err(syn::Error::new(input.span(), "unexpected list input"));
+        }
       }
-    } else {
-      let _: Token![,] = input.parse()?;
-      input.call(syn::Path::parse_mod_style)?
-    };
+    }
 
     Ok(Self {
-      config_file,
-      root: context_path,
+      config_file: config_file
+        .unwrap_or_else(|| {
+          std::env::var("CARGO_MANIFEST_DIR")
+            .map(|m| PathBuf::from(m).join("tauri.conf.json"))
+            .map_err(|e| e.to_string())
+        })
+        .map_err(|e| input.error(e))?,
+      root: root.unwrap_or_else(|| {
+        let mut segments = Punctuated::new();
+        segments.push(PathSegment {
+          ident: Ident::new("tauri", Span::call_site()),
+          arguments: PathArguments::None,
+        });
+        syn::Path {
+          leading_colon: Some(Token![::](Span::call_site())),
+          segments,
+        }
+      }),
+      capabilities,
     })
   }
 }
@@ -83,6 +125,7 @@ pub(crate) fn generate_context(context: ContextItems) -> TokenStream {
       config,
       config_parent,
       root: context.root.to_token_stream(),
+      capabilities: context.capabilities,
     })
     .and_then(|data| context_codegen(data).map_err(|e| e.to_string()));
 

+ 5 - 24
core/tauri-utils/src/acl/build.rs

@@ -16,9 +16,11 @@ use schemars::{
   schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation},
   schema_for,
 };
-use serde::Deserialize;
 
-use super::{capability::Capability, plugin::PermissionFile};
+use super::{
+  capability::{Capability, CapabilityFile},
+  plugin::PermissionFile,
+};
 
 /// Known name of the folder containing autogenerated permissions.
 pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
@@ -49,19 +51,6 @@ const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
 
 const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
 
-/// Capability formats accepted in a capability file.
-#[derive(Deserialize, schemars::JsonSchema)]
-#[serde(untagged)]
-pub enum CapabilityFile {
-  /// A single capability.
-  Capability(Capability),
-  /// A list of capabilities.
-  List {
-    /// The list of capabilities.
-    capabilities: Vec<Capability>,
-  },
-}
-
 /// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
 pub fn define_permissions(
   pattern: &str,
@@ -143,15 +132,7 @@ pub fn parse_capabilities(
     // TODO: remove this before stable
     .filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
   {
-    let capability_file = std::fs::read_to_string(&path).map_err(Error::ReadFile)?;
-    let ext = path.extension().unwrap().to_string_lossy().to_string();
-    let capability: CapabilityFile = match ext.as_str() {
-      "toml" => toml::from_str(&capability_file)?,
-      "json" => serde_json::from_str(&capability_file)?,
-      _ => return Err(Error::UnknownCapabilityFormat(ext)),
-    };
-
-    match capability {
+    match CapabilityFile::load(&path)? {
       CapabilityFile::Capability(capability) => {
         capabilities_map.insert(capability.identifier.clone(), capability);
       }

+ 43 - 0
core/tauri-utils/src/acl/capability.rs

@@ -4,6 +4,8 @@
 
 //! End-user abstraction for selecting permissions a window has access to.
 
+use std::{path::Path, str::FromStr};
+
 use crate::{acl::Identifier, platform::Target};
 use serde::{Deserialize, Serialize};
 
@@ -101,6 +103,47 @@ pub enum CapabilityContext {
   },
 }
 
+/// Capability formats accepted in a capability file.
+#[derive(Deserialize)]
+#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+#[serde(untagged)]
+pub enum CapabilityFile {
+  /// A single capability.
+  Capability(Capability),
+  /// A list of capabilities.
+  List {
+    /// The list of capabilities.
+    capabilities: Vec<Capability>,
+  },
+}
+
+impl CapabilityFile {
+  /// Load the given capability file.
+  pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
+    let path = path.as_ref();
+    let capability_file = std::fs::read_to_string(path).map_err(super::Error::ReadFile)?;
+    let ext = path.extension().unwrap().to_string_lossy().to_string();
+    let file: Self = match ext.as_str() {
+      "toml" => toml::from_str(&capability_file)?,
+      "json" => serde_json::from_str(&capability_file)?,
+      _ => return Err(super::Error::UnknownCapabilityFormat(ext)),
+    };
+    Ok(file)
+  }
+}
+
+impl FromStr for CapabilityFile {
+  type Err = super::Error;
+
+  fn from_str(s: &str) -> Result<Self, Self::Err> {
+    match s.chars().next() {
+      Some('[') => toml::from_str(s).map_err(Into::into),
+      Some('{') => serde_json::from_str(s).map_err(Into::into),
+      _ => Err(super::Error::UnknownCapabilityFormat(s.into())),
+    }
+  }
+}
+
 #[cfg(feature = "build")]
 mod build {
   use std::convert::identity;

+ 87 - 96
core/tauri-utils/src/acl/resolved.rs

@@ -77,11 +77,8 @@ pub struct CommandKey {
 }
 
 /// Resolved access control list.
-#[derive(Default)]
+#[derive(Debug, 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`].
@@ -92,21 +89,10 @@ 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(
-    acl: BTreeMap<String, Manifest>,
+    acl: &BTreeMap<String, Manifest>,
     capabilities: BTreeMap<String, Capability>,
     target: Target,
   ) -> Result<Self, Error> {
@@ -123,67 +109,25 @@ impl Resolved {
         continue;
       }
 
-      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)?;
-
-          let mut resolved_scope = Scopes::default();
-          let mut commands = Commands::default();
-
-          if let PermissionEntry::ExtendedPermission {
-            identifier: _,
-            scope,
-          } = permission_entry
-          {
-            if let Some(allow) = scope.allow.clone() {
-              resolved_scope
-                .allow
-                .get_or_insert_with(Default::default)
-                .extend(allow);
-            }
-            if let Some(deny) = scope.deny.clone() {
-              resolved_scope
-                .deny
-                .get_or_insert_with(Default::default)
-                .extend(deny);
-            }
-          }
-
-          for permission in permissions {
-            if let Some(allow) = permission.scope.allow.clone() {
-              resolved_scope
-                .allow
-                .get_or_insert_with(Default::default)
-                .extend(allow);
-            }
-            if let Some(deny) = permission.scope.deny.clone() {
-              resolved_scope
-                .deny
-                .get_or_insert_with(Default::default)
-                .extend(deny);
-            }
-
-            commands.allow.extend(permission.commands.allow.clone());
-            commands.deny.extend(permission.commands.deny.clone());
-          }
-
+      with_resolved_permissions(
+        capability,
+        acl,
+        |ResolvedPermission {
+           plugin_name,
+           permission_name,
+           commands,
+           scope,
+         }| {
           if commands.allow.is_empty() && commands.deny.is_empty() {
             // global scope
             global_scope
               .entry(plugin_name.to_string())
               .or_default()
-              .push(resolved_scope);
+              .push(scope);
           } else {
-            let has_scope = resolved_scope.allow.is_some() || resolved_scope.deny.is_some();
-            if has_scope {
+            let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
               current_scope_id += 1;
-              command_scopes.insert(current_scope_id, resolved_scope);
-            }
-
-            let scope_id = if has_scope {
+              command_scopes.insert(current_scope_id, scope);
               Some(current_scope_id)
             } else {
               None
@@ -211,8 +155,8 @@ impl Resolved {
               );
             }
           }
-        }
-      }
+        },
+      )?;
     }
 
     // resolve scopes
@@ -264,8 +208,6 @@ impl Resolved {
       .collect();
 
     let resolved = Self {
-      #[cfg(debug_assertions)]
-      acl,
       allowed_commands: allowed_commands
         .into_iter()
         .map(|(key, cmd)| {
@@ -316,6 +258,77 @@ fn parse_glob_patterns(raw: HashSet<String>) -> Result<Vec<glob::Pattern>, Error
   Ok(patterns)
 }
 
+struct ResolvedPermission<'a> {
+  plugin_name: &'a str,
+  permission_name: &'a str,
+  commands: Commands,
+  scope: Scopes,
+}
+
+fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>)>(
+  capability: &Capability,
+  acl: &BTreeMap<String, Manifest>,
+  mut f: F,
+) -> Result<(), Error> {
+  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)?;
+
+      let mut resolved_scope = Scopes::default();
+      let mut commands = Commands::default();
+
+      if let PermissionEntry::ExtendedPermission {
+        identifier: _,
+        scope,
+      } = permission_entry
+      {
+        if let Some(allow) = scope.allow.clone() {
+          resolved_scope
+            .allow
+            .get_or_insert_with(Default::default)
+            .extend(allow);
+        }
+        if let Some(deny) = scope.deny.clone() {
+          resolved_scope
+            .deny
+            .get_or_insert_with(Default::default)
+            .extend(deny);
+        }
+      }
+
+      for permission in permissions {
+        if let Some(allow) = permission.scope.allow.clone() {
+          resolved_scope
+            .allow
+            .get_or_insert_with(Default::default)
+            .extend(allow);
+        }
+        if let Some(deny) = permission.scope.deny.clone() {
+          resolved_scope
+            .deny
+            .get_or_insert_with(Default::default)
+            .extend(deny);
+        }
+
+        commands.allow.extend(permission.commands.allow.clone());
+        commands.deny.extend(permission.commands.deny.clone());
+      }
+
+      f(ResolvedPermission {
+        plugin_name,
+        permission_name,
+        commands,
+        scope: resolved_scope,
+      });
+    }
+  }
+
+  Ok(())
+}
+
 #[derive(Debug, Default)]
 struct ResolvedCommandTemp {
   #[cfg(debug_assertions)]
@@ -325,7 +338,6 @@ struct ResolvedCommandTemp {
   pub scope: Vec<ScopeKey>,
   pub resolved_scope_key: Option<ScopeKey>,
 }
-
 fn resolve_command(
   commands: &mut BTreeMap<CommandKey, ResolvedCommandTemp>,
   command: String,
@@ -511,14 +523,6 @@ mod build {
 
   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,
@@ -547,19 +551,6 @@ 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,
         ::tauri::utils::acl::resolved::Resolved,

+ 135 - 29
core/tauri/src/ipc/authority.rs

@@ -8,6 +8,8 @@ use std::{collections::BTreeMap, ops::Deref};
 use serde::de::DeserializeOwned;
 use state::TypeMap;
 
+use tauri_utils::acl::capability::CapabilityFile;
+use tauri_utils::acl::plugin::Manifest;
 use tauri_utils::acl::Value;
 use tauri_utils::acl::{
   resolved::{CommandKey, Resolved, ResolvedCommand, ResolvedScope, ScopeKey},
@@ -21,7 +23,6 @@ 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>,
@@ -64,15 +65,15 @@ impl Origin {
 }
 
 impl RuntimeAuthority {
-  pub(crate) fn new(resolved_acl: Resolved) -> Self {
+  #[doc(hidden)]
+  pub fn new(acl: BTreeMap<String, Manifest>, resolved_acl: Resolved) -> Self {
     let command_cache = resolved_acl
       .command_scope
       .keys()
       .map(|key| (*key, <TypeMap![Send + Sync]>::new()))
       .collect();
     Self {
-      #[cfg(debug_assertions)]
-      acl: resolved_acl.acl,
+      acl,
       allowed_commands: resolved_acl.allowed_commands,
       denied_commands: resolved_acl.denied_commands,
       scope_manager: ScopeManager {
@@ -84,6 +85,93 @@ impl RuntimeAuthority {
     }
   }
 
+  #[doc(hidden)]
+  pub fn __allow_command(&mut self, command: String, context: ExecutionContext) {
+    self.allowed_commands.insert(
+      CommandKey {
+        name: command,
+        context,
+      },
+      ResolvedCommand {
+        windows: vec!["*".parse().unwrap()],
+        ..Default::default()
+      },
+    );
+  }
+
+  /// Adds the given capability to the runtime authority.
+  pub fn add_capability(&mut self, capability: CapabilityFile) -> crate::Result<()> {
+    let mut capabilities = BTreeMap::new();
+    match capability {
+      CapabilityFile::Capability(c) => {
+        capabilities.insert(c.identifier.clone(), c);
+      }
+      CapabilityFile::List {
+        capabilities: capabilities_list,
+      } => {
+        capabilities.extend(
+          capabilities_list
+            .into_iter()
+            .map(|c| (c.identifier.clone(), c)),
+        );
+      }
+    }
+
+    let resolved = Resolved::resolve(
+      &self.acl,
+      capabilities,
+      tauri_utils::platform::Target::current(),
+    )
+    .unwrap();
+
+    // fill global scope
+    for (plugin, global_scope) in resolved.global_scope {
+      let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default();
+
+      global_scope_entry.allow.extend(global_scope.allow);
+      global_scope_entry.deny.extend(global_scope.deny);
+
+      self.scope_manager.global_scope_cache = Default::default();
+    }
+
+    // denied commands
+    for (cmd_key, resolved_cmd) in resolved.denied_commands {
+      let entry = self.denied_commands.entry(cmd_key).or_default();
+
+      entry.windows.extend(resolved_cmd.windows);
+      #[cfg(debug_assertions)]
+      entry.referenced_by.extend(resolved_cmd.referenced_by);
+    }
+
+    // allowed commands
+    for (cmd_key, resolved_cmd) in resolved.allowed_commands {
+      let entry = self.allowed_commands.entry(cmd_key).or_default();
+
+      entry.windows.extend(resolved_cmd.windows);
+      #[cfg(debug_assertions)]
+      entry.referenced_by.extend(resolved_cmd.referenced_by);
+
+      // fill command scope
+      if let Some(scope_id) = resolved_cmd.scope {
+        let command_scope = resolved.command_scope.get(&scope_id).unwrap();
+
+        let command_scope_entry = self
+          .scope_manager
+          .command_scope
+          .entry(scope_id)
+          .or_default();
+        command_scope_entry
+          .allow
+          .extend(command_scope.allow.clone());
+        command_scope_entry.deny.extend(command_scope.deny.clone());
+
+        self.scope_manager.command_cache.remove(&scope_id);
+      }
+    }
+
+    Ok(())
+  }
+
   #[cfg(debug_assertions)]
   pub(crate) fn resolve_access_message(
     &self,
@@ -488,10 +576,13 @@ mod tests {
       .into_iter()
       .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        ..Default::default()
+      },
+    );
 
     assert_eq!(
       authority.resolve_access(
@@ -522,10 +613,13 @@ mod tests {
       .into_iter()
       .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        ..Default::default()
+      },
+    );
 
     assert_eq!(
       authority.resolve_access(
@@ -559,10 +653,13 @@ mod tests {
       .into_iter()
       .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        ..Default::default()
+      },
+    );
 
     assert_eq!(
       authority.resolve_access(
@@ -598,10 +695,13 @@ mod tests {
       .into_iter()
       .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        ..Default::default()
+      },
+    );
 
     assert_eq!(
       authority.resolve_access(
@@ -634,10 +734,13 @@ mod tests {
       .into_iter()
       .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        ..Default::default()
+      },
+    );
 
     assert!(authority
       .resolve_access(
@@ -679,11 +782,14 @@ mod tests {
     .into_iter()
     .collect();
 
-    let authority = RuntimeAuthority::new(Resolved {
-      allowed_commands,
-      denied_commands,
-      ..Default::default()
-    });
+    let authority = RuntimeAuthority::new(
+      Default::default(),
+      Resolved {
+        allowed_commands,
+        denied_commands,
+        ..Default::default()
+      },
+    );
 
     assert!(authority
       .resolve_access(&command.name, window, webview, &Origin::Local)

+ 6 - 6
core/tauri/src/lib.rs

@@ -69,6 +69,7 @@ pub use cocoa;
 #[doc(hidden)]
 pub use embed_plist;
 pub use error::{Error, Result};
+use ipc::RuntimeAuthority;
 pub use resources::{Resource, ResourceId, ResourceTable};
 #[cfg(target_os = "ios")]
 #[doc(hidden)]
@@ -193,7 +194,6 @@ use std::{
   fmt::{self, Debug},
   sync::MutexGuard,
 };
-use utils::acl::resolved::Resolved;
 
 #[cfg(feature = "wry")]
 #[cfg_attr(docsrs, doc(cfg(feature = "wry")))]
@@ -432,7 +432,7 @@ pub struct Context<A: Assets> {
   pub(crate) package_info: PackageInfo,
   pub(crate) _info_plist: (),
   pub(crate) pattern: Pattern,
-  pub(crate) resolved_acl: Resolved,
+  pub(crate) runtime_authority: RuntimeAuthority,
 }
 
 impl<A: Assets> fmt::Debug for Context<A> {
@@ -529,8 +529,8 @@ impl<A: Assets> Context<A> {
   /// This API is unstable.
   #[doc(hidden)]
   #[inline(always)]
-  pub fn resolved_acl(&mut self) -> &mut Resolved {
-    &mut self.resolved_acl
+  pub fn runtime_authority_mut(&mut self) -> &mut RuntimeAuthority {
+    &mut self.runtime_authority
   }
 
   /// Create a new [`Context`] from the minimal required items.
@@ -544,7 +544,7 @@ impl<A: Assets> Context<A> {
     package_info: PackageInfo,
     info_plist: (),
     pattern: Pattern,
-    resolved_acl: Resolved,
+    runtime_authority: RuntimeAuthority,
   ) -> Self {
     Self {
       config,
@@ -556,7 +556,7 @@ impl<A: Assets> Context<A> {
       package_info,
       _info_plist: info_plist,
       pattern,
-      resolved_acl,
+      runtime_authority,
     }
   }
 

+ 1 - 1
core/tauri/src/manager/mod.rs

@@ -245,7 +245,7 @@ impl<R: Runtime> AppManager<R> {
     }
 
     Self {
-      runtime_authority: RuntimeAuthority::new(context.resolved_acl),
+      runtime_authority: context.runtime_authority,
       window: window::WindowManager {
         windows: Mutex::default(),
         default_icon: context.default_window_icon,

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

@@ -55,7 +55,7 @@ use serde::Serialize;
 use std::{borrow::Cow, collections::HashMap, fmt::Debug};
 
 use crate::{
-  ipc::{InvokeBody, InvokeError, InvokeResponse},
+  ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority},
   webview::InvokeRequest,
   App, Builder, Context, Pattern, Webview,
 };
@@ -126,14 +126,7 @@ 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(),
-      global_scope: Default::default(),
-    },
+    runtime_authority: RuntimeAuthority::new(Default::default(), Resolved::default()),
   }
 }
 

+ 1 - 1
core/tests/acl/src/lib.rs

@@ -57,7 +57,7 @@ mod tests {
       let capabilities = parse_capabilities(&format!("{}/cap*", fixture_entry.path().display()))
         .expect("failed to parse capabilities");
 
-      let resolved = Resolved::resolve(manifests, capabilities, Target::current())
+      let resolved = Resolved::resolve(&manifests, capabilities, Target::current())
         .expect("failed to resolve ACL");
 
       insta::assert_debug_snapshot!(

+ 1 - 0
examples/api/src-tauri/Cargo.lock

@@ -3767,6 +3767,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
+ "syn 2.0.48",
  "tauri-utils",
  "thiserror",
  "time",

+ 4 - 14
examples/multiwindow/main.rs

@@ -5,10 +5,7 @@
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use tauri::{webview::PageLoadEvent, WebviewWindowBuilder};
-use tauri_utils::acl::{
-  resolved::{CommandKey, ResolvedCommand},
-  ExecutionContext,
-};
+use tauri_utils::acl::ExecutionContext;
 
 fn main() {
   let mut context = tauri::generate_context!("../../examples/multiwindow/tauri.conf.json");
@@ -17,16 +14,9 @@ fn main() {
     "plugin:event|emit",
     "plugin:event|emit_to",
   ] {
-    context.resolved_acl().allowed_commands.insert(
-      CommandKey {
-        name: cmd.into(),
-        context: ExecutionContext::Local,
-      },
-      ResolvedCommand {
-        windows: vec!["*".parse().unwrap()],
-        ..Default::default()
-      },
-    );
+    context
+      .runtime_authority_mut()
+      .__allow_command(cmd.to_string(), ExecutionContext::Local);
   }
 
   tauri::Builder::default()

+ 4 - 14
examples/parent-window/main.rs

@@ -5,10 +5,7 @@
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 use tauri::{webview::PageLoadEvent, WebviewUrl, WebviewWindowBuilder};
-use tauri_utils::acl::{
-  resolved::{CommandKey, ResolvedCommand},
-  ExecutionContext,
-};
+use tauri_utils::acl::ExecutionContext;
 
 fn main() {
   let mut context = tauri::generate_context!("../../examples/parent-window/tauri.conf.json");
@@ -16,16 +13,9 @@ fn main() {
     "plugin:event|listen",
     "plugin:webview|create_webview_window",
   ] {
-    context.resolved_acl().allowed_commands.insert(
-      CommandKey {
-        name: cmd.into(),
-        context: ExecutionContext::Local,
-      },
-      ResolvedCommand {
-        windows: vec!["*".parse().unwrap()],
-        ..Default::default()
-      },
-    );
+    context
+      .runtime_authority_mut()
+      .__allow_command(cmd.to_string(), ExecutionContext::Local);
   }
 
   tauri::Builder::default()

+ 2 - 1
tooling/cli/templates/tauri.conf.json

@@ -19,7 +19,8 @@
       }
     ],
     "security": {
-      "csp": null
+      "csp": null,
+      "capabilities": ["default-plugins"]
     }
   },
   "bundle": {