diff --git a/.changes/resolve_command_scope.md b/.changes/resolve_command_scope.md new file mode 100644 index 000000000..8245a8acb --- /dev/null +++ b/.changes/resolve_command_scope.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:feat +--- + +Added `WebviewWindow::resolve_command_scope` to check a command scope at runtime. diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index 4c2a29e34..0539935e3 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -204,7 +204,7 @@ fn get_str_array(helper: &Helper, formatter: impl Fn(&str) -> String) -> Option< .map(|val| { val.as_str().map( #[allow(clippy::redundant_closure)] - |s| formatter(s), + &formatter, ) }) .collect() diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs index 21e5f0da7..368a9034c 100644 --- a/crates/tauri/src/ipc/authority.rs +++ b/crates/tauri/src/ipc/authority.rs @@ -23,7 +23,7 @@ use tauri_utils::platform::Target; use url::Url; use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime}; -use crate::{AppHandle, Manager, StateManager}; +use crate::{AppHandle, Manager, StateManager, Webview}; use super::{CommandArg, CommandItem}; @@ -614,6 +614,33 @@ pub struct CommandScope { } impl CommandScope { + pub(crate) fn resolve( + webview: &Webview, + scope_ids: Vec, + ) -> crate::Result { + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + for scope_id in scope_ids { + let scope = webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_command_scope_typed::(webview.app_handle(), &scope_id)?; + + for s in scope.allows() { + allow.push(s.clone()); + } + for s in scope.denies() { + deny.push(s.clone()); + } + } + + Ok(CommandScope { allow, deny }) + } + /// What this access scope allows. pub fn allows(&self) -> &Vec> { &self.allow @@ -698,29 +725,7 @@ impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope { .collect::>() }); if let Some(scope_ids) = scope_ids { - let mut allow = Vec::new(); - let mut deny = Vec::new(); - - for scope_id in scope_ids { - let scope = command - .message - .webview - .manager() - .runtime_authority - .lock() - .unwrap() - .scope_manager - .get_command_scope_typed::(command.message.webview.app_handle(), &scope_id)?; - - for s in scope.allows() { - allow.push(s.clone()); - } - for s in scope.denies() { - deny.push(s.clone()); - } - } - - Ok(CommandScope { allow, deny }) + CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into) } else { Ok(CommandScope { allow: Default::default(), @@ -735,6 +740,17 @@ impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope { pub struct GlobalScope(ScopeValue); impl GlobalScope { + pub(crate) fn resolve(webview: &Webview, plugin: &str) -> crate::Result { + webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_global_scope_typed(webview.app_handle(), plugin) + .map(Self) + } + /// What this access scope allows. pub fn allows(&self) -> &Vec> { &self.0.allow @@ -749,20 +765,11 @@ impl GlobalScope { impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope { /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`GlobalScope`]. fn from_command(command: CommandItem<'a, R>) -> Result { - command - .message - .webview - .manager() - .runtime_authority - .lock() - .unwrap() - .scope_manager - .get_global_scope_typed( - command.message.webview.app_handle(), - command.plugin.unwrap_or(APP_ACL_KEY), - ) - .map_err(InvokeError::from_error) - .map(GlobalScope) + GlobalScope::resolve( + &command.message.webview, + command.plugin.unwrap_or(APP_ACL_KEY), + ) + .map_err(InvokeError::from_error) } } diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index 278b21d6d..e0df92ece 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -29,8 +29,8 @@ use crate::{ app::{UriSchemeResponder, WebviewEvent}, event::{EmitArgs, EventTarget}, ipc::{ - CallbackFn, CommandArg, CommandItem, Invoke, InvokeBody, InvokeError, InvokeMessage, - InvokeResolver, Origin, OwnedInvokeResponder, + CallbackFn, CommandArg, CommandItem, CommandScope, GlobalScope, Invoke, InvokeBody, + InvokeError, InvokeMessage, InvokeResolver, Origin, OwnedInvokeResponder, ScopeObject, }, manager::AppManager, sealed::{ManagerBase, RuntimeOrDispatch}, @@ -880,6 +880,83 @@ impl Webview { .dispatcher .on_webview_event(move |event| f(&event.clone().into())); } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + let current_url = self.url()?; + let is_local = self.is_local_url(¤t_url); + let origin = if is_local { + Origin::Local + } else { + Origin::Remote { url: current_url } + }; + + let cmd_name = format!("plugin:{plugin}|{command}"); + let resolved_access = self + .manager() + .runtime_authority + .lock() + .unwrap() + .resolve_access(&cmd_name, self.window().label(), self.label(), &origin); + + if let Some(access) = resolved_access { + let scope_ids = access + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect::>(); + + let command_scope = CommandScope::resolve(self, scope_ids)?; + let global_scope = GlobalScope::resolve(self, plugin)?; + + Ok(Some(ResolvedScope { + global_scope, + command_scope, + })) + } else { + Ok(None) + } + } } /// Desktop webview setters and actions. @@ -1702,6 +1779,24 @@ impl<'de, R: Runtime> CommandArg<'de, R> for Webview { } } +/// Resolved scope that can be obtained via [`Webview::resolve_command_scope`]. +pub struct ResolvedScope { + command_scope: CommandScope, + global_scope: GlobalScope, +} + +impl ResolvedScope { + /// The global plugin scope. + pub fn global_scope(&self) -> &GlobalScope { + &self.global_scope + } + + /// The command-specific scope. + pub fn command_scope(&self) -> &CommandScope { + &self.command_scope + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index 3d258ce14..ee1878e72 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -12,6 +12,7 @@ use std::{ use crate::{ event::EventTarget, + ipc::ScopeObject, runtime::dpi::{PhysicalPosition, PhysicalSize}, window::Monitor, Emitter, Listener, ResourceTable, Window, @@ -48,7 +49,7 @@ use tauri_macros::default_runtime; #[cfg(windows)] use windows::Win32::Foundation::HWND; -use super::DownloadEvent; +use super::{DownloadEvent, ResolvedScope}; /// A builder for [`WebviewWindow`], a window that hosts a single webview. pub struct WebviewWindowBuilder<'a, R: Runtime, M: Manager> { @@ -989,6 +990,52 @@ impl WebviewWindow { pub fn on_window_event(&self, f: F) { self.window.on_window_event(f); } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [crate::ipc::CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + self.webview.resolve_command_scope(plugin, command) + } } /// Menu APIs @@ -1038,7 +1085,7 @@ impl WebviewWindow { self.window.on_menu_event(f) } - /// Returns this window menu . + /// Returns this window menu. pub fn menu(&self) -> Option> { self.window.menu() } diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs index df5013d2c..d3b79bbc7 100644 --- a/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs +++ b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs @@ -19,10 +19,10 @@ pub struct Sample(AppHandle); impl Sample { pub fn ping(&self, payload: PingRequest) -> crate::Result { - let _ = payload.on_event.send(Event { + payload.on_event.send(Event { kind: "ping".to_string(), value: payload.value.clone(), - }); + })?; Ok(PingResponse { value: payload.value, }) diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/error.rs b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs index ca39ae0e0..c30dbabdc 100644 --- a/examples/api/src-tauri/tauri-plugin-sample/src/error.rs +++ b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs @@ -7,6 +7,8 @@ pub enum Error { #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + #[error(transparent)] + Tauri(#[from] tauri::Error), } pub type Result = std::result::Result;