From 10b80391fcdef28e26124505053fb3a4c4f85e75 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 19 Dec 2023 23:19:01 -0300 Subject: [PATCH] refactor(fs): use scope from tauri core (#825) * fix(fs): scope checks on Android On Android, when we call canonicalize() on "/data/user/0/appid" (which is the data dir), the result is a "/data/data/appid" path, so we need to adjust our scope for that. * use scope from core * update persisted-scope * fix build * dev branch --- .changes/fs-scope-tauri.md | 6 + Cargo.toml | 9 +- plugins/fs/src/config.rs | 52 +--- plugins/fs/src/lib.rs | 5 +- plugins/fs/src/scope.rs | 368 ----------------------------- plugins/persisted-scope/src/lib.rs | 33 +-- 6 files changed, 19 insertions(+), 454 deletions(-) create mode 100644 .changes/fs-scope-tauri.md delete mode 100644 plugins/fs/src/scope.rs diff --git a/.changes/fs-scope-tauri.md b/.changes/fs-scope-tauri.md new file mode 100644 index 00000000..a1444855 --- /dev/null +++ b/.changes/fs-scope-tauri.md @@ -0,0 +1,6 @@ +--- +"fs": patch +"persisted-scope": patch +--- + +Use `tauri::scope::fs::Scope` instead of local copy of its implementation. diff --git a/Cargo.toml b/Cargo.toml index 46738a1c..eb8a7180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["plugins/*", "plugins/*/tests/*", "plugins/*/examples/*/src-tauri", "examples/*/src-tauri"] +members = [ + "plugins/*", + "plugins/*/tests/*", + "plugins/*/examples/*/src-tauri", + "examples/*/src-tauri", +] resolver = "2" [workspace.dependencies] @@ -13,7 +18,7 @@ url = "2" [workspace.package] edition = "2021" -authors = [ "Tauri Programme within The Commons Conservancy" ] +authors = ["Tauri Programme within The Commons Conservancy"] license = "Apache-2.0 OR MIT" rust-version = "1.70" diff --git a/plugins/fs/src/config.rs b/plugins/fs/src/config.rs index f6c9b235..b05321f3 100644 --- a/plugins/fs/src/config.rs +++ b/plugins/fs/src/config.rs @@ -2,60 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; - use serde::Deserialize; +use tauri::utils::config::FsScope; #[derive(Debug, Deserialize)] pub struct Config { pub scope: FsScope, } - -/// Protocol scope definition. -/// It is a list of glob patterns that restrict the API access from the webview. -/// -/// Each pattern can start with a variable that resolves to a system base directory. -/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, -/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, -/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, -/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. -#[derive(Debug, PartialEq, Eq, Clone, Deserialize)] -#[serde(untagged)] -pub enum FsScope { - /// A list of paths that are allowed by this scope. - AllowedPaths(Vec), - /// A complete scope configuration. - Scope { - /// A list of paths that are allowed by this scope. - #[serde(default)] - allow: Vec, - /// A list of paths that are not allowed by this scope. - /// This gets precedence over the [`Self::Scope::allow`] list. - #[serde(default)] - deny: Vec, - }, -} - -impl Default for FsScope { - fn default() -> Self { - Self::AllowedPaths(Vec::new()) - } -} - -impl FsScope { - /// The list of allowed paths. - pub fn allowed_paths(&self) -> &Vec { - match self { - Self::AllowedPaths(p) => p, - Self::Scope { allow, .. } => allow, - } - } - - /// The list of forbidden paths. - pub fn forbidden_paths(&self) -> Option<&Vec> { - match self { - Self::AllowedPaths(_) => None, - Self::Scope { deny, .. } => Some(deny), - } - } -} diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index fda482d0..6b8d5994 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -11,22 +11,21 @@ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] -use config::FsScope; use tauri::{ plugin::{Builder as PluginBuilder, TauriPlugin}, + scope::fs::Scope, + utils::config::FsScope, FileDropEvent, Manager, RunEvent, Runtime, WindowEvent, }; mod commands; mod config; mod error; -mod scope; #[cfg(feature = "watch")] mod watcher; pub use config::Config; pub use error::Error; -pub use scope::{Event as ScopeEvent, Scope}; type Result = std::result::Result; diff --git a/plugins/fs/src/scope.rs b/plugins/fs/src/scope.rs deleted file mode 100644 index 609506fc..00000000 --- a/plugins/fs/src/scope.rs +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - collections::{HashMap, HashSet}, - fmt, - path::{Path, PathBuf, MAIN_SEPARATOR}, - sync::{Arc, Mutex}, -}; - -use crate::config::FsScope; -pub use glob::Pattern; -use uuid::Uuid; - -use crate::{Manager, Runtime}; - -/// Scope change event. -#[derive(Debug, Clone)] -pub enum Event { - /// A path has been allowed. - PathAllowed(PathBuf), - /// A path has been forbidden. - PathForbidden(PathBuf), -} - -type EventListener = Box; - -/// Scope for filesystem access. -#[derive(Clone)] -pub struct Scope { - allowed_patterns: Arc>>, - forbidden_patterns: Arc>>, - event_listeners: Arc>>, -} - -impl fmt::Debug for Scope { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Scope") - .field( - "allowed_patterns", - &self - .allowed_patterns - .lock() - .unwrap() - .iter() - .map(|p| p.as_str()) - .collect::>(), - ) - .field( - "forbidden_patterns", - &self - .forbidden_patterns - .lock() - .unwrap() - .iter() - .map(|p| p.as_str()) - .collect::>(), - ) - .finish() - } -} - -fn push_pattern, F: Fn(&str) -> Result>( - list: &mut HashSet, - pattern: P, - f: F, -) -> crate::Result<()> { - let path: PathBuf = pattern.as_ref().components().collect(); - list.insert(f(&path.to_string_lossy())?); - #[cfg(windows)] - { - if let Ok(p) = std::fs::canonicalize(&path) { - list.insert(f(&p.to_string_lossy())?); - } else { - list.insert(f(&format!("\\\\?\\{}", path.display()))?); - } - } - Ok(()) -} - -impl Scope { - /// Creates a new scope from a `FsScope` configuration. - pub(crate) fn new>( - manager: &M, - scope: &FsScope, - ) -> crate::Result { - let mut allowed_patterns = HashSet::new(); - for path in scope.allowed_paths() { - if let Ok(path) = manager.path().parse(path) { - push_pattern(&mut allowed_patterns, path, Pattern::new)?; - } - } - - let mut forbidden_patterns = HashSet::new(); - if let Some(forbidden_paths) = scope.forbidden_paths() { - for path in forbidden_paths { - if let Ok(path) = manager.path().parse(path) { - push_pattern(&mut forbidden_patterns, path, Pattern::new)?; - } - } - } - - Ok(Self { - allowed_patterns: Arc::new(Mutex::new(allowed_patterns)), - forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)), - event_listeners: Default::default(), - }) - } - - /// The list of allowed patterns. - pub fn allowed_patterns(&self) -> HashSet { - self.allowed_patterns.lock().unwrap().clone() - } - - /// The list of forbidden patterns. - pub fn forbidden_patterns(&self) -> HashSet { - self.forbidden_patterns.lock().unwrap().clone() - } - - /// Listen to an event on this scope. - pub fn listen(&self, f: F) -> Uuid { - let id = Uuid::new_v4(); - self.event_listeners.lock().unwrap().insert(id, Box::new(f)); - id - } - - fn trigger(&self, event: Event) { - let listeners = self.event_listeners.lock().unwrap(); - let handlers = listeners.values(); - for listener in handlers { - listener(&event); - } - } - - /// Extend the allowed patterns with the given directory. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read - /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. - pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { - let path = path.as_ref(); - { - let mut list = self.allowed_patterns.lock().unwrap(); - - // allow the directory to be read - push_pattern(&mut list, path, escaped_pattern)?; - // allow its files and subdirectories to be read - push_pattern(&mut list, path, |p| { - escaped_pattern_with(p, if recursive { "**" } else { "*" }) - })?; - } - self.trigger(Event::PathAllowed(path.to_path_buf())); - Ok(()) - } - - /// Extend the allowed patterns with the given file path. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. - pub fn allow_file>(&self, path: P) -> crate::Result<()> { - let path = path.as_ref(); - push_pattern( - &mut self.allowed_patterns.lock().unwrap(), - path, - escaped_pattern, - )?; - self.trigger(Event::PathAllowed(path.to_path_buf())); - Ok(()) - } - - /// Set the given directory path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { - let path = path.as_ref(); - { - let mut list = self.forbidden_patterns.lock().unwrap(); - - // allow the directory to be read - push_pattern(&mut list, path, escaped_pattern)?; - // allow its files and subdirectories to be read - push_pattern(&mut list, path, |p| { - escaped_pattern_with(p, if recursive { "**" } else { "*" }) - })?; - } - self.trigger(Event::PathForbidden(path.to_path_buf())); - Ok(()) - } - - /// Set the given file path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_file>(&self, path: P) -> crate::Result<()> { - let path = path.as_ref(); - push_pattern( - &mut self.forbidden_patterns.lock().unwrap(), - path, - escaped_pattern, - )?; - self.trigger(Event::PathForbidden(path.to_path_buf())); - Ok(()) - } - - /// Determines if the given path is allowed on this scope. - pub fn is_allowed>(&self, path: P) -> bool { - let path = path.as_ref(); - let path = if !path.exists() { - crate::Result::Ok(path.to_path_buf()) - } else { - std::fs::canonicalize(path).map_err(Into::into) - }; - - if let Ok(path) = path { - let path: PathBuf = path.components().collect(); - let options = glob::MatchOptions { - // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` - // see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5 - require_literal_separator: true, - // dotfiles are not supposed to be exposed by default - #[cfg(unix)] - require_literal_leading_dot: true, - ..Default::default() - }; - - let forbidden = self - .forbidden_patterns - .lock() - .unwrap() - .iter() - .any(|p| p.matches_path_with(&path, options)); - - if forbidden { - false - } else { - let allowed = self - .allowed_patterns - .lock() - .unwrap() - .iter() - .any(|p| p.matches_path_with(&path, options)); - allowed - } - } else { - false - } - } -} - -fn escaped_pattern(p: &str) -> Result { - Pattern::new(&glob::Pattern::escape(p)) -} - -fn escaped_pattern_with(p: &str, append: &str) -> Result { - Pattern::new(&format!( - "{}{}{append}", - glob::Pattern::escape(p), - MAIN_SEPARATOR - )) -} - -#[cfg(test)] -mod tests { - use super::Scope; - - fn new_scope() -> Scope { - Scope { - allowed_patterns: Default::default(), - forbidden_patterns: Default::default(), - event_listeners: Default::default(), - } - } - - #[test] - fn path_is_escaped() { - let scope = new_scope(); - #[cfg(unix)] - { - scope.allow_directory("/home/tauri/**", false).unwrap(); - assert!(scope.is_allowed("/home/tauri/**")); - assert!(scope.is_allowed("/home/tauri/**/file")); - assert!(!scope.is_allowed("/home/tauri/anyfile")); - } - #[cfg(windows)] - { - scope.allow_directory("C:\\home\\tauri\\**", false).unwrap(); - assert!(scope.is_allowed("C:\\home\\tauri\\**")); - assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); - assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); - } - - let scope = new_scope(); - #[cfg(unix)] - { - scope.allow_file("/home/tauri/**").unwrap(); - assert!(scope.is_allowed("/home/tauri/**")); - assert!(!scope.is_allowed("/home/tauri/**/file")); - assert!(!scope.is_allowed("/home/tauri/anyfile")); - } - #[cfg(windows)] - { - scope.allow_file("C:\\home\\tauri\\**").unwrap(); - assert!(scope.is_allowed("C:\\home\\tauri\\**")); - assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); - assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); - } - - let scope = new_scope(); - #[cfg(unix)] - { - scope.allow_directory("/home/tauri", true).unwrap(); - scope.forbid_directory("/home/tauri/**", false).unwrap(); - assert!(!scope.is_allowed("/home/tauri/**")); - assert!(!scope.is_allowed("/home/tauri/**/file")); - assert!(scope.is_allowed("/home/tauri/**/inner/file")); - assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile")); - assert!(scope.is_allowed("/home/tauri/anyfile")); - } - #[cfg(windows)] - { - scope.allow_directory("C:\\home\\tauri", true).unwrap(); - scope - .forbid_directory("C:\\home\\tauri\\**", false) - .unwrap(); - assert!(!scope.is_allowed("C:\\home\\tauri\\**")); - assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); - assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); - assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile")); - assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); - } - - let scope = new_scope(); - #[cfg(unix)] - { - scope.allow_directory("/home/tauri", true).unwrap(); - scope.forbid_file("/home/tauri/**").unwrap(); - assert!(!scope.is_allowed("/home/tauri/**")); - assert!(scope.is_allowed("/home/tauri/**/file")); - assert!(scope.is_allowed("/home/tauri/**/inner/file")); - assert!(scope.is_allowed("/home/tauri/anyfile")); - } - #[cfg(windows)] - { - scope.allow_directory("C:\\home\\tauri", true).unwrap(); - scope.forbid_file("C:\\home\\tauri\\**").unwrap(); - assert!(!scope.is_allowed("C:\\home\\tauri\\**")); - assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); - assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); - assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); - } - - let scope = new_scope(); - #[cfg(unix)] - { - scope.allow_directory("/home/tauri", false).unwrap(); - assert!(scope.is_allowed("/home/tauri/**")); - assert!(!scope.is_allowed("/home/tauri/**/file")); - assert!(!scope.is_allowed("/home/tauri/**/inner/file")); - assert!(scope.is_allowed("/home/tauri/anyfile")); - } - #[cfg(windows)] - { - scope.allow_directory("C:\\home\\tauri", false).unwrap(); - assert!(scope.is_allowed("C:\\home\\tauri\\**")); - assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); - assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); - assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); - } - } -} diff --git a/plugins/persisted-scope/src/lib.rs b/plugins/persisted-scope/src/lib.rs index 337a9c55..1d289071 100644 --- a/plugins/persisted-scope/src/lib.rs +++ b/plugins/persisted-scope/src/lib.rs @@ -13,14 +13,14 @@ use aho_corasick::AhoCorasick; use serde::{Deserialize, Serialize}; -#[cfg(feature = "protocol-asset")] + use tauri::scope::fs::{Event as FsScopeEvent, Scope as FsScope}; use tauri::{ plugin::{Builder, TauriPlugin}, scope::fs::Pattern as GlobPattern, Manager, Runtime, }; -use tauri_plugin_fs::{FsExt, Scope as FsPluginScope, ScopeEvent as FsPluginScopeEvent}; +use tauri_plugin_fs::FsExt; use std::{ collections::HashSet, @@ -58,33 +58,6 @@ trait ScopeExt { fn forbidden_patterns(&self) -> HashSet; } -impl ScopeExt for FsPluginScope { - fn allow_file(&self, path: &Path) { - let _ = FsPluginScope::allow_file(self, path); - } - - fn allow_directory(&self, path: &Path, recursive: bool) { - let _ = FsPluginScope::allow_directory(self, path, recursive); - } - - fn forbid_file(&self, path: &Path) { - let _ = FsPluginScope::forbid_file(self, path); - } - - fn forbid_directory(&self, path: &Path, recursive: bool) { - let _ = FsPluginScope::forbid_directory(self, path, recursive); - } - - fn allowed_patterns(&self) -> HashSet { - FsPluginScope::allowed_patterns(self) - } - - fn forbidden_patterns(&self) -> HashSet { - FsPluginScope::forbidden_patterns(self) - } -} - -#[cfg(feature = "protocol-asset")] impl ScopeExt for FsScope { fn allow_file(&self, path: &Path) { let _ = FsScope::allow_file(self, path); @@ -299,7 +272,7 @@ pub fn init() -> TauriPlugin { if let Some(fs_scope) = fs_scope { let fs_scope_ = fs_scope.clone(); fs_scope.listen(move |event| { - if let FsPluginScopeEvent::PathAllowed(_) = event { + if let FsScopeEvent::PathAllowed(_) = event { save_scopes(&fs_scope_, &app_dir, &fs_scope_state_path); } });