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 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
33/// Known foldername of the permission schema files
34pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
35/// Known filename of the permission schema JSON file
36pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
37/// Known ACL key for the app permissions.
38pub const APP_ACL_KEY: &str = "__app-acl__";
39/// Known acl manifests file
40pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
41/// Known capabilityies file
42pub 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/// Possible errors while processing ACL files.
55#[derive(Debug, Error)]
56pub enum Error {
57  /// Could not find an environmental variable that is set inside of build scripts.
58  ///
59  /// Whatever generated this should be called inside of a build script.
60  #[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  /// The links field in the manifest **MUST** be set and match the name of the crate.
64  #[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  /// The links field in the manifest **MUST** match the name of the crate.
68  #[error(
69    "package.links field in the Cargo manifest MUST be set to the same value as package.name"
70  )]
71  LinksName,
72
73  /// IO error while reading a file
74  #[error("failed to read file '{}': {}", _1.display(), _0)]
75  ReadFile(std::io::Error, PathBuf),
76
77  /// IO error while writing a file
78  #[error("failed to write file '{}': {}", _1.display(), _0)]
79  WriteFile(std::io::Error, PathBuf),
80
81  /// IO error while creating a file
82  #[error("failed to create file '{}': {}", _1.display(), _0)]
83  CreateFile(std::io::Error, PathBuf),
84
85  /// IO error while creating a dir
86  #[error("failed to create dir '{}': {}", _1.display(), _0)]
87  CreateDir(std::io::Error, PathBuf),
88
89  /// [`cargo_metadata`] was not able to complete successfully
90  #[cfg(feature = "build")]
91  #[error("failed to execute: {0}")]
92  Metadata(#[from] ::cargo_metadata::Error),
93
94  /// Invalid glob
95  #[error("failed to run glob: {0}")]
96  Glob(#[from] glob::PatternError),
97
98  /// Invalid TOML encountered
99  #[error("failed to parse TOML: {0}")]
100  Toml(#[from] toml::de::Error),
101
102  /// Invalid JSON encountered
103  #[error("failed to parse JSON: {0}")]
104  Json(#[from] serde_json::Error),
105
106  /// Invalid JSON5 encountered
107  #[cfg(feature = "config-json5")]
108  #[error("failed to parse JSON5: {0}")]
109  Json5(#[from] json5::Error),
110
111  /// Invalid permissions file format
112  #[error("unknown permission format {0}")]
113  UnknownPermissionFormat(String),
114
115  /// Invalid capabilities file format
116  #[error("unknown capability format {0}")]
117  UnknownCapabilityFormat(String),
118
119  /// Permission referenced in set not found.
120  #[error("permission {permission} not found from set {set}")]
121  SetPermissionNotFound {
122    /// Permission identifier.
123    permission: String,
124    /// Set identifier.
125    set: String,
126  },
127
128  /// Unknown ACL manifest.
129  #[error("unknown ACL for {key}, expected one of {available}")]
130  UnknownManifest {
131    /// Manifest key.
132    key: String,
133    /// Available manifest keys.
134    available: String,
135  },
136
137  /// Unknown permission.
138  #[error("unknown permission {permission} for {key}")]
139  UnknownPermission {
140    /// Manifest key.
141    key: String,
142
143    /// Permission identifier.
144    permission: String,
145  },
146
147  /// Capability with the given identifier already exists.
148  #[error("capability with identifier `{identifier}` already exists")]
149  CapabilityAlreadyExists {
150    /// Capability identifier.
151    identifier: String,
152  },
153}
154
155/// Allowed and denied commands inside a permission.
156///
157/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
158#[derive(Debug, Default, Serialize, Deserialize)]
159#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
160pub struct Commands {
161  /// Allowed command.
162  #[serde(default)]
163  pub allow: Vec<String>,
164
165  /// Denied command, which takes priority.
166  #[serde(default)]
167  pub deny: Vec<String>,
168}
169
170/// An argument for fine grained behavior control of Tauri commands.
171///
172/// It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.
173/// The configured scope is passed to the command and will be enforced by the command implementation.
174///
175/// ## Example
176///
177/// ```json
178/// {
179///   "allow": [{ "path": "$HOME/**" }],
180///   "deny": [{ "path": "$HOME/secret.txt" }]
181/// }
182/// ```
183#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
185pub struct Scopes {
186  /// Data that defines what is allowed by the scope.
187  #[serde(skip_serializing_if = "Option::is_none")]
188  pub allow: Option<Vec<Value>>,
189  /// Data that defines what is denied by the scope. This should be prioritized by validation logic.
190  #[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/// Descriptions of explicit privileges of commands.
201///
202/// It can enable commands to be accessible in the frontend of the application.
203///
204/// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
205#[derive(Debug, Serialize, Deserialize, Default)]
206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
207pub struct Permission {
208  /// The version of the permission.
209  #[serde(skip_serializing_if = "Option::is_none")]
210  pub version: Option<NonZeroU64>,
211
212  /// A unique identifier for the permission.
213  pub identifier: String,
214
215  /// Human-readable description of what the permission does.
216  /// Tauri internal convention is to use <h4> headings in markdown content
217  /// for Tauri documentation generation purposes.
218  #[serde(skip_serializing_if = "Option::is_none")]
219  pub description: Option<String>,
220
221  /// Allowed or denied commands when using this permission.
222  #[serde(default)]
223  pub commands: Commands,
224
225  /// Allowed or denied scoped when using this permission.
226  #[serde(default, skip_serializing_if = "Scopes::is_empty")]
227  pub scope: Scopes,
228
229  /// Target platforms this permission applies. By default all platforms are affected by this permission.
230  #[serde(skip_serializing_if = "Option::is_none")]
231  pub platforms: Option<Vec<Target>>,
232}
233
234impl Permission {
235  /// Whether this permission should be active based on the platform target or not.
236  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/// A set of direct permissions grouped together under a new name.
246#[derive(Debug, Serialize, Deserialize)]
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
248pub struct PermissionSet {
249  /// A unique identifier for the permission.
250  pub identifier: String,
251
252  /// Human-readable description of what the permission does.
253  pub description: String,
254
255  /// All permissions this set contains.
256  pub permissions: Vec<String>,
257}
258
259/// UrlPattern for [`ExecutionContext::Remote`].
260#[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  /// Test if a given URL matches the pattern.
294  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/// Execution context of an IPC call.
318#[derive(Debug, Default, Clone, Eq, PartialEq)]
319pub enum ExecutionContext {
320  /// A local URL is used (the Tauri app URL).
321  #[default]
322  Local,
323  /// Remote URL is trying to use the IPC.
324  Remote {
325    /// The URL trying to access the IPC (URL pattern).
326    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}