From 4ef17d08336a2e0df4a7ef9adea746d7419710b6 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Thu, 7 Mar 2024 13:08:57 -0300 Subject: [PATCH] refactor(acl): use URLPattern instead of glob for remote URLs (#9116) --- .changes/acl-urlpattern.md | 6 + Cargo.lock | 57 +++ core/tauri-config-schema/schema.json | 2 +- core/tauri-utils/Cargo.toml | 2 + core/tauri-utils/src/acl/capability.rs | 2 +- core/tauri-utils/src/acl/mod.rs | 54 ++- core/tauri-utils/src/acl/resolved.rs | 7 +- core/tauri/Cargo.toml | 1 + core/tauri/src/ipc/authority.rs | 88 ++-- core/tauri/src/webview/mod.rs | 2 +- ...cl_tests__tests__file-explorer-remote.snap | 380 ++++++++++++------ tooling/cli/Cargo.lock | 56 +++ tooling/cli/schema.json | 2 +- 13 files changed, 490 insertions(+), 169 deletions(-) create mode 100644 .changes/acl-urlpattern.md diff --git a/.changes/acl-urlpattern.md b/.changes/acl-urlpattern.md new file mode 100644 index 000000000..3a2a6b4c1 --- /dev/null +++ b/.changes/acl-urlpattern.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:breaking +"tauri-utils": patch:breaking +--- + +The ACL configuration for remote URLs now uses the URLPattern standard instead of glob patterns. diff --git a/Cargo.lock b/Cargo.lock index 5da2a63d5..42684d88c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3642,6 +3642,7 @@ dependencies = [ "tracing", "tray-icon", "url", + "urlpattern", "uuid", "webkit2gtk", "webview2-com", @@ -3796,6 +3797,7 @@ dependencies = [ "phf 0.11.2", "proc-macro2", "quote", + "regex", "schemars", "semver", "serde", @@ -3806,6 +3808,7 @@ dependencies = [ "thiserror", "toml 0.8.2", "url", + "urlpattern", "walkdir", ] @@ -4179,6 +4182,47 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4234,6 +4278,19 @@ dependencies = [ "serde", ] +[[package]] +name = "urlpattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +dependencies = [ + "derive_more", + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 58dd4b891..d1f3bc5b4 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -1146,7 +1146,7 @@ ], "properties": { "urls": { - "description": "Remote domains this capability refers to. Can use glob patterns.", + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).", "type": "array", "items": { "type": "string" diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index e49f814a5..319ff89f6 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -33,6 +33,8 @@ json5 = { version = "0.4", optional = true } toml = { version = "0.8", features = [ "parse" ] } json-patch = "1.2" glob = "0.3" +urlpattern = "0.2" +regex = "1" walkdir = { version = "2", optional = true } memchr = "2" semver = "1" diff --git a/core/tauri-utils/src/acl/capability.rs b/core/tauri-utils/src/acl/capability.rs index 369eb4ddb..0ab01bbc7 100644 --- a/core/tauri-utils/src/acl/capability.rs +++ b/core/tauri-utils/src/acl/capability.rs @@ -98,7 +98,7 @@ fn default_platforms() -> Vec { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct CapabilityRemote { - /// Remote domains this capability refers to. Can use glob patterns. + /// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/). pub urls: Vec, } diff --git a/core/tauri-utils/src/acl/mod.rs b/core/tauri-utils/src/acl/mod.rs index 235126035..a7825dc57 100644 --- a/core/tauri-utils/src/acl/mod.rs +++ b/core/tauri-utils/src/acl/mod.rs @@ -4,10 +4,10 @@ //! Access Control List types. -use glob::Pattern; use serde::{Deserialize, Serialize}; -use std::num::NonZeroU64; +use std::{num::NonZeroU64, str::FromStr, sync::Arc}; use thiserror::Error; +use url::Url; use crate::platform::Target; @@ -204,16 +204,60 @@ pub struct PermissionSet { pub permissions: Vec, } +/// UrlPattern for [`ExecutionContext::Remote`]. +#[derive(Debug, Clone)] +pub struct RemoteUrlPattern(Arc, String); + +impl FromStr for RemoteUrlPattern { + type Err = urlpattern::quirks::Error; + + fn from_str(s: &str) -> std::result::Result { + let init = urlpattern::UrlPatternInit::parse_constructor_string::(s, None)?; + let pattern = urlpattern::UrlPattern::parse(init)?; + Ok(Self(Arc::new(pattern), s.to_string())) + } +} + +impl RemoteUrlPattern { + #[doc(hidden)] + pub fn as_str(&self) -> &str { + &self.1 + } + + /// Test if a given URL matches the pattern. + pub fn test(&self, url: &Url) -> bool { + self + .0 + .test(urlpattern::UrlPatternMatchInput::Url(url.clone())) + .unwrap_or_default() + } +} + +impl PartialEq for RemoteUrlPattern { + fn eq(&self, other: &Self) -> bool { + self.0.protocol() == other.0.protocol() + && self.0.username() == other.0.username() + && self.0.password() == other.0.password() + && self.0.hostname() == other.0.hostname() + && self.0.port() == other.0.port() + && self.0.pathname() == other.0.pathname() + && self.0.search() == other.0.search() + && self.0.hash() == other.0.hash() + } +} + +impl Eq for RemoteUrlPattern {} + /// Execution context of an IPC call. -#[derive(Debug, Default, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[derive(Debug, Default, Clone, Eq, PartialEq)] pub enum ExecutionContext { /// A local URL is used (the Tauri app URL). #[default] Local, /// Remote URL is tring to use the IPC. Remote { - /// The URL trying to access the IPC (glob pattern). - url: Pattern, + /// The URL trying to access the IPC (URL pattern). + url: RemoteUrlPattern, }, } diff --git a/core/tauri-utils/src/acl/resolved.rs b/core/tauri-utils/src/acl/resolved.rs index c9b5a7337..5c3cd2e24 100644 --- a/core/tauri-utils/src/acl/resolved.rs +++ b/core/tauri-utils/src/acl/resolved.rs @@ -6,8 +6,6 @@ use std::{collections::BTreeMap, fmt}; -use glob::Pattern; - use crate::platform::Target; use super::{ @@ -292,8 +290,9 @@ fn resolve_command( if let Some(remote) = &capability.remote { contexts.extend(remote.urls.iter().map(|url| { ExecutionContext::Remote { - url: Pattern::new(url) - .unwrap_or_else(|e| panic!("invalid glob pattern for remote URL {url}: {e}")), + url: url + .parse() + .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")), } })); } diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 7d197f0cb..73a554e7b 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -64,6 +64,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json", "st bytes = { version = "1", features = [ "serde" ] } raw-window-handle = "0.6" glob = "0.3" +urlpattern = "0.2" mime = "0.3" data-url = { version = "0.3", optional = true } serialize-to-javascript = "=0.1.1" diff --git a/core/tauri/src/ipc/authority.rs b/core/tauri/src/ipc/authority.rs index 34a7ba408..0fda44af3 100644 --- a/core/tauri/src/ipc/authority.rs +++ b/core/tauri/src/ipc/authority.rs @@ -20,6 +20,8 @@ use tauri_utils::acl::{ ExecutionContext, Scopes, }; +use url::Url; + use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime}; use crate::{AppHandle, Manager}; @@ -40,7 +42,7 @@ pub enum Origin { /// Remote origin. Remote { /// Remote URL. - url: String, + url: Url, }, } @@ -58,7 +60,7 @@ impl Origin { match (self, context) { (Self::Local, ExecutionContext::Local) => true, (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => { - url_pattern.matches(url) + url_pattern.test(url) } _ => false, } @@ -816,44 +818,7 @@ mod tests { let resolved_cmd = vec![ResolvedCommand { windows: vec![Pattern::new(window).unwrap()], context: ExecutionContext::Remote { - url: Pattern::new(url).unwrap(), - }, - ..Default::default() - }]; - let allowed_commands = [(command.to_string(), resolved_cmd.clone())] - .into_iter() - .collect(); - - let authority = RuntimeAuthority::new( - Default::default(), - Resolved { - allowed_commands, - ..Default::default() - }, - ); - - assert_eq!( - authority.resolve_access( - command, - window, - webview, - &Origin::Remote { url: url.into() } - ), - Some(resolved_cmd) - ); - } - - #[test] - fn remote_domain_glob_pattern_matches() { - let url = "http://tauri.*"; - let command = "my-command"; - let window = "main"; - let webview = "main"; - - let resolved_cmd = vec![ResolvedCommand { - windows: vec![Pattern::new(window).unwrap()], - context: ExecutionContext::Remote { - url: Pattern::new(url).unwrap(), + url: url.parse().unwrap(), }, ..Default::default() }]; @@ -875,7 +840,46 @@ mod tests { window, webview, &Origin::Remote { - url: url.replace('*', "studio") + url: url.parse().unwrap() + } + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_domain_glob_pattern_matches() { + let url = "http://tauri.*"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.replace('*', "studio").parse().unwrap() } ), Some(resolved_cmd) @@ -908,7 +912,7 @@ mod tests { window, webview, &Origin::Remote { - url: "https://tauri.app".into() + url: "https://tauri.app".parse().unwrap() } ) .is_none()); diff --git a/core/tauri/src/webview/mod.rs b/core/tauri/src/webview/mod.rs index cf5bd43b8..43b4d168f 100644 --- a/core/tauri/src/webview/mod.rs +++ b/core/tauri/src/webview/mod.rs @@ -1137,7 +1137,7 @@ fn main() { Origin::Local } else { Origin::Remote { - url: current_url.to_string(), + url: current_url.clone(), } }; let (resolved_acl, has_app_acl_manifest) = { diff --git a/core/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap b/core/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap index d51791b1e..994f5f61d 100644 --- a/core/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap +++ b/core/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap @@ -7,63 +7,139 @@ Resolved { "plugin:fs|read_dir": [ ResolvedCommand { context: Remote { - url: Pattern { - original: "https://tauri.app", - tokens: [ - Char( - 'h', - ), - Char( - 't', - ), - Char( - 't', - ), - Char( - 'p', - ), - Char( - 's', - ), - Char( - ':', - ), - Char( - '/', - ), - Char( - '/', - ), - Char( - 't', - ), - Char( - 'a', - ), - Char( - 'u', - ), - Char( - 'r', - ), - Char( - 'i', - ), - Char( - '.', - ), - Char( - 'a', - ), - Char( - 'p', - ), - Char( - 'p', - ), - ], - is_recursive: false, - }, + url: RemoteUrlPattern( + UrlPattern { + protocol: Component { + pattern_string: "https", + regexp: Ok( + Regex( + "^https$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "https", + }, + }, + }, + username: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + password: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + hostname: Component { + pattern_string: "tauri.app", + regexp: Ok( + Regex( + "^tauri\\.app$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "tauri.app", + }, + }, + }, + port: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + pathname: Component { + pattern_string: "/", + regexp: Ok( + Regex( + "^/$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "/", + }, + }, + }, + search: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + hash: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + }, + "https://tauri.app", + ), }, windows: [ Pattern { @@ -92,63 +168,139 @@ Resolved { "plugin:fs|read_file": [ ResolvedCommand { context: Remote { - url: Pattern { - original: "https://tauri.app", - tokens: [ - Char( - 'h', - ), - Char( - 't', - ), - Char( - 't', - ), - Char( - 'p', - ), - Char( - 's', - ), - Char( - ':', - ), - Char( - '/', - ), - Char( - '/', - ), - Char( - 't', - ), - Char( - 'a', - ), - Char( - 'u', - ), - Char( - 'r', - ), - Char( - 'i', - ), - Char( - '.', - ), - Char( - 'a', - ), - Char( - 'p', - ), - Char( - 'p', - ), - ], - is_recursive: false, - }, + url: RemoteUrlPattern( + UrlPattern { + protocol: Component { + pattern_string: "https", + regexp: Ok( + Regex( + "^https$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "https", + }, + }, + }, + username: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + password: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + hostname: Component { + pattern_string: "tauri.app", + regexp: Ok( + Regex( + "^tauri\\.app$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "tauri.app", + }, + }, + }, + port: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + pathname: Component { + pattern_string: "/", + regexp: Ok( + Regex( + "^/$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "/", + }, + }, + }, + search: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + hash: Component { + pattern_string: "", + regexp: Ok( + Regex( + "^$", + ), + ), + group_name_list: [], + matcher: Matcher { + prefix: "", + suffix: "", + inner: Literal { + literal: "", + }, + }, + }, + }, + "https://tauri.app", + ), }, windows: [ Pattern { diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index 018dab8ff..0d42bc005 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -4936,6 +4936,7 @@ dependencies = [ "log", "memchr", "phf 0.11.2", + "regex", "schemars", "semver", "serde", @@ -4945,6 +4946,7 @@ dependencies = [ "thiserror", "toml 0.8.10", "url", + "urlpattern", "walkdir", ] @@ -5359,6 +5361,47 @@ dependencies = [ "libc", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5475,6 +5518,19 @@ dependencies = [ "serde", ] +[[package]] +name = "urlpattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +dependencies = [ + "derive_more", + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "usvg" version = "0.40.0" diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 58dd4b891..d1f3bc5b4 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -1146,7 +1146,7 @@ ], "properties": { "urls": { - "description": "Remote domains this capability refers to. Can use glob patterns.", + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).", "type": "array", "items": { "type": "string"