From f0da0bde87a80fdca20c588cefcad86e03b9627c Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 21 Oct 2024 15:16:08 -0300 Subject: [PATCH] feat(core): add WebviewWindow::resolve_command_scope (#11439) * feat(core): add WebviewWindow::resolve_command_scope This new functionality exposes the `CommandScope` resolution as a function (currently only commands can resolve them as a dependency injection via CommandItem) This function is useful to validate the configuration at runtime (do some asserts at setup phase to ensure capabilities are properly configured) and to resolve scopes in a separate thread or context * adjust return type --- .changes/resolve_command_scope.md | 5 + crates/tauri-cli/src/mobile/init.rs | 2 +- crates/tauri/src/ipc/authority.rs | 83 +++++++++------- crates/tauri/src/webview/mod.rs | 99 ++++++++++++++++++- crates/tauri/src/webview/webview_window.rs | 51 +++++++++- .../tauri-plugin-sample/src/desktop.rs | 4 +- .../tauri-plugin-sample/src/error.rs | 2 + 7 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 .changes/resolve_command_scope.md 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;