tauri/ipc/
authority.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::collections::BTreeMap;
6use std::fmt::{Debug, Display};
7use std::sync::Arc;
8
9use serde::de::DeserializeOwned;
10
11#[cfg(feature = "dynamic-acl")]
12use tauri_utils::acl::capability::CapabilityFile;
13#[cfg(any(feature = "dynamic-acl", debug_assertions))]
14use tauri_utils::acl::manifest::Manifest;
15use tauri_utils::acl::{
16  resolved::{Resolved, ResolvedCommand, ResolvedScope, ScopeKey},
17  ExecutionContext, Value, APP_ACL_KEY,
18};
19
20use url::Url;
21
22use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime};
23use crate::{AppHandle, Manager, StateManager, Webview};
24
25use super::{CommandArg, CommandItem};
26
27/// The runtime authority used to authorize IPC execution based on the Access Control List.
28pub struct RuntimeAuthority {
29  #[cfg(any(feature = "dynamic-acl", debug_assertions))]
30  acl: BTreeMap<String, Manifest>,
31  has_app_acl: bool,
32  allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
33  denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
34  pub(crate) scope_manager: ScopeManager,
35}
36
37/// The origin trying to access the IPC.
38pub enum Origin {
39  /// Local app origin.
40  Local,
41  /// Remote origin.
42  Remote {
43    /// Remote URL.
44    url: Url,
45  },
46}
47
48impl Display for Origin {
49  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50    match self {
51      Self::Local => write!(f, "local"),
52      Self::Remote { url } => write!(f, "remote: {url}"),
53    }
54  }
55}
56
57impl Origin {
58  fn matches(&self, context: &ExecutionContext) -> bool {
59    match (self, context) {
60      (Self::Local, ExecutionContext::Local) => true,
61      (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => {
62        url_pattern.test(url)
63      }
64      _ => false,
65    }
66  }
67}
68
69/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`]
70/// to only include the raw ACL when it's needed
71///
72/// ## Stability
73///
74/// The output of this macro is managed internally by Tauri,
75/// and should not be accessed directly on normal applications.
76/// It may have breaking changes in the future.
77#[cfg(any(feature = "dynamic-acl", debug_assertions))]
78#[doc(hidden)]
79#[macro_export]
80macro_rules! runtime_authority {
81  ($acl:expr, $resolved_acl:expr) => {
82    $crate::ipc::RuntimeAuthority::new($acl, $resolved_acl)
83  };
84}
85
86/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`]
87/// to only include the raw ACL when it's needed
88///
89/// ## Stability
90///
91/// The output of this macro is managed internally by Tauri,
92/// and should not be accessed directly on normal applications.
93/// It may have breaking changes in the future.
94#[cfg(not(any(feature = "dynamic-acl", debug_assertions)))]
95#[doc(hidden)]
96#[macro_export]
97macro_rules! runtime_authority {
98  ($_acl:expr, $resolved_acl:expr) => {
99    $crate::ipc::RuntimeAuthority::new($resolved_acl)
100  };
101}
102
103impl RuntimeAuthority {
104  /// Contruct a new [`RuntimeAuthority`] from the ACL
105  ///
106  /// **Please prefer using the [`runtime_authority`] macro instead of calling this directly**
107  #[doc(hidden)]
108  pub fn new(
109    #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap<String, Manifest>,
110    resolved_acl: Resolved,
111  ) -> Self {
112    let command_cache = resolved_acl
113      .command_scope
114      .keys()
115      .map(|key| (*key, StateManager::new()))
116      .collect();
117    Self {
118      #[cfg(any(feature = "dynamic-acl", debug_assertions))]
119      acl,
120      has_app_acl: resolved_acl.has_app_acl,
121      allowed_commands: resolved_acl.allowed_commands,
122      denied_commands: resolved_acl.denied_commands,
123      scope_manager: ScopeManager {
124        command_scope: resolved_acl.command_scope,
125        global_scope: resolved_acl.global_scope,
126        command_cache,
127        global_scope_cache: StateManager::new(),
128      },
129    }
130  }
131
132  pub(crate) fn has_app_manifest(&self) -> bool {
133    self.has_app_acl
134  }
135
136  #[doc(hidden)]
137  pub fn __allow_command(&mut self, command: String, context: ExecutionContext) {
138    self.allowed_commands.insert(
139      command,
140      vec![ResolvedCommand {
141        context,
142        windows: vec!["*".parse().unwrap()],
143        ..Default::default()
144      }],
145    );
146  }
147
148  /// Adds the given capability to the runtime authority.
149  #[cfg(feature = "dynamic-acl")]
150  pub fn add_capability(&mut self, capability: impl super::RuntimeCapability) -> crate::Result<()> {
151    self.add_capability_inner(capability.build())
152  }
153
154  #[cfg(feature = "dynamic-acl")]
155  fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> {
156    let mut capabilities = BTreeMap::new();
157    match capability {
158      CapabilityFile::Capability(c) => {
159        capabilities.insert(c.identifier.clone(), c);
160      }
161
162      CapabilityFile::List(capabilities_list)
163      | CapabilityFile::NamedList {
164        capabilities: capabilities_list,
165      } => {
166        capabilities.extend(
167          capabilities_list
168            .into_iter()
169            .map(|c| (c.identifier.clone(), c)),
170        );
171      }
172    }
173
174    let resolved = Resolved::resolve(
175      &self.acl,
176      capabilities,
177      tauri_utils::platform::Target::current(),
178    )
179    .unwrap();
180
181    // fill global scope
182    for (plugin, global_scope) in resolved.global_scope {
183      let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default();
184
185      global_scope_entry.allow.extend(global_scope.allow);
186      global_scope_entry.deny.extend(global_scope.deny);
187
188      self.scope_manager.global_scope_cache = StateManager::new();
189    }
190
191    // denied commands
192    for (cmd_key, resolved_cmds) in resolved.denied_commands {
193      let entry = self.denied_commands.entry(cmd_key).or_default();
194      entry.extend(resolved_cmds);
195    }
196
197    // allowed commands
198    for (cmd_key, resolved_cmds) in resolved.allowed_commands {
199      // fill command scope
200      for resolved_cmd in &resolved_cmds {
201        if let Some(scope_id) = resolved_cmd.scope_id {
202          let command_scope = resolved.command_scope.get(&scope_id).unwrap();
203
204          let command_scope_entry = self
205            .scope_manager
206            .command_scope
207            .entry(scope_id)
208            .or_default();
209          command_scope_entry
210            .allow
211            .extend(command_scope.allow.clone());
212          command_scope_entry.deny.extend(command_scope.deny.clone());
213
214          self
215            .scope_manager
216            .command_cache
217            .insert(scope_id, StateManager::new());
218        }
219      }
220
221      let entry = self.allowed_commands.entry(cmd_key).or_default();
222      entry.extend(resolved_cmds);
223    }
224
225    Ok(())
226  }
227
228  #[cfg(debug_assertions)]
229  pub(crate) fn resolve_access_message(
230    &self,
231    key: &str,
232    command_name: &str,
233    window: &str,
234    webview: &str,
235    origin: &Origin,
236  ) -> String {
237    fn print_references(resolved: &[ResolvedCommand]) -> String {
238      resolved
239        .iter()
240        .map(|r| {
241          format!(
242            "capability: {}, permission: {}",
243            r.referenced_by.capability, r.referenced_by.permission
244          )
245        })
246        .collect::<Vec<_>>()
247        .join(" || ")
248    }
249
250    fn print_allowed_on(resolved: &[ResolvedCommand]) -> String {
251      if resolved.is_empty() {
252        "command not allowed on any window/webview/URL context".to_string()
253      } else {
254        let mut s = "allowed on: ".to_string();
255
256        let last_index = resolved.len() - 1;
257        for (index, cmd) in resolved.iter().enumerate() {
258          let windows = cmd
259            .windows
260            .iter()
261            .map(|w| format!("\"{}\"", w.as_str()))
262            .collect::<Vec<_>>()
263            .join(", ");
264          let webviews = cmd
265            .webviews
266            .iter()
267            .map(|w| format!("\"{}\"", w.as_str()))
268            .collect::<Vec<_>>()
269            .join(", ");
270
271          s.push('[');
272
273          if !windows.is_empty() {
274            s.push_str(&format!("windows: {windows}, "));
275          }
276
277          if !webviews.is_empty() {
278            s.push_str(&format!("webviews: {webviews}, "));
279          }
280
281          match &cmd.context {
282            ExecutionContext::Local => s.push_str("URL: local"),
283            ExecutionContext::Remote { url } => s.push_str(&format!("URL: {}", url.as_str())),
284          }
285
286          s.push(']');
287
288          if index != last_index {
289            s.push_str(", ");
290          }
291        }
292
293        s
294      }
295    }
296
297    fn has_permissions_allowing_command(
298      manifest: &Manifest,
299      set: &crate::utils::acl::PermissionSet,
300      command: &str,
301    ) -> bool {
302      for permission_id in &set.permissions {
303        if permission_id == "default" {
304          if let Some(default) = &manifest.default_permission {
305            if has_permissions_allowing_command(manifest, default, command) {
306              return true;
307            }
308          }
309        } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) {
310          if has_permissions_allowing_command(manifest, ref_set, command) {
311            return true;
312          }
313        } else if let Some(permission) = manifest.permissions.get(permission_id) {
314          if permission.commands.allow.contains(&command.into()) {
315            return true;
316          }
317        }
318      }
319      false
320    }
321
322    let command = if key == APP_ACL_KEY {
323      command_name.to_string()
324    } else {
325      format!("plugin:{key}|{command_name}")
326    };
327
328    let command_pretty_name = if key == APP_ACL_KEY {
329      command_name.to_string()
330    } else {
331      format!("{key}.{command_name}")
332    };
333
334    if let Some(resolved) = self.denied_commands.get(&command) {
335      format!(
336        "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}",
337        print_references(resolved)
338      )
339    } else {
340      let command_matches = self.allowed_commands.get(&command);
341
342      if let Some(resolved) = self.allowed_commands.get(&command) {
343        let resolved_matching_origin = resolved
344          .iter()
345          .filter(|cmd| origin.matches(&cmd.context))
346          .collect::<Vec<&ResolvedCommand>>();
347        if resolved_matching_origin
348          .iter()
349          .any(|cmd| cmd.webviews.iter().any(|w| w.matches(webview)))
350          || resolved_matching_origin
351            .iter()
352            .any(|cmd| cmd.windows.iter().any(|w| w.matches(window)))
353        {
354          "allowed".to_string()
355        } else {
356          format!("{command_pretty_name} not allowed on window \"{window}\", webview \"{webview}\", URL: {}\n\n{}\n\nreferenced by: {}",
357            match origin {
358              Origin::Local => "local",
359              Origin::Remote { url } => url.as_str()
360            },
361            print_allowed_on(resolved),
362            print_references(resolved)
363          )
364        }
365      } else {
366        let permission_error_detail = if let Some((key, manifest)) = self
367          .acl
368          .get_key_value(key)
369          .or_else(|| self.acl.get_key_value(&format!("core:{key}")))
370        {
371          let mut permissions_referencing_command = Vec::new();
372
373          if let Some(default) = &manifest.default_permission {
374            if has_permissions_allowing_command(manifest, default, command_name) {
375              permissions_referencing_command.push("default".into());
376            }
377          }
378          for set in manifest.permission_sets.values() {
379            if has_permissions_allowing_command(manifest, set, command_name) {
380              permissions_referencing_command.push(set.identifier.clone());
381            }
382          }
383          for permission in manifest.permissions.values() {
384            if permission.commands.allow.contains(&command_name.into()) {
385              permissions_referencing_command.push(permission.identifier.clone());
386            }
387          }
388
389          permissions_referencing_command.sort();
390
391          let associated_permissions = permissions_referencing_command
392            .into_iter()
393            .map(|permission| {
394              if key == APP_ACL_KEY {
395                permission
396              } else {
397                format!("{key}:{permission}")
398              }
399            })
400            .collect::<Vec<_>>()
401            .join(", ");
402
403          if associated_permissions.is_empty() {
404            "Command not found".to_string()
405          } else {
406            format!("Permissions associated with this command: {associated_permissions}")
407          }
408        } else {
409          "Plugin not found".to_string()
410        };
411
412        if let Some(resolved_cmds) = command_matches {
413          format!(
414            "{command_pretty_name} not allowed on origin [{origin}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}",
415            resolved_cmds
416              .iter()
417              .map(|resolved| {
418                let context = match &resolved.context {
419                  ExecutionContext::Local => "[local]".to_string(),
420                  ExecutionContext::Remote { url } => format!("[remote: {}]", url.as_str()),
421                };
422                format!(
423                  "- context: {context}, referenced by: capability: {}, permission: {}",
424                  resolved.referenced_by.capability,
425                  resolved.referenced_by.permission
426                )
427              })
428              .collect::<Vec<_>>()
429              .join("\n")
430          )
431        } else {
432          format!("{command_pretty_name} not allowed. {permission_error_detail}")
433        }
434      }
435    }
436  }
437
438  /// Checks if the given IPC execution is allowed and returns the [`ResolvedCommand`] if it is.
439  pub fn resolve_access(
440    &self,
441    command: &str,
442    window: &str,
443    webview: &str,
444    origin: &Origin,
445  ) -> Option<Vec<ResolvedCommand>> {
446    if self
447      .denied_commands
448      .get(command)
449      .map(|resolved| resolved.iter().any(|cmd| origin.matches(&cmd.context)))
450      .is_some()
451    {
452      None
453    } else {
454      self.allowed_commands.get(command).and_then(|resolved| {
455        let resolved_cmds = resolved
456          .iter()
457          .filter(|cmd| {
458            origin.matches(&cmd.context)
459              && (cmd.webviews.iter().any(|w| w.matches(webview))
460                || cmd.windows.iter().any(|w| w.matches(window)))
461          })
462          .cloned()
463          .collect::<Vec<_>>();
464        if resolved_cmds.is_empty() {
465          None
466        } else {
467          Some(resolved_cmds)
468        }
469      })
470    }
471  }
472}
473
474/// List of allowed and denied objects that match either the command-specific or plugin global scope criteria.
475#[derive(Debug)]
476pub struct ScopeValue<T: ScopeObject> {
477  allow: Arc<Vec<Arc<T>>>,
478  deny: Arc<Vec<Arc<T>>>,
479}
480
481impl<T: ScopeObject> ScopeValue<T> {
482  fn clone(&self) -> Self {
483    Self {
484      allow: self.allow.clone(),
485      deny: self.deny.clone(),
486    }
487  }
488
489  /// What this access scope allows.
490  pub fn allows(&self) -> &Vec<Arc<T>> {
491    &self.allow
492  }
493
494  /// What this access scope denies.
495  pub fn denies(&self) -> &Vec<Arc<T>> {
496    &self.deny
497  }
498}
499
500/// Access scope for a command that can be retrieved directly in the command function.
501#[derive(Debug)]
502pub struct CommandScope<T: ScopeObject> {
503  allow: Vec<Arc<T>>,
504  deny: Vec<Arc<T>>,
505}
506
507impl<T: ScopeObject> CommandScope<T> {
508  pub(crate) fn resolve<R: Runtime>(
509    webview: &Webview<R>,
510    scope_ids: Vec<u64>,
511  ) -> crate::Result<Self> {
512    let mut allow = Vec::new();
513    let mut deny = Vec::new();
514
515    for scope_id in scope_ids {
516      let scope = webview
517        .manager()
518        .runtime_authority
519        .lock()
520        .unwrap()
521        .scope_manager
522        .get_command_scope_typed::<R, T>(webview.app_handle(), &scope_id)?;
523
524      for s in scope.allows() {
525        allow.push(s.clone());
526      }
527      for s in scope.denies() {
528        deny.push(s.clone());
529      }
530    }
531
532    Ok(CommandScope { allow, deny })
533  }
534
535  /// What this access scope allows.
536  pub fn allows(&self) -> &Vec<Arc<T>> {
537    &self.allow
538  }
539
540  /// What this access scope denies.
541  pub fn denies(&self) -> &Vec<Arc<T>> {
542    &self.deny
543  }
544}
545
546impl<T: ScopeObjectMatch> CommandScope<T> {
547  /// Ensure all deny scopes were not matched and any allow scopes were.
548  ///
549  /// This **WILL** return `true` if the allow scopes are empty and the deny
550  /// scopes did not trigger. If you require at least one allow scope, then
551  /// ensure the allow scopes are not empty before calling this method.
552  ///
553  /// ```
554  /// # use tauri::ipc::CommandScope;
555  /// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> {
556  /// if scope.allows().is_empty() {
557  ///   return Err("you need to specify at least 1 allow scope!");
558  /// }
559  /// # Ok(())
560  /// # }
561  /// ```
562  ///
563  /// # Example
564  ///
565  /// ```
566  /// # use serde::{Serialize, Deserialize};
567  /// # use url::Url;
568  /// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command};
569  /// #
570  /// #[derive(Debug, Clone, Serialize, Deserialize)]
571  /// # pub struct Scope;
572  /// #
573  /// # impl ScopeObjectMatch for Scope {
574  /// #   type Input = str;
575  /// #
576  /// #   fn matches(&self, input: &str) -> bool {
577  /// #     true
578  /// #   }
579  /// # }
580  /// #
581  /// # fn do_work(_: String) -> Result<String, &'static str> {
582  /// #   Ok("Output".into())
583  /// # }
584  /// #
585  /// #[command]
586  /// fn my_command(scope: CommandScope<Scope>, input: String) -> Result<String, &'static str> {
587  ///   if scope.matches(&input) {
588  ///     do_work(input)
589  ///   } else {
590  ///     Err("Scope didn't match input")
591  ///   }
592  /// }
593  /// ```
594  pub fn matches(&self, input: &T::Input) -> bool {
595    // first make sure the input doesn't match any existing deny scope
596    if self.deny.iter().any(|s| s.matches(input)) {
597      return false;
598    }
599
600    // if there are allow scopes, ensure the input matches at least 1
601    if self.allow.is_empty() {
602      true
603    } else {
604      self.allow.iter().any(|s| s.matches(input))
605    }
606  }
607}
608
609impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T> {
610  /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`].
611  fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
612    let scope_ids = command.acl.as_ref().map(|resolved| {
613      resolved
614        .iter()
615        .filter_map(|cmd| cmd.scope_id)
616        .collect::<Vec<_>>()
617    });
618    if let Some(scope_ids) = scope_ids {
619      CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into)
620    } else {
621      Ok(CommandScope {
622        allow: Default::default(),
623        deny: Default::default(),
624      })
625    }
626  }
627}
628
629/// Global access scope that can be retrieved directly in the command function.
630#[derive(Debug)]
631pub struct GlobalScope<T: ScopeObject>(ScopeValue<T>);
632
633impl<T: ScopeObject> GlobalScope<T> {
634  pub(crate) fn resolve<R: Runtime>(webview: &Webview<R>, plugin: &str) -> crate::Result<Self> {
635    webview
636      .manager()
637      .runtime_authority
638      .lock()
639      .unwrap()
640      .scope_manager
641      .get_global_scope_typed(webview.app_handle(), plugin)
642      .map(Self)
643  }
644
645  /// What this access scope allows.
646  pub fn allows(&self) -> &Vec<Arc<T>> {
647    &self.0.allow
648  }
649
650  /// What this access scope denies.
651  pub fn denies(&self) -> &Vec<Arc<T>> {
652    &self.0.deny
653  }
654}
655
656impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope<T> {
657  /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`GlobalScope`].
658  fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
659    GlobalScope::resolve(
660      &command.message.webview,
661      command.plugin.unwrap_or(APP_ACL_KEY),
662    )
663    .map_err(InvokeError::from_error)
664  }
665}
666
667#[derive(Debug)]
668pub struct ScopeManager {
669  command_scope: BTreeMap<ScopeKey, ResolvedScope>,
670  global_scope: BTreeMap<String, ResolvedScope>,
671  command_cache: BTreeMap<ScopeKey, StateManager>,
672  global_scope_cache: StateManager,
673}
674
675/// Marks a type as a scope object.
676///
677/// Usually you will just rely on [`serde::de::DeserializeOwned`] instead of implementing it manually,
678/// though this is useful if you need to do some initialization logic on the type itself.
679pub trait ScopeObject: Sized + Send + Sync + Debug + 'static {
680  /// The error type.
681  type Error: std::error::Error + Send + Sync;
682  /// Deserialize the raw scope value.
683  fn deserialize<R: Runtime>(app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error>;
684}
685
686impl<T: Send + Sync + Debug + DeserializeOwned + 'static> ScopeObject for T {
687  type Error = serde_json::Error;
688  fn deserialize<R: Runtime>(_app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error> {
689    serde_json::from_value(raw.into())
690  }
691}
692
693/// A [`ScopeObject`] whose validation can be represented as a `bool`.
694///
695/// # Example
696///
697/// ```
698/// # use serde::{Deserialize, Serialize};
699/// # use tauri::{ipc::ScopeObjectMatch, Url};
700/// #
701/// #[derive(Debug, Clone, Serialize, Deserialize)]
702/// #[serde(rename_all = "camelCase")]
703/// pub enum Scope {
704///   Domain(Url),
705///   StartsWith(String),
706/// }
707///
708/// impl ScopeObjectMatch for Scope {
709///   type Input = str;
710///
711///   fn matches(&self, input: &str) -> bool {
712///     match self {
713///       Scope::Domain(url) => {
714///         let parsed: Url = match input.parse() {
715///           Ok(parsed) => parsed,
716///           Err(_) => return false,
717///         };
718///
719///         let domain = parsed.domain();
720///
721///         domain.is_some() && domain == url.domain()
722///       }
723///       Scope::StartsWith(start) => input.starts_with(start),
724///     }
725///   }
726/// }
727/// ```
728pub trait ScopeObjectMatch: ScopeObject {
729  /// The type of input expected to validate against the scope.
730  ///
731  /// This will be borrowed, so if you want to match on a `&str` this type should be `str`.
732  type Input: ?Sized;
733
734  /// Check if the input matches against the scope.
735  fn matches(&self, input: &Self::Input) -> bool;
736}
737
738impl ScopeManager {
739  pub(crate) fn get_global_scope_typed<R: Runtime, T: ScopeObject>(
740    &self,
741    app: &AppHandle<R>,
742    key: &str,
743  ) -> crate::Result<ScopeValue<T>> {
744    match self.global_scope_cache.try_get::<ScopeValue<T>>() {
745      Some(cached) => Ok(cached.inner().clone()),
746      None => {
747        let mut allow = Vec::new();
748        let mut deny = Vec::new();
749
750        if let Some(global_scope) = self.global_scope.get(key) {
751          for allowed in &global_scope.allow {
752            allow
753              .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
754                |e| crate::Error::CannotDeserializeScope(Box::new(e)),
755              )?));
756          }
757          for denied in &global_scope.deny {
758            deny
759              .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
760                |e| crate::Error::CannotDeserializeScope(Box::new(e)),
761              )?));
762          }
763        }
764
765        let scope = ScopeValue {
766          allow: Arc::new(allow),
767          deny: Arc::new(deny),
768        };
769        self.global_scope_cache.set(scope.clone());
770        Ok(scope)
771      }
772    }
773  }
774
775  fn get_command_scope_typed<R: Runtime, T: ScopeObject>(
776    &self,
777    app: &AppHandle<R>,
778    key: &ScopeKey,
779  ) -> crate::Result<ScopeValue<T>> {
780    let cache = self.command_cache.get(key).unwrap();
781    match cache.try_get::<ScopeValue<T>>() {
782      Some(cached) => Ok(cached.inner().clone()),
783      None => {
784        let resolved_scope = self
785          .command_scope
786          .get(key)
787          .unwrap_or_else(|| panic!("missing command scope for key {key}"));
788
789        let mut allow = Vec::new();
790        let mut deny = Vec::new();
791
792        for allowed in &resolved_scope.allow {
793          allow
794            .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
795              |e| crate::Error::CannotDeserializeScope(Box::new(e)),
796            )?));
797        }
798        for denied in &resolved_scope.deny {
799          deny
800            .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
801              |e| crate::Error::CannotDeserializeScope(Box::new(e)),
802            )?));
803        }
804
805        let value = ScopeValue {
806          allow: Arc::new(allow),
807          deny: Arc::new(deny),
808        };
809
810        let _ = cache.set(value.clone());
811        Ok(value)
812      }
813    }
814  }
815}
816
817#[cfg(test)]
818mod tests {
819  use glob::Pattern;
820  use tauri_utils::acl::{
821    resolved::{Resolved, ResolvedCommand},
822    ExecutionContext,
823  };
824
825  use crate::ipc::Origin;
826
827  use super::RuntimeAuthority;
828
829  #[test]
830  fn window_glob_pattern_matches() {
831    let command = "my-command";
832    let window = "main-*";
833    let webview = "other-*";
834
835    let resolved_cmd = vec![ResolvedCommand {
836      windows: vec![Pattern::new(window).unwrap()],
837      ..Default::default()
838    }];
839    let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
840      .into_iter()
841      .collect();
842
843    let authority = RuntimeAuthority::new(
844      Default::default(),
845      Resolved {
846        allowed_commands,
847        ..Default::default()
848      },
849    );
850
851    assert_eq!(
852      authority.resolve_access(
853        command,
854        &window.replace('*', "something"),
855        webview,
856        &Origin::Local
857      ),
858      Some(resolved_cmd)
859    );
860  }
861
862  #[test]
863  fn webview_glob_pattern_matches() {
864    let command = "my-command";
865    let window = "other-*";
866    let webview = "main-*";
867
868    let resolved_cmd = vec![ResolvedCommand {
869      windows: vec![Pattern::new(window).unwrap()],
870      webviews: vec![Pattern::new(webview).unwrap()],
871      ..Default::default()
872    }];
873    let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
874      .into_iter()
875      .collect();
876
877    let authority = RuntimeAuthority::new(
878      Default::default(),
879      Resolved {
880        allowed_commands,
881        ..Default::default()
882      },
883    );
884
885    assert_eq!(
886      authority.resolve_access(
887        command,
888        window,
889        &webview.replace('*', "something"),
890        &Origin::Local
891      ),
892      Some(resolved_cmd)
893    );
894  }
895
896  #[test]
897  fn remote_domain_matches() {
898    let url = "https://tauri.app";
899    let command = "my-command";
900    let window = "main";
901    let webview = "main";
902
903    let resolved_cmd = vec![ResolvedCommand {
904      windows: vec![Pattern::new(window).unwrap()],
905      context: ExecutionContext::Remote {
906        url: url.parse().unwrap(),
907      },
908      ..Default::default()
909    }];
910    let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
911      .into_iter()
912      .collect();
913
914    let authority = RuntimeAuthority::new(
915      Default::default(),
916      Resolved {
917        allowed_commands,
918        ..Default::default()
919      },
920    );
921
922    assert_eq!(
923      authority.resolve_access(
924        command,
925        window,
926        webview,
927        &Origin::Remote {
928          url: url.parse().unwrap()
929        }
930      ),
931      Some(resolved_cmd)
932    );
933  }
934
935  #[test]
936  fn remote_domain_glob_pattern_matches() {
937    let url = "http://tauri.*";
938    let command = "my-command";
939    let window = "main";
940    let webview = "main";
941
942    let resolved_cmd = vec![ResolvedCommand {
943      windows: vec![Pattern::new(window).unwrap()],
944      context: ExecutionContext::Remote {
945        url: url.parse().unwrap(),
946      },
947      ..Default::default()
948    }];
949    let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
950      .into_iter()
951      .collect();
952
953    let authority = RuntimeAuthority::new(
954      Default::default(),
955      Resolved {
956        allowed_commands,
957        ..Default::default()
958      },
959    );
960
961    assert_eq!(
962      authority.resolve_access(
963        command,
964        window,
965        webview,
966        &Origin::Remote {
967          url: url.replace('*', "studio").parse().unwrap()
968        }
969      ),
970      Some(resolved_cmd)
971    );
972  }
973
974  #[test]
975  fn remote_context_denied() {
976    let command = "my-command";
977    let window = "main";
978    let webview = "main";
979
980    let resolved_cmd = vec![ResolvedCommand {
981      windows: vec![Pattern::new(window).unwrap()],
982      ..Default::default()
983    }];
984    let allowed_commands = [(command.to_string(), resolved_cmd)].into_iter().collect();
985
986    let authority = RuntimeAuthority::new(
987      Default::default(),
988      Resolved {
989        allowed_commands,
990        ..Default::default()
991      },
992    );
993
994    assert!(authority
995      .resolve_access(
996        command,
997        window,
998        webview,
999        &Origin::Remote {
1000          url: "https://tauri.app".parse().unwrap()
1001        }
1002      )
1003      .is_none());
1004  }
1005
1006  #[test]
1007  fn denied_command_takes_precendence() {
1008    let command = "my-command";
1009    let window = "main";
1010    let webview = "main";
1011    let windows = vec![Pattern::new(window).unwrap()];
1012    let allowed_commands = [(
1013      command.to_string(),
1014      vec![ResolvedCommand {
1015        windows: windows.clone(),
1016        ..Default::default()
1017      }],
1018    )]
1019    .into_iter()
1020    .collect();
1021    let denied_commands = [(
1022      command.to_string(),
1023      vec![ResolvedCommand {
1024        windows,
1025        ..Default::default()
1026      }],
1027    )]
1028    .into_iter()
1029    .collect();
1030
1031    let authority = RuntimeAuthority::new(
1032      Default::default(),
1033      Resolved {
1034        allowed_commands,
1035        denied_commands,
1036        ..Default::default()
1037      },
1038    );
1039
1040    assert!(authority
1041      .resolve_access(command, window, webview, &Origin::Local)
1042      .is_none());
1043  }
1044
1045  #[cfg(debug_assertions)]
1046  #[test]
1047  fn resolve_access_message() {
1048    use tauri_utils::acl::manifest::Manifest;
1049
1050    let plugin_name = "myplugin";
1051    let command_allowed_on_window = "my-command-window";
1052    let command_allowed_on_webview_window = "my-command-webview-window";
1053    let window = "main-*";
1054    let webview = "webview-*";
1055    let remote_url = "http://localhost:8080";
1056
1057    let referenced_by = tauri_utils::acl::resolved::ResolvedCommandReference {
1058      capability: "maincap".to_string(),
1059      permission: "allow-command".to_string(),
1060    };
1061
1062    let resolved_window_cmd = ResolvedCommand {
1063      windows: vec![Pattern::new(window).unwrap()],
1064      referenced_by: referenced_by.clone(),
1065      ..Default::default()
1066    };
1067    let resolved_webview_window_cmd = ResolvedCommand {
1068      windows: vec![Pattern::new(window).unwrap()],
1069      webviews: vec![Pattern::new(webview).unwrap()],
1070      referenced_by: referenced_by.clone(),
1071      ..Default::default()
1072    };
1073    let resolved_webview_window_remote_cmd = ResolvedCommand {
1074      windows: vec![Pattern::new(window).unwrap()],
1075      webviews: vec![Pattern::new(webview).unwrap()],
1076      referenced_by,
1077      context: ExecutionContext::Remote {
1078        url: remote_url.parse().unwrap(),
1079      },
1080      ..Default::default()
1081    };
1082
1083    let allowed_commands = [
1084      (
1085        format!("plugin:{plugin_name}|{command_allowed_on_window}"),
1086        vec![resolved_window_cmd],
1087      ),
1088      (
1089        format!("plugin:{plugin_name}|{command_allowed_on_webview_window}"),
1090        vec![
1091          resolved_webview_window_cmd,
1092          resolved_webview_window_remote_cmd,
1093        ],
1094      ),
1095    ]
1096    .into_iter()
1097    .collect();
1098
1099    let authority = RuntimeAuthority::new(
1100      [(
1101        plugin_name.to_string(),
1102        Manifest {
1103          default_permission: None,
1104          permissions: Default::default(),
1105          permission_sets: Default::default(),
1106          global_scope_schema: None,
1107        },
1108      )]
1109      .into_iter()
1110      .collect(),
1111      Resolved {
1112        allowed_commands,
1113        ..Default::default()
1114      },
1115    );
1116
1117    // unknown plugin
1118    assert_eq!(
1119      authority.resolve_access_message(
1120        "unknown-plugin",
1121        command_allowed_on_window,
1122        window,
1123        webview,
1124        &Origin::Local
1125      ),
1126      "unknown-plugin.my-command-window not allowed. Plugin not found"
1127    );
1128
1129    // unknown command
1130    assert_eq!(
1131      authority.resolve_access_message(
1132        plugin_name,
1133        "unknown-command",
1134        window,
1135        webview,
1136        &Origin::Local
1137      ),
1138      "myplugin.unknown-command not allowed. Command not found"
1139    );
1140
1141    // window/webview do not match
1142    assert_eq!(
1143      authority.resolve_access_message(
1144        plugin_name,
1145        command_allowed_on_window,
1146        "other-window",
1147        "any-webview",
1148        &Origin::Local
1149      ),
1150      "myplugin.my-command-window not allowed on window \"other-window\", webview \"any-webview\", URL: local\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command"
1151    );
1152
1153    // window matches, but not origin
1154    assert_eq!(
1155      authority.resolve_access_message(
1156        plugin_name,
1157        command_allowed_on_window,
1158        window,
1159        "any-webview",
1160        &Origin::Remote {
1161          url: "http://localhst".parse().unwrap()
1162        }
1163      ),
1164      "myplugin.my-command-window not allowed on window \"main-*\", webview \"any-webview\", URL: http://localhst/\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command"
1165    );
1166
1167    // window/webview do not match
1168    assert_eq!(
1169      authority.resolve_access_message(
1170        plugin_name,
1171        command_allowed_on_webview_window,
1172        "other-window",
1173        "other-webview",
1174        &Origin::Local
1175      ),
1176      "myplugin.my-command-webview-window not allowed on window \"other-window\", webview \"other-webview\", URL: local\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command"
1177    );
1178
1179    // window/webview matches, but not origin
1180    assert_eq!(
1181      authority.resolve_access_message(
1182        plugin_name,
1183        command_allowed_on_webview_window,
1184        window,
1185        webview,
1186        &Origin::Remote {
1187          url: "http://localhost:123".parse().unwrap()
1188        }
1189      ),
1190      "myplugin.my-command-webview-window not allowed on window \"main-*\", webview \"webview-*\", URL: http://localhost:123/\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command"
1191    );
1192  }
1193}