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