refactor(acl): use URLPattern instead of glob for remote URLs (#9116)

This commit is contained in:
Lucas Fernandes Nogueira 2024-03-07 13:08:57 -03:00 committed by GitHub
parent 9dc9ca6e38
commit 4ef17d0833
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 490 additions and 169 deletions

View File

@ -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.

57
Cargo.lock generated
View File

@ -3642,6 +3642,7 @@ dependencies = [
"tracing", "tracing",
"tray-icon", "tray-icon",
"url", "url",
"urlpattern",
"uuid", "uuid",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
@ -3796,6 +3797,7 @@ dependencies = [
"phf 0.11.2", "phf 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex",
"schemars", "schemars",
"semver", "semver",
"serde", "serde",
@ -3806,6 +3808,7 @@ dependencies = [
"thiserror", "thiserror",
"toml 0.8.2", "toml 0.8.2",
"url", "url",
"urlpattern",
"walkdir", "walkdir",
] ]
@ -4179,6 +4182,47 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 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]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"
@ -4234,6 +4278,19 @@ dependencies = [
"serde", "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]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"

View File

@ -1146,7 +1146,7 @@
], ],
"properties": { "properties": {
"urls": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"

View File

@ -33,6 +33,8 @@ json5 = { version = "0.4", optional = true }
toml = { version = "0.8", features = [ "parse" ] } toml = { version = "0.8", features = [ "parse" ] }
json-patch = "1.2" json-patch = "1.2"
glob = "0.3" glob = "0.3"
urlpattern = "0.2"
regex = "1"
walkdir = { version = "2", optional = true } walkdir = { version = "2", optional = true }
memchr = "2" memchr = "2"
semver = "1" semver = "1"

View File

@ -98,7 +98,7 @@ fn default_platforms() -> Vec<Target> {
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CapabilityRemote { 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<String>, pub urls: Vec<String>,
} }

View File

@ -4,10 +4,10 @@
//! Access Control List types. //! Access Control List types.
use glob::Pattern;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::num::NonZeroU64; use std::{num::NonZeroU64, str::FromStr, sync::Arc};
use thiserror::Error; use thiserror::Error;
use url::Url;
use crate::platform::Target; use crate::platform::Target;
@ -204,16 +204,60 @@ pub struct PermissionSet {
pub permissions: Vec<String>, pub permissions: Vec<String>,
} }
/// UrlPattern for [`ExecutionContext::Remote`].
#[derive(Debug, Clone)]
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
impl FromStr for RemoteUrlPattern {
type Err = urlpattern::quirks::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(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. /// Execution context of an IPC call.
#[derive(Debug, Default, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] #[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum ExecutionContext { pub enum ExecutionContext {
/// A local URL is used (the Tauri app URL). /// A local URL is used (the Tauri app URL).
#[default] #[default]
Local, Local,
/// Remote URL is tring to use the IPC. /// Remote URL is tring to use the IPC.
Remote { Remote {
/// The URL trying to access the IPC (glob pattern). /// The URL trying to access the IPC (URL pattern).
url: Pattern, url: RemoteUrlPattern,
}, },
} }

View File

@ -6,8 +6,6 @@
use std::{collections::BTreeMap, fmt}; use std::{collections::BTreeMap, fmt};
use glob::Pattern;
use crate::platform::Target; use crate::platform::Target;
use super::{ use super::{
@ -292,8 +290,9 @@ fn resolve_command(
if let Some(remote) = &capability.remote { if let Some(remote) = &capability.remote {
contexts.extend(remote.urls.iter().map(|url| { contexts.extend(remote.urls.iter().map(|url| {
ExecutionContext::Remote { ExecutionContext::Remote {
url: Pattern::new(url) url: url
.unwrap_or_else(|e| panic!("invalid glob pattern for remote URL {url}: {e}")), .parse()
.unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
} }
})); }));
} }

View File

@ -64,6 +64,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json", "st
bytes = { version = "1", features = [ "serde" ] } bytes = { version = "1", features = [ "serde" ] }
raw-window-handle = "0.6" raw-window-handle = "0.6"
glob = "0.3" glob = "0.3"
urlpattern = "0.2"
mime = "0.3" mime = "0.3"
data-url = { version = "0.3", optional = true } data-url = { version = "0.3", optional = true }
serialize-to-javascript = "=0.1.1" serialize-to-javascript = "=0.1.1"

View File

@ -20,6 +20,8 @@ use tauri_utils::acl::{
ExecutionContext, Scopes, ExecutionContext, Scopes,
}; };
use url::Url;
use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime}; use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime};
use crate::{AppHandle, Manager}; use crate::{AppHandle, Manager};
@ -40,7 +42,7 @@ pub enum Origin {
/// Remote origin. /// Remote origin.
Remote { Remote {
/// Remote URL. /// Remote URL.
url: String, url: Url,
}, },
} }
@ -58,7 +60,7 @@ impl Origin {
match (self, context) { match (self, context) {
(Self::Local, ExecutionContext::Local) => true, (Self::Local, ExecutionContext::Local) => true,
(Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => { (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => {
url_pattern.matches(url) url_pattern.test(url)
} }
_ => false, _ => false,
} }
@ -816,44 +818,7 @@ mod tests {
let resolved_cmd = vec![ResolvedCommand { let resolved_cmd = vec![ResolvedCommand {
windows: vec![Pattern::new(window).unwrap()], windows: vec![Pattern::new(window).unwrap()],
context: ExecutionContext::Remote { context: ExecutionContext::Remote {
url: Pattern::new(url).unwrap(), 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.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(),
}, },
..Default::default() ..Default::default()
}]; }];
@ -875,7 +840,46 @@ mod tests {
window, window,
webview, webview,
&Origin::Remote { &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) Some(resolved_cmd)
@ -908,7 +912,7 @@ mod tests {
window, window,
webview, webview,
&Origin::Remote { &Origin::Remote {
url: "https://tauri.app".into() url: "https://tauri.app".parse().unwrap()
} }
) )
.is_none()); .is_none());

View File

@ -1137,7 +1137,7 @@ fn main() {
Origin::Local Origin::Local
} else { } else {
Origin::Remote { Origin::Remote {
url: current_url.to_string(), url: current_url.clone(),
} }
}; };
let (resolved_acl, has_app_acl_manifest) = { let (resolved_acl, has_app_acl_manifest) = {

View File

@ -7,63 +7,139 @@ Resolved {
"plugin:fs|read_dir": [ "plugin:fs|read_dir": [
ResolvedCommand { ResolvedCommand {
context: Remote { context: Remote {
url: Pattern { url: RemoteUrlPattern(
original: "https://tauri.app", UrlPattern {
tokens: [ protocol: Component {
Char( pattern_string: "https",
'h', regexp: Ok(
), Regex(
Char( "^https$",
't', ),
), ),
Char( group_name_list: [],
't', matcher: Matcher {
), prefix: "",
Char( suffix: "",
'p', inner: Literal {
), literal: "https",
Char( },
's', },
), },
Char( username: Component {
':', pattern_string: "",
), regexp: Ok(
Char( Regex(
'/', "^$",
), ),
Char( ),
'/', group_name_list: [],
), matcher: Matcher {
Char( prefix: "",
't', suffix: "",
), inner: Literal {
Char( literal: "",
'a', },
), },
Char( },
'u', password: Component {
), pattern_string: "",
Char( regexp: Ok(
'r', Regex(
), "^$",
Char( ),
'i', ),
), group_name_list: [],
Char( matcher: Matcher {
'.', prefix: "",
), suffix: "",
Char( inner: Literal {
'a', literal: "",
), },
Char( },
'p', },
), hostname: Component {
Char( pattern_string: "tauri.app",
'p', regexp: Ok(
), Regex(
], "^tauri\\.app$",
is_recursive: false, ),
}, ),
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: [ windows: [
Pattern { Pattern {
@ -92,63 +168,139 @@ Resolved {
"plugin:fs|read_file": [ "plugin:fs|read_file": [
ResolvedCommand { ResolvedCommand {
context: Remote { context: Remote {
url: Pattern { url: RemoteUrlPattern(
original: "https://tauri.app", UrlPattern {
tokens: [ protocol: Component {
Char( pattern_string: "https",
'h', regexp: Ok(
), Regex(
Char( "^https$",
't', ),
), ),
Char( group_name_list: [],
't', matcher: Matcher {
), prefix: "",
Char( suffix: "",
'p', inner: Literal {
), literal: "https",
Char( },
's', },
), },
Char( username: Component {
':', pattern_string: "",
), regexp: Ok(
Char( Regex(
'/', "^$",
), ),
Char( ),
'/', group_name_list: [],
), matcher: Matcher {
Char( prefix: "",
't', suffix: "",
), inner: Literal {
Char( literal: "",
'a', },
), },
Char( },
'u', password: Component {
), pattern_string: "",
Char( regexp: Ok(
'r', Regex(
), "^$",
Char( ),
'i', ),
), group_name_list: [],
Char( matcher: Matcher {
'.', prefix: "",
), suffix: "",
Char( inner: Literal {
'a', literal: "",
), },
Char( },
'p', },
), hostname: Component {
Char( pattern_string: "tauri.app",
'p', regexp: Ok(
), Regex(
], "^tauri\\.app$",
is_recursive: false, ),
}, ),
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: [ windows: [
Pattern { Pattern {

56
tooling/cli/Cargo.lock generated
View File

@ -4936,6 +4936,7 @@ dependencies = [
"log", "log",
"memchr", "memchr",
"phf 0.11.2", "phf 0.11.2",
"regex",
"schemars", "schemars",
"semver", "semver",
"serde", "serde",
@ -4945,6 +4946,7 @@ dependencies = [
"thiserror", "thiserror",
"toml 0.8.10", "toml 0.8.10",
"url", "url",
"urlpattern",
"walkdir", "walkdir",
] ]
@ -5359,6 +5361,47 @@ dependencies = [
"libc", "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]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"
@ -5475,6 +5518,19 @@ dependencies = [
"serde", "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]] [[package]]
name = "usvg" name = "usvg"
version = "0.40.0" version = "0.40.0"

View File

@ -1146,7 +1146,7 @@
], ],
"properties": { "properties": {
"urls": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"