1use anyhow::Context;
25use capability::{Capability, CapabilityFile};
26use serde::{Deserialize, Serialize};
27use std::{
28 collections::{BTreeMap, HashSet},
29 fs,
30 num::NonZeroU64,
31 path::PathBuf,
32 str::FromStr,
33 sync::Arc,
34};
35use thiserror::Error;
36use url::Url;
37
38use crate::{
39 config::{CapabilityEntry, Config},
40 platform::Target,
41};
42
43pub use self::{identifier::*, value::*};
44
45pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
47pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
49pub const APP_ACL_KEY: &str = "__app-acl__";
51pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
53pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
55pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
57pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS";
60
61#[cfg(any(feature = "build", feature = "build-2"))]
62pub mod build;
63pub mod capability;
64pub mod identifier;
65pub mod manifest;
66pub mod resolved;
67#[cfg(feature = "schema")]
68pub mod schema;
69pub mod value;
70
71#[derive(Debug, Error)]
73pub enum Error {
74 #[error(
78 "expected build script env var {0}, but it was not found - ensure this is called in a build script"
79 )]
80 BuildVar(&'static str),
81
82 #[error(
84 "package.links field in the Cargo manifest is not set, it should be set to the same as package.name"
85 )]
86 LinksMissing,
87
88 #[error(
90 "package.links field in the Cargo manifest MUST be set to the same value as package.name"
91 )]
92 LinksName,
93
94 #[error("failed to read file '{}': {}", _1.display(), _0)]
96 ReadFile(std::io::Error, PathBuf),
97
98 #[error("failed to write file '{}': {}", _1.display(), _0)]
100 WriteFile(std::io::Error, PathBuf),
101
102 #[error("failed to create file '{}': {}", _1.display(), _0)]
104 CreateFile(std::io::Error, PathBuf),
105
106 #[error("failed to create dir '{}': {}", _1.display(), _0)]
108 CreateDir(std::io::Error, PathBuf),
109
110 #[cfg(any(feature = "build", feature = "build-2"))]
112 #[error("failed to execute: {0}")]
113 Metadata(#[from] ::cargo_metadata::Error),
114
115 #[error("failed to run glob: {0}")]
117 Glob(#[from] glob::PatternError),
118
119 #[error("failed to parse TOML: {0}")]
121 Toml(#[from] toml::de::Error),
122
123 #[error("failed to parse JSON: {0}")]
125 Json(#[from] serde_json::Error),
126
127 #[cfg(feature = "config-json5")]
129 #[error("failed to parse JSON5: {0}")]
130 Json5(#[from] json5::Error),
131
132 #[error("unknown permission format {0}")]
134 UnknownPermissionFormat(String),
135
136 #[error("unknown capability format {0}")]
138 UnknownCapabilityFormat(String),
139
140 #[error("permission {permission} not found from set {set}")]
142 SetPermissionNotFound {
143 permission: String,
145 set: String,
147 },
148
149 #[error("unknown ACL for {key}, expected one of {available}")]
151 UnknownManifest {
152 key: String,
154 available: String,
156 },
157
158 #[error("unknown permission {permission} for {key}")]
160 UnknownPermission {
161 key: String,
163
164 permission: String,
166 },
167
168 #[error("capability with identifier `{identifier}` already exists")]
170 CapabilityAlreadyExists {
171 identifier: String,
173 },
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181pub struct Commands {
182 #[serde(default)]
184 pub allow: Vec<String>,
185
186 #[serde(default)]
188 pub deny: Vec<String>,
189}
190
191#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
205#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
206pub struct Scopes {
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub allow: Option<Vec<Value>>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub deny: Option<Vec<Value>>,
213}
214
215impl Scopes {
216 fn is_empty(&self) -> bool {
217 self.allow.is_none() && self.deny.is_none()
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
228pub struct Permission {
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub version: Option<NonZeroU64>,
232
233 pub identifier: String,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
240 pub description: Option<String>,
241
242 #[serde(default)]
244 pub commands: Commands,
245
246 #[serde(default, skip_serializing_if = "Scopes::is_empty")]
248 pub scope: Scopes,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub platforms: Option<Vec<Target>>,
253}
254
255impl Permission {
256 pub fn is_active(&self, target: &Target) -> bool {
258 self
259 .platforms
260 .as_ref()
261 .map(|platforms| platforms.contains(target))
262 .unwrap_or(true)
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
269pub struct PermissionSet {
270 pub identifier: String,
272
273 pub description: String,
275
276 pub permissions: Vec<String>,
278}
279
280#[derive(Debug, Clone)]
282pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
283
284impl FromStr for RemoteUrlPattern {
285 type Err = urlpattern::quirks::Error;
286
287 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
288 let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
289 if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
290 init.search.replace("*".to_string());
291 }
292 if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
293 init.hash.replace("*".to_string());
294 }
295 if init
296 .pathname
297 .as_ref()
298 .map(|p| p.is_empty() || p == "/")
299 .unwrap_or(true)
300 {
301 init.pathname.replace("*".to_string());
302 }
303 let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
304 Ok(Self(Arc::new(pattern), s.to_string()))
305 }
306}
307
308impl RemoteUrlPattern {
309 #[doc(hidden)]
310 pub fn as_str(&self) -> &str {
311 &self.1
312 }
313
314 pub fn test(&self, url: &Url) -> bool {
316 self
317 .0
318 .test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
319 .unwrap_or_default()
320 }
321}
322
323impl PartialEq for RemoteUrlPattern {
324 fn eq(&self, other: &Self) -> bool {
325 self.0.protocol() == other.0.protocol()
326 && self.0.username() == other.0.username()
327 && self.0.password() == other.0.password()
328 && self.0.hostname() == other.0.hostname()
329 && self.0.port() == other.0.port()
330 && self.0.pathname() == other.0.pathname()
331 && self.0.search() == other.0.search()
332 && self.0.hash() == other.0.hash()
333 }
334}
335
336impl Eq for RemoteUrlPattern {}
337
338#[derive(Debug, Default, Clone, Eq, PartialEq)]
340pub enum ExecutionContext {
341 #[default]
343 Local,
344 Remote {
346 url: RemoteUrlPattern,
348 },
349}
350
351pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
353 acl.contains_key(APP_ACL_KEY)
354}
355
356pub fn get_capabilities(
358 config: &Config,
359 mut capabilities_from_files: BTreeMap<String, Capability>,
360 additional_capability_files: Option<&[PathBuf]>,
361) -> anyhow::Result<BTreeMap<String, Capability>> {
362 let mut capabilities = if config.app.security.capabilities.is_empty() {
363 capabilities_from_files
364 } else {
365 let mut capabilities = BTreeMap::new();
366 for capability_entry in &config.app.security.capabilities {
367 match capability_entry {
368 CapabilityEntry::Inlined(capability) => {
369 capabilities.insert(capability.identifier.clone(), capability.clone());
370 }
371 CapabilityEntry::Reference(id) => {
372 let capability = capabilities_from_files
373 .remove(id)
374 .with_context(|| format!("capability with identifier {id} not found"))?;
375 capabilities.insert(id.clone(), capability);
376 }
377 }
378 }
379 capabilities
380 };
381
382 if let Some(paths) = additional_capability_files {
383 for path in paths {
384 let capability = CapabilityFile::load(path)
385 .with_context(|| format!("failed to read capability {}", path.display()))?;
386 match capability {
387 CapabilityFile::Capability(c) => {
388 capabilities.insert(c.identifier.clone(), c);
389 }
390 CapabilityFile::List(capabilities_list)
391 | CapabilityFile::NamedList {
392 capabilities: capabilities_list,
393 } => {
394 capabilities.extend(
395 capabilities_list
396 .into_iter()
397 .map(|c| (c.identifier.clone(), c)),
398 );
399 }
400 }
401 }
402 }
403
404 Ok(capabilities)
405}
406
407#[derive(Debug, Default, Serialize, Deserialize)]
409pub struct AllowedCommands {
410 pub commands: HashSet<String>,
412 pub has_app_acl: bool,
414}
415
416pub fn read_allowed_commands() -> Option<AllowedCommands> {
418 let out_file = std::env::var("OUT_DIR")
419 .map(PathBuf::from)
420 .ok()?
421 .join(ALLOWED_COMMANDS_FILE_NAME);
422 let file = fs::read_to_string(&out_file).ok()?;
423 let json = serde_json::from_str(&file).ok()?;
424 Some(json)
425}
426
427#[cfg(test)]
428mod tests {
429 use crate::acl::RemoteUrlPattern;
430
431 #[test]
432 fn url_pattern_domain_wildcard() {
433 let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
434
435 assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
436 assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
437
438 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
439 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
440
441 let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
442
443 assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
444 assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
445 assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
446 assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
447 assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
448 assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
449 }
450
451 #[test]
452 fn url_pattern_path_wildcard() {
453 let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
454 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
455 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
456 }
457
458 #[test]
459 fn url_pattern_scheme_wildcard() {
460 let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
461 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
462 assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
463 assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
464 }
465}
466
467#[cfg(any(feature = "build", feature = "build-2"))]
468mod build_ {
469 use std::convert::identity;
470
471 use crate::{literal_struct, tokens::*};
472
473 use super::*;
474 use proc_macro2::TokenStream;
475 use quote::{ToTokens, TokenStreamExt, quote};
476
477 impl ToTokens for ExecutionContext {
478 fn to_tokens(&self, tokens: &mut TokenStream) {
479 let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
480
481 tokens.append_all(match self {
482 Self::Local => {
483 quote! { #prefix::Local }
484 }
485 Self::Remote { url } => {
486 let url = url.as_str();
487 quote! { #prefix::Remote { url: #url.parse().unwrap() } }
488 }
489 });
490 }
491 }
492
493 impl ToTokens for Commands {
494 fn to_tokens(&self, tokens: &mut TokenStream) {
495 let allow = vec_lit(&self.allow, str_lit);
496 let deny = vec_lit(&self.deny, str_lit);
497 literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
498 }
499 }
500
501 impl ToTokens for Scopes {
502 fn to_tokens(&self, tokens: &mut TokenStream) {
503 let allow = opt_vec_lit(self.allow.as_ref(), identity);
504 let deny = opt_vec_lit(self.deny.as_ref(), identity);
505 literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
506 }
507 }
508
509 impl ToTokens for Permission {
510 fn to_tokens(&self, tokens: &mut TokenStream) {
511 let version = opt_lit_owned(self.version.as_ref().map(|v| {
512 let v = v.get();
513 quote!(::core::num::NonZeroU64::new(#v).unwrap())
514 }));
515 let identifier = str_lit(&self.identifier);
516 let description = quote! { ::core::option::Option::None };
518 let commands = &self.commands;
519 let scope = &self.scope;
520 let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
521
522 literal_struct!(
523 tokens,
524 ::tauri::utils::acl::Permission,
525 version,
526 identifier,
527 description,
528 commands,
529 scope,
530 platforms
531 )
532 }
533 }
534
535 impl ToTokens for PermissionSet {
536 fn to_tokens(&self, tokens: &mut TokenStream) {
537 let identifier = str_lit(&self.identifier);
538 let description = quote! { "".to_string() };
540 let permissions = vec_lit(&self.permissions, str_lit);
541 literal_struct!(
542 tokens,
543 ::tauri::utils::acl::PermissionSet,
544 identifier,
545 description,
546 permissions
547 )
548 }
549 }
550}