1use anyhow::Context;
25use capability::{Capability, CapabilityFile};
26use serde::{Deserialize, Serialize};
27use std::{
28 collections::{BTreeMap, HashSet},
29 fs,
30 num::NonZeroU64,
31 path::{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 pre_built_capabilities_file_path: Option<&Path>,
356 additional_capability_files: Option<&[PathBuf]>,
357) -> anyhow::Result<BTreeMap<String, Capability>> {
358 let mut capabilities_from_files: BTreeMap<String, Capability> = BTreeMap::new();
359 if let Some(capabilities_file_path) = pre_built_capabilities_file_path {
360 if capabilities_file_path.exists() {
361 let capabilities_file =
362 std::fs::read_to_string(capabilities_file_path).context("failed to read capabilities")?;
363 capabilities_from_files =
364 serde_json::from_str(&capabilities_file).context("failed to parse capabilities")?;
365 }
366 }
367
368 let mut capabilities = if config.app.security.capabilities.is_empty() {
369 capabilities_from_files
370 } else {
371 let mut capabilities = BTreeMap::new();
372 for capability_entry in &config.app.security.capabilities {
373 match capability_entry {
374 CapabilityEntry::Inlined(capability) => {
375 capabilities.insert(capability.identifier.clone(), capability.clone());
376 }
377 CapabilityEntry::Reference(id) => {
378 let capability = capabilities_from_files
379 .remove(id)
380 .with_context(|| format!("capability with identifier {id} not found"))?;
381 capabilities.insert(id.clone(), capability);
382 }
383 }
384 }
385 capabilities
386 };
387
388 if let Some(paths) = additional_capability_files {
389 for path in paths {
390 let capability = CapabilityFile::load(path)
391 .with_context(|| format!("failed to read capability {}", path.display()))?;
392 match capability {
393 CapabilityFile::Capability(c) => {
394 capabilities.insert(c.identifier.clone(), c);
395 }
396 CapabilityFile::List(capabilities_list)
397 | CapabilityFile::NamedList {
398 capabilities: capabilities_list,
399 } => {
400 capabilities.extend(
401 capabilities_list
402 .into_iter()
403 .map(|c| (c.identifier.clone(), c)),
404 );
405 }
406 }
407 }
408 }
409
410 Ok(capabilities)
411}
412
413#[derive(Debug, Default, Serialize, Deserialize)]
415pub struct AllowedCommands {
416 pub commands: HashSet<String>,
418 pub has_app_acl: bool,
420}
421
422pub fn read_allowed_commands() -> Option<AllowedCommands> {
424 let out_file = std::env::var("OUT_DIR")
425 .map(PathBuf::from)
426 .ok()?
427 .join(ALLOWED_COMMANDS_FILE_NAME);
428 let file = fs::read_to_string(&out_file).ok()?;
429 let json = serde_json::from_str(&file).ok()?;
430 Some(json)
431}
432
433#[cfg(test)]
434mod tests {
435 use crate::acl::RemoteUrlPattern;
436
437 #[test]
438 fn url_pattern_domain_wildcard() {
439 let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
440
441 assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
442 assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
443
444 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
445 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
446
447 let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
448
449 assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
450 assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
451 assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
452 assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
453 assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
454 assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
455 }
456
457 #[test]
458 fn url_pattern_path_wildcard() {
459 let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
460 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
461 assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
462 }
463
464 #[test]
465 fn url_pattern_scheme_wildcard() {
466 let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
467 assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
468 assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
469 assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
470 }
471}
472
473#[cfg(feature = "build")]
474mod build_ {
475 use std::convert::identity;
476
477 use crate::{literal_struct, tokens::*};
478
479 use super::*;
480 use proc_macro2::TokenStream;
481 use quote::{quote, ToTokens, TokenStreamExt};
482
483 impl ToTokens for ExecutionContext {
484 fn to_tokens(&self, tokens: &mut TokenStream) {
485 let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
486
487 tokens.append_all(match self {
488 Self::Local => {
489 quote! { #prefix::Local }
490 }
491 Self::Remote { url } => {
492 let url = url.as_str();
493 quote! { #prefix::Remote { url: #url.parse().unwrap() } }
494 }
495 });
496 }
497 }
498
499 impl ToTokens for Commands {
500 fn to_tokens(&self, tokens: &mut TokenStream) {
501 let allow = vec_lit(&self.allow, str_lit);
502 let deny = vec_lit(&self.deny, str_lit);
503 literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
504 }
505 }
506
507 impl ToTokens for Scopes {
508 fn to_tokens(&self, tokens: &mut TokenStream) {
509 let allow = opt_vec_lit(self.allow.as_ref(), identity);
510 let deny = opt_vec_lit(self.deny.as_ref(), identity);
511 literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
512 }
513 }
514
515 impl ToTokens for Permission {
516 fn to_tokens(&self, tokens: &mut TokenStream) {
517 let version = opt_lit_owned(self.version.as_ref().map(|v| {
518 let v = v.get();
519 quote!(::core::num::NonZeroU64::new(#v).unwrap())
520 }));
521 let identifier = str_lit(&self.identifier);
522 let description = quote! { ::core::option::Option::None };
524 let commands = &self.commands;
525 let scope = &self.scope;
526 let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
527
528 literal_struct!(
529 tokens,
530 ::tauri::utils::acl::Permission,
531 version,
532 identifier,
533 description,
534 commands,
535 scope,
536 platforms
537 )
538 }
539 }
540
541 impl ToTokens for PermissionSet {
542 fn to_tokens(&self, tokens: &mut TokenStream) {
543 let identifier = str_lit(&self.identifier);
544 let description = quote! { "".to_string() };
546 let permissions = vec_lit(&self.permissions, str_lit);
547 literal_struct!(
548 tokens,
549 ::tauri::utils::acl::PermissionSet,
550 identifier,
551 description,
552 permissions
553 )
554 }
555 }
556}