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
This commit is contained in:
Lucas Fernandes Nogueira 2024-10-21 15:16:08 -03:00 committed by GitHub
parent a5bf48eab0
commit f0da0bde87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 201 additions and 45 deletions

View File

@ -0,0 +1,5 @@
---
"tauri": patch:feat
---
Added `WebviewWindow::resolve_command_scope` to check a command scope at runtime.

View File

@ -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()

View File

@ -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<T: ScopeObject> {
}
impl<T: ScopeObject> CommandScope<T> {
pub(crate) fn resolve<R: Runtime>(
webview: &Webview<R>,
scope_ids: Vec<u64>,
) -> crate::Result<Self> {
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::<R, T>(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<Arc<T>> {
&self.allow
@ -698,29 +725,7 @@ impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T> {
.collect::<Vec<_>>()
});
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::<R, T>(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<T> {
pub struct GlobalScope<T: ScopeObject>(ScopeValue<T>);
impl<T: ScopeObject> GlobalScope<T> {
pub(crate) fn resolve<R: Runtime>(webview: &Webview<R>, plugin: &str) -> crate::Result<Self> {
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<Arc<T>> {
&self.0.allow
@ -749,20 +765,11 @@ impl<T: ScopeObject> GlobalScope<T> {
impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope<T> {
/// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`GlobalScope`].
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
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)
}
}

View File

@ -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<R: Runtime> Webview<R> {
.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<ScopeType>) {
/// // 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::<ScopeType>("my-plugin", "read");
/// Ok(())
/// });
/// ```
pub fn resolve_command_scope<T: ScopeObject>(
&self,
plugin: &str,
command: &str,
) -> crate::Result<Option<ResolvedScope<T>>> {
let current_url = self.url()?;
let is_local = self.is_local_url(&current_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::<Vec<_>>();
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<R> {
}
}
/// Resolved scope that can be obtained via [`Webview::resolve_command_scope`].
pub struct ResolvedScope<T: ScopeObject> {
command_scope: CommandScope<T>,
global_scope: GlobalScope<T>,
}
impl<T: ScopeObject> ResolvedScope<T> {
/// The global plugin scope.
pub fn global_scope(&self) -> &GlobalScope<T> {
&self.global_scope
}
/// The command-specific scope.
pub fn command_scope(&self) -> &CommandScope<T> {
&self.command_scope
}
}
#[cfg(test)]
mod tests {
#[test]

View File

@ -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<R>> {
@ -989,6 +990,52 @@ impl<R: Runtime> WebviewWindow<R> {
pub fn on_window_event<F: Fn(&WindowEvent) + Send + 'static>(&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<ScopeType>) {
/// // 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::<ScopeType>("my-plugin", "read");
/// Ok(())
/// });
/// ```
pub fn resolve_command_scope<T: ScopeObject>(
&self,
plugin: &str,
command: &str,
) -> crate::Result<Option<ResolvedScope<T>>> {
self.webview.resolve_command_scope(plugin, command)
}
}
/// Menu APIs
@ -1038,7 +1085,7 @@ impl<R: Runtime> WebviewWindow<R> {
self.window.on_menu_event(f)
}
/// Returns this window menu .
/// Returns this window menu.
pub fn menu(&self) -> Option<Menu<R>> {
self.window.menu()
}

View File

@ -19,10 +19,10 @@ pub struct Sample<R: Runtime>(AppHandle<R>);
impl<R: Runtime> Sample<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
let _ = payload.on_event.send(Event {
payload.on_event.send(Event {
kind: "ping".to_string(),
value: payload.value.clone(),
});
})?;
Ok(PingResponse {
value: payload.value,
})

View File

@ -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<T> = std::result::Result<T, Error>;