Skip to content

Commit 5621174

Browse files
authored
feat: add ScopeObjectMatch trait for easy scope validation (#11132)
1 parent 2a654fd commit 5621174

File tree

3 files changed

+114
-1
lines changed

3 files changed

+114
-1
lines changed

.changes/scope-object-match.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch:feat
3+
---
4+
5+
Add `ScopeObjectMatch` for easy scope validation those that can be represented by a boolean return value.

crates/tauri/src/ipc/authority.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,69 @@ impl<T: ScopeObject> CommandScope<T> {
625625
}
626626
}
627627

628+
impl<T: ScopeObjectMatch> CommandScope<T> {
629+
/// Ensure all deny scopes were not matched and any allow scopes were.
630+
///
631+
/// This **WILL** return `true` if the allow scopes are empty and the deny
632+
/// scopes did not trigger. If you require at least one allow scope, then
633+
/// ensure the allow scopes are not empty before calling this method.
634+
///
635+
/// ```
636+
/// # use tauri::ipc::CommandScope;
637+
/// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> {
638+
/// if scope.allows().is_empty() {
639+
/// return Err("you need to specify at least 1 allow scope!");
640+
/// }
641+
/// # Ok(())
642+
/// # }
643+
/// ```
644+
///
645+
/// # Example
646+
///
647+
/// ```
648+
/// # use serde::{Serialize, Deserialize};
649+
/// # use url::Url;
650+
/// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command};
651+
/// #
652+
/// #[derive(Debug, Clone, Serialize, Deserialize)]
653+
/// # pub struct Scope;
654+
/// #
655+
/// # impl ScopeObjectMatch for Scope {
656+
/// # type Input = str;
657+
/// #
658+
/// # fn matches(&self, input: &str) -> bool {
659+
/// # true
660+
/// # }
661+
/// # }
662+
/// #
663+
/// # fn do_work(_: String) -> Result<String, &'static str> {
664+
/// # Ok("Output".into())
665+
/// # }
666+
/// #
667+
/// #[command]
668+
/// fn my_command(scope: CommandScope<Scope>, input: String) -> Result<String, &'static str> {
669+
/// if scope.matches(&input) {
670+
/// do_work(input)
671+
/// } else {
672+
/// Err("Scope didn't match input")
673+
/// }
674+
/// }
675+
/// ```
676+
pub fn matches(&self, input: &T::Input) -> bool {
677+
// first make sure the input doesn't match any existing deny scope
678+
if self.deny.iter().any(|s| s.matches(input)) {
679+
return false;
680+
}
681+
682+
// if there are allow scopes, ensure the input matches at least 1
683+
if self.allow.is_empty() {
684+
true
685+
} else {
686+
self.allow.iter().any(|s| s.matches(input))
687+
}
688+
}
689+
}
690+
628691
impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T> {
629692
/// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`].
630693
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
@@ -729,6 +792,51 @@ impl<T: Send + Sync + Debug + DeserializeOwned + 'static> ScopeObject for T {
729792
}
730793
}
731794

795+
/// A [`ScopeObject`] whose validation can be represented as a `bool`.
796+
///
797+
/// # Example
798+
///
799+
/// ```
800+
/// # use serde::{Deserialize, Serialize};
801+
/// # use tauri::{ipc::ScopeObjectMatch, Url};
802+
/// #
803+
/// #[derive(Debug, Clone, Serialize, Deserialize)]
804+
/// #[serde(rename_all = "camelCase")]
805+
/// pub enum Scope {
806+
/// Domain(Url),
807+
/// StartsWith(String),
808+
/// }
809+
///
810+
/// impl ScopeObjectMatch for Scope {
811+
/// type Input = str;
812+
///
813+
/// fn matches(&self, input: &str) -> bool {
814+
/// match self {
815+
/// Scope::Domain(url) => {
816+
/// let parsed: Url = match input.parse() {
817+
/// Ok(parsed) => parsed,
818+
/// Err(_) => return false,
819+
/// };
820+
///
821+
/// let domain = parsed.domain();
822+
///
823+
/// domain.is_some() && domain == url.domain()
824+
/// }
825+
/// Scope::StartsWith(start) => input.starts_with(start),
826+
/// }
827+
/// }
828+
/// }
829+
/// ```
830+
pub trait ScopeObjectMatch: ScopeObject {
831+
/// The type of input expected to validate against the scope.
832+
///
833+
/// This will be borrowed, so if you want to match on a `&str` this type should be `str`.
834+
type Input: ?Sized;
835+
836+
/// Check if the input matches against the scope.
837+
fn matches(&self, input: &Self::Input) -> bool;
838+
}
839+
732840
impl ScopeManager {
733841
pub(crate) fn get_global_scope_typed<R: Runtime, T: ScopeObject>(
734842
&self,

crates/tauri/src/ipc/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub(crate) mod protocol;
2929

3030
pub use authority::{
3131
CapabilityBuilder, CommandScope, GlobalScope, Origin, RuntimeAuthority, RuntimeCapability,
32-
ScopeObject, ScopeValue,
32+
ScopeObject, ScopeObjectMatch, ScopeValue,
3333
};
3434
pub use channel::{Channel, JavaScriptChannelId};
3535
pub use command::{private, CommandArg, CommandItem};

0 commit comments

Comments
 (0)