1use serde::{Deserialize, Serialize};
25use std::{num::NonZeroU64, path::PathBuf, str::FromStr, sync::Arc};
26use thiserror::Error;
27use url::Url;
28
29use crate::platform::Target;
30
31pub use self::{identifier::*, value::*};
32
33pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
35pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
37pub const APP_ACL_KEY: &str = "__app-acl__";
39pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
41pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
43
44#[cfg(feature = "build")]
45pub mod build;
46pub mod capability;
47pub mod identifier;
48pub mod manifest;
49pub mod resolved;
50#[cfg(feature = "schema")]
51pub mod schema;
52pub mod value;
53
54#[derive(Debug, Error)]
56pub enum Error {
57 #[error("expected build script env var {0}, but it was not found - ensure this is called in a build script")]
61 BuildVar(&'static str),
62
63 #[error("package.links field in the Cargo manifest is not set, it should be set to the same as package.name")]
65 LinksMissing,
66
67 #[error(
69 "package.links field in the Cargo manifest MUST be set to the same value as package.name"
70 )]
71 LinksName,
72
73 #[error("failed to read file '{}': {}", _1.display(), _0)]
75 ReadFile(std::io::Error, PathBuf),
76
77 #[error("failed to write file '{}': {}", _1.display(), _0)]
79 WriteFile(std::io::Error, PathBuf),
80
81 #[error("failed to create file '{}': {}", _1.display(), _0)]
83 CreateFile(std::io::Error, PathBuf),
84
85 #[error("failed to create dir '{}': {}", _1.display(), _0)]
87 CreateDir(std::io::Error, PathBuf),
88
89 #[cfg(feature = "build")]
91 #[error("failed to execute: {0}")]
92 Metadata(#[from] ::cargo_metadata::Error),
93
94 #[error("failed to run glob: {0}")]
96 Glob(#[from] glob::PatternError),
97
98 #[error("failed to parse TOML: {0}")]
100 Toml(#[from] toml::de::Error),
101
102 #[error("failed to parse JSON: {0}")]
104 Json(#[from] serde_json::Error),
105
106 #[cfg(feature = "config-json5")]
108 #[error("failed to parse JSON5: {0}")]
109 Json5(#[from] json5::Error),
110
111 #[error("unknown permission format {0}")]
113 UnknownPermissionFormat(String),
114
115 #[error("unknown capability format {0}")]
117 UnknownCapabilityFormat(String),
118
119 #[error("permission {permission} not found from set {set}")]
121 SetPermissionNotFound {
122 permission: String,
124 set: String,
126 },
127
128 #[error("unknown ACL for {key}, expected one of {available}")]
130 UnknownManifest {
131 key: String,
133 available: String,
135 },
136
137 #[error("unknown permission {permission} for {key}")]
139 UnknownPermission {
140 key: String,
142
143 permission: String,
145 },
146
147 #[error("capability with identifier `{identifier}` already exists")]
149 CapabilityAlreadyExists {
150 identifier: String,
152 },
153}
154
155#[derive(Debug, Default, Serialize, Deserialize)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160pub struct Commands {
161 #[serde(default)]
163 pub allow: Vec<String>,
164
165 #[serde(default)]
167 pub deny: Vec<String>,
168}
169
170#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
185pub struct Scopes {
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub allow: Option<Vec<Value>>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub deny: Option<Vec<Value>>,
192}
193
194impl Scopes {
195 fn is_empty(&self) -> bool {
196 self.allow.is_none() && self.deny.is_none()
197 }
198}
199
200#[derive(Debug, Serialize, Deserialize, Default)]
206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
207pub struct Permission {
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub version: Option<NonZeroU64>,
211
212 pub identifier: String,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
219 pub description: Option<String>,
220
221 #[serde(default)]
223 pub commands: Commands,
224
225 #[serde(default, skip_serializing_if = "Scopes::is_empty")]
227 pub scope: Scopes,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub platforms: Option<Vec<Target>>,
232}
233
234impl Permission {
235 pub fn is_active(&self, target: &Target) -> bool {
237 self
238 .platforms
239 .as_ref()
240 .map(|platforms| platforms.contains(target))
241 .unwrap_or(true)
242 }
243}
244
245#[derive(Debug, Serialize, Deserialize)]
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
248pub struct PermissionSet {
249 pub identifier: String,
251
252 pub description: String,
254
255 pub permissions: Vec<String>,
257}
258
259#[derive(Debug, Clone)]
261pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
262
263impl FromStr for RemoteUrlPattern {
264 type Err = urlpattern::quirks::Error;
265
266 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
267 let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
268 if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
269 init.search.replace("*".to_string());
270 }
271 if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
272 init.hash.replace("*".to_string());
273 }
274 if init
275 .pathname
276 .as_ref()
277 .map(|p| p.is_empty() || p == "/")
278 .unwrap_or(true)
279 {
280 init.pathname.replace("*".to_string());
281 }
282 let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
283 Ok(Self(Arc::new(pattern), s.to_string()))
284 }
285}
286
287impl RemoteUrlPattern {
288 #[doc(hidden)]
289 pub fn as_str(&self) -> &str {
290 &self.1
291 }
292
293 pub fn test(&self, url: &Url) -> bool {
295 self
296 .0
297 .test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
298 .unwrap_or_default()
299 }
300}
301
302impl PartialEq for RemoteUrlPattern {
303 fn eq(&self, other: &Self) -> bool {
304 self.0.protocol() == other.0.protocol()
305 && self.0.username() == other.0.username()
306 && self.0.password() == other.0.password()
307 && self.0.hostname() == other.0.hostname()
308 && self.0.port() == other.0.port()
309 && self.0.pathname() == other.0.pathname()
310 && self.0.search() == other.0.search()
311 && self.0.hash() == other.0.hash()
312 }
313}
314
315impl Eq for RemoteUrlPattern {}
316
317#[derive(Debug, Default, Clone, Eq, PartialEq)]
319pub enum ExecutionContext {
320 #[default]
322 Local,
323 Remote {
325 url: RemoteUrlPattern,
327 },
328}
329
330#[cfg(test)]
331mod tests {
332 use crate::acl::RemoteUrlPattern;
333
334 #[test]
335 fn url_pattern_domain_wildcard() {
336 let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
337
338 assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
339 assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
340
341 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
342 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
343
344 let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
345
346 assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
347 assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
348 assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
349 assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
350 assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
351 assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
352 }
353
354 #[test]
355 fn url_pattern_path_wildcard() {
356 let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
357 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
358 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
359 }
360
361 #[test]
362 fn url_pattern_scheme_wildcard() {
363 let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
364 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
365 assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
366 assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
367 }
368}
369
370#[cfg(feature = "build")]
371mod build_ {
372 use std::convert::identity;
373
374 use crate::{literal_struct, tokens::*};
375
376 use super::*;
377 use proc_macro2::TokenStream;
378 use quote::{quote, ToTokens, TokenStreamExt};
379
380 impl ToTokens for ExecutionContext {
381 fn to_tokens(&self, tokens: &mut TokenStream) {
382 let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
383
384 tokens.append_all(match self {
385 Self::Local => {
386 quote! { #prefix::Local }
387 }
388 Self::Remote { url } => {
389 let url = url.as_str();
390 quote! { #prefix::Remote { url: #url.parse().unwrap() } }
391 }
392 });
393 }
394 }
395
396 impl ToTokens for Commands {
397 fn to_tokens(&self, tokens: &mut TokenStream) {
398 let allow = vec_lit(&self.allow, str_lit);
399 let deny = vec_lit(&self.deny, str_lit);
400 literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
401 }
402 }
403
404 impl ToTokens for Scopes {
405 fn to_tokens(&self, tokens: &mut TokenStream) {
406 let allow = opt_vec_lit(self.allow.as_ref(), identity);
407 let deny = opt_vec_lit(self.deny.as_ref(), identity);
408 literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
409 }
410 }
411
412 impl ToTokens for Permission {
413 fn to_tokens(&self, tokens: &mut TokenStream) {
414 let version = opt_lit_owned(self.version.as_ref().map(|v| {
415 let v = v.get();
416 quote!(::core::num::NonZeroU64::new(#v).unwrap())
417 }));
418 let identifier = str_lit(&self.identifier);
419 let description = opt_str_lit(self.description.as_ref());
420 let commands = &self.commands;
421 let scope = &self.scope;
422 let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
423
424 literal_struct!(
425 tokens,
426 ::tauri::utils::acl::Permission,
427 version,
428 identifier,
429 description,
430 commands,
431 scope,
432 platforms
433 )
434 }
435 }
436
437 impl ToTokens for PermissionSet {
438 fn to_tokens(&self, tokens: &mut TokenStream) {
439 let identifier = str_lit(&self.identifier);
440 let description = str_lit(&self.description);
441 let permissions = vec_lit(&self.permissions, str_lit);
442 literal_struct!(
443 tokens,
444 ::tauri::utils::acl::PermissionSet,
445 identifier,
446 description,
447 permissions
448 )
449 }
450 }
451}