tauri_utils/acl/
mod.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Access Control List types.
6//!
7//! # Stability
8//!
9//! This is a core functionality that is not considered part of the stable API.
10//! If you use it, note that it may include breaking changes in the future.
11//!
12//! These items are intended to be non-breaking from a de/serialization standpoint only.
13//! Using and modifying existing config values will try to avoid breaking changes, but they are
14//! free to add fields in the future - causing breaking changes for creating and full destructuring.
15//!
16//! To avoid this, [ignore unknown fields when destructuring] with the `{my, config, ..}` pattern.
17//! If you need to create the Rust config directly without deserializing, then create the struct
18//! the [Struct Update Syntax] with `..Default::default()`, which may need a
19//! `#[allow(clippy::needless_update)]` attribute if you are declaring all fields.
20//!
21//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
22//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
23
24use 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
45/// Known foldername of the permission schema files
46pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
47/// Known filename of the permission schema JSON file
48pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
49/// Known ACL key for the app permissions.
50pub const APP_ACL_KEY: &str = "__app-acl__";
51/// Known acl manifests file
52pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
53/// Known capabilityies file
54pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
55/// Allowed commands file name
56pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
57/// Set by the CLI with when `build > removeUnusedCommands` is set for dead code elimination,
58/// the value is set to the config's directory
59pub 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/// Possible errors while processing ACL files.
72#[derive(Debug, Error)]
73pub enum Error {
74  /// Could not find an environmental variable that is set inside of build scripts.
75  ///
76  /// Whatever generated this should be called inside of a build script.
77  #[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  /// The links field in the manifest **MUST** be set and match the name of the crate.
81  #[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  /// The links field in the manifest **MUST** match the name of the crate.
85  #[error(
86    "package.links field in the Cargo manifest MUST be set to the same value as package.name"
87  )]
88  LinksName,
89
90  /// IO error while reading a file
91  #[error("failed to read file '{}': {}", _1.display(), _0)]
92  ReadFile(std::io::Error, PathBuf),
93
94  /// IO error while writing a file
95  #[error("failed to write file '{}': {}", _1.display(), _0)]
96  WriteFile(std::io::Error, PathBuf),
97
98  /// IO error while creating a file
99  #[error("failed to create file '{}': {}", _1.display(), _0)]
100  CreateFile(std::io::Error, PathBuf),
101
102  /// IO error while creating a dir
103  #[error("failed to create dir '{}': {}", _1.display(), _0)]
104  CreateDir(std::io::Error, PathBuf),
105
106  /// [`cargo_metadata`] was not able to complete successfully
107  #[cfg(feature = "build")]
108  #[error("failed to execute: {0}")]
109  Metadata(#[from] ::cargo_metadata::Error),
110
111  /// Invalid glob
112  #[error("failed to run glob: {0}")]
113  Glob(#[from] glob::PatternError),
114
115  /// Invalid TOML encountered
116  #[error("failed to parse TOML: {0}")]
117  Toml(#[from] toml::de::Error),
118
119  /// Invalid JSON encountered
120  #[error("failed to parse JSON: {0}")]
121  Json(#[from] serde_json::Error),
122
123  /// Invalid JSON5 encountered
124  #[cfg(feature = "config-json5")]
125  #[error("failed to parse JSON5: {0}")]
126  Json5(#[from] json5::Error),
127
128  /// Invalid permissions file format
129  #[error("unknown permission format {0}")]
130  UnknownPermissionFormat(String),
131
132  /// Invalid capabilities file format
133  #[error("unknown capability format {0}")]
134  UnknownCapabilityFormat(String),
135
136  /// Permission referenced in set not found.
137  #[error("permission {permission} not found from set {set}")]
138  SetPermissionNotFound {
139    /// Permission identifier.
140    permission: String,
141    /// Set identifier.
142    set: String,
143  },
144
145  /// Unknown ACL manifest.
146  #[error("unknown ACL for {key}, expected one of {available}")]
147  UnknownManifest {
148    /// Manifest key.
149    key: String,
150    /// Available manifest keys.
151    available: String,
152  },
153
154  /// Unknown permission.
155  #[error("unknown permission {permission} for {key}")]
156  UnknownPermission {
157    /// Manifest key.
158    key: String,
159
160    /// Permission identifier.
161    permission: String,
162  },
163
164  /// Capability with the given identifier already exists.
165  #[error("capability with identifier `{identifier}` already exists")]
166  CapabilityAlreadyExists {
167    /// Capability identifier.
168    identifier: String,
169  },
170}
171
172/// Allowed and denied commands inside a permission.
173///
174/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177pub struct Commands {
178  /// Allowed command.
179  #[serde(default)]
180  pub allow: Vec<String>,
181
182  /// Denied command, which takes priority.
183  #[serde(default)]
184  pub deny: Vec<String>,
185}
186
187/// An argument for fine grained behavior control of Tauri commands.
188///
189/// It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.
190/// The configured scope is passed to the command and will be enforced by the command implementation.
191///
192/// ## Example
193///
194/// ```json
195/// {
196///   "allow": [{ "path": "$HOME/**" }],
197///   "deny": [{ "path": "$HOME/secret.txt" }]
198/// }
199/// ```
200#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202pub struct Scopes {
203  /// Data that defines what is allowed by the scope.
204  #[serde(skip_serializing_if = "Option::is_none")]
205  pub allow: Option<Vec<Value>>,
206  /// Data that defines what is denied by the scope. This should be prioritized by validation logic.
207  #[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/// Descriptions of explicit privileges of commands.
218///
219/// It can enable commands to be accessible in the frontend of the application.
220///
221/// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
222#[derive(Debug, Clone, Serialize, Deserialize, Default)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224pub struct Permission {
225  /// The version of the permission.
226  #[serde(skip_serializing_if = "Option::is_none")]
227  pub version: Option<NonZeroU64>,
228
229  /// A unique identifier for the permission.
230  pub identifier: String,
231
232  /// Human-readable description of what the permission does.
233  /// Tauri internal convention is to use `<h4>` headings in markdown content
234  /// for Tauri documentation generation purposes.
235  #[serde(skip_serializing_if = "Option::is_none")]
236  pub description: Option<String>,
237
238  /// Allowed or denied commands when using this permission.
239  #[serde(default)]
240  pub commands: Commands,
241
242  /// Allowed or denied scoped when using this permission.
243  #[serde(default, skip_serializing_if = "Scopes::is_empty")]
244  pub scope: Scopes,
245
246  /// Target platforms this permission applies. By default all platforms are affected by this permission.
247  #[serde(skip_serializing_if = "Option::is_none")]
248  pub platforms: Option<Vec<Target>>,
249}
250
251impl Permission {
252  /// Whether this permission should be active based on the platform target or not.
253  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/// A set of direct permissions grouped together under a new name.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
265pub struct PermissionSet {
266  /// A unique identifier for the permission.
267  pub identifier: String,
268
269  /// Human-readable description of what the permission does.
270  pub description: String,
271
272  /// All permissions this set contains.
273  pub permissions: Vec<String>,
274}
275
276/// UrlPattern for [`ExecutionContext::Remote`].
277#[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  /// Test if a given URL matches the pattern.
311  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/// Execution context of an IPC call.
335#[derive(Debug, Default, Clone, Eq, PartialEq)]
336pub enum ExecutionContext {
337  /// A local URL is used (the Tauri app URL).
338  #[default]
339  Local,
340  /// Remote URL is trying to use the IPC.
341  Remote {
342    /// The URL trying to access the IPC (URL pattern).
343    url: RemoteUrlPattern,
344  },
345}
346
347/// Test if the app has an application manifest from the ACL
348pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
349  acl.contains_key(APP_ACL_KEY)
350}
351
352/// Get the capabilities from the config file
353pub 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/// Allowed commands used to communicate between `generate_handle` and `generate_allowed_commands` through json files
414#[derive(Debug, Default, Serialize, Deserialize)]
415pub struct AllowedCommands {
416  /// The commands allowed
417  pub commands: HashSet<String>,
418  /// Has application ACL or not
419  pub has_app_acl: bool,
420}
421
422/// Try to reads allowed commands from the out dir made by our build script
423pub 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      // Only used in build script and macros, so don't include them in runtime
523      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      // Only used in build script and macros, so don't include them in runtime
545      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}