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