From 5621174b05f615e1589292ccd3954dc7e6b5569f Mon Sep 17 00:00:00 2001 From: chip Date: Thu, 26 Sep 2024 01:22:37 +0200 Subject: [PATCH] feat: add `ScopeObjectMatch` trait for easy scope validation (#11132) --- .changes/scope-object-match.md | 5 ++ crates/tauri/src/ipc/authority.rs | 108 ++++++++++++++++++++++++++++++ crates/tauri/src/ipc/mod.rs | 2 +- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 .changes/scope-object-match.md diff --git a/.changes/scope-object-match.md b/.changes/scope-object-match.md new file mode 100644 index 000000000000..d469c34a5f5d --- /dev/null +++ b/.changes/scope-object-match.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:feat +--- + +Add `ScopeObjectMatch` for easy scope validation those that can be represented by a boolean return value. diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs index 19eda03c989d..21e5f0da785b 100644 --- a/crates/tauri/src/ipc/authority.rs +++ b/crates/tauri/src/ipc/authority.rs @@ -625,6 +625,69 @@ impl CommandScope { } } +impl CommandScope { + /// Ensure all deny scopes were not matched and any allow scopes were. + /// + /// This **WILL** return `true` if the allow scopes are empty and the deny + /// scopes did not trigger. If you require at least one allow scope, then + /// ensure the allow scopes are not empty before calling this method. + /// + /// ``` + /// # use tauri::ipc::CommandScope; + /// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> { + /// if scope.allows().is_empty() { + /// return Err("you need to specify at least 1 allow scope!"); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use url::Url; + /// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command}; + /// # + /// #[derive(Debug, Clone, Serialize, Deserialize)] + /// # pub struct Scope; + /// # + /// # impl ScopeObjectMatch for Scope { + /// # type Input = str; + /// # + /// # fn matches(&self, input: &str) -> bool { + /// # true + /// # } + /// # } + /// # + /// # fn do_work(_: String) -> Result { + /// # Ok("Output".into()) + /// # } + /// # + /// #[command] + /// fn my_command(scope: CommandScope, input: String) -> Result { + /// if scope.matches(&input) { + /// do_work(input) + /// } else { + /// Err("Scope didn't match input") + /// } + /// } + /// ``` + pub fn matches(&self, input: &T::Input) -> bool { + // first make sure the input doesn't match any existing deny scope + if self.deny.iter().any(|s| s.matches(input)) { + return false; + } + + // if there are allow scopes, ensure the input matches at least 1 + if self.allow.is_empty() { + true + } else { + self.allow.iter().any(|s| s.matches(input)) + } + } +} + impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope { /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`]. fn from_command(command: CommandItem<'a, R>) -> Result { @@ -729,6 +792,51 @@ impl ScopeObject for T { } } +/// A [`ScopeObject`] whose validation can be represented as a `bool`. +/// +/// # Example +/// +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// # use tauri::{ipc::ScopeObjectMatch, Url}; +/// # +/// #[derive(Debug, Clone, Serialize, Deserialize)] +/// #[serde(rename_all = "camelCase")] +/// pub enum Scope { +/// Domain(Url), +/// StartsWith(String), +/// } +/// +/// impl ScopeObjectMatch for Scope { +/// type Input = str; +/// +/// fn matches(&self, input: &str) -> bool { +/// match self { +/// Scope::Domain(url) => { +/// let parsed: Url = match input.parse() { +/// Ok(parsed) => parsed, +/// Err(_) => return false, +/// }; +/// +/// let domain = parsed.domain(); +/// +/// domain.is_some() && domain == url.domain() +/// } +/// Scope::StartsWith(start) => input.starts_with(start), +/// } +/// } +/// } +/// ``` +pub trait ScopeObjectMatch: ScopeObject { + /// The type of input expected to validate against the scope. + /// + /// This will be borrowed, so if you want to match on a `&str` this type should be `str`. + type Input: ?Sized; + + /// Check if the input matches against the scope. + fn matches(&self, input: &Self::Input) -> bool; +} + impl ScopeManager { pub(crate) fn get_global_scope_typed( &self, diff --git a/crates/tauri/src/ipc/mod.rs b/crates/tauri/src/ipc/mod.rs index 05442f3cf9ac..3d8752313928 100644 --- a/crates/tauri/src/ipc/mod.rs +++ b/crates/tauri/src/ipc/mod.rs @@ -29,7 +29,7 @@ pub(crate) mod protocol; pub use authority::{ CapabilityBuilder, CommandScope, GlobalScope, Origin, RuntimeAuthority, RuntimeCapability, - ScopeObject, ScopeValue, + ScopeObject, ScopeObjectMatch, ScopeValue, }; pub use channel::{Channel, JavaScriptChannelId}; pub use command::{private, CommandArg, CommandItem};