Skip to main content

tauri_utils/acl/
capability.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//! End-user abstraction for selecting permissions a window has access to.
6
7use std::{path::Path, str::FromStr};
8
9use crate::{acl::Identifier, platform::Target};
10use serde::{
11  Deserialize, Deserializer, Serialize,
12  de::{Error, IntoDeserializer},
13};
14use serde_untagged::UntaggedEnumVisitor;
15
16use super::Scopes;
17
18/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
19/// or an object that references a permission and extends its scope.
20#[derive(Debug, Clone, PartialEq, Serialize)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[serde(untagged)]
23pub enum PermissionEntry {
24  /// Reference a permission or permission set by identifier.
25  PermissionRef(Identifier),
26  /// Reference a permission or permission set by identifier and extends its scope.
27  ExtendedPermission {
28    /// Identifier of the permission or permission set.
29    identifier: Identifier,
30    /// Scope to append to the existing permission scope.
31    #[serde(default, flatten)]
32    scope: Scopes,
33  },
34}
35
36impl PermissionEntry {
37  /// The identifier of the permission referenced in this entry.
38  pub fn identifier(&self) -> &Identifier {
39    match self {
40      Self::PermissionRef(identifier) => identifier,
41      Self::ExtendedPermission {
42        identifier,
43        scope: _,
44      } => identifier,
45    }
46  }
47}
48
49impl<'de> Deserialize<'de> for PermissionEntry {
50  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51  where
52    D: Deserializer<'de>,
53  {
54    #[derive(Deserialize)]
55    struct ExtendedPermissionStruct {
56      identifier: Identifier,
57      #[serde(default, flatten)]
58      scope: Scopes,
59    }
60
61    UntaggedEnumVisitor::new()
62      .string(|string| {
63        let de = string.into_deserializer();
64        Identifier::deserialize(de).map(Self::PermissionRef)
65      })
66      .map(|map| {
67        let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
68        Ok(Self::ExtendedPermission {
69          identifier: ext_perm.identifier,
70          scope: ext_perm.scope,
71        })
72      })
73      .deserialize(deserializer)
74  }
75}
76
77/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
78///
79/// It controls application windows' and webviews' fine grained access
80/// to the Tauri core, application, or plugin commands.
81/// If a webview or its window is not matching any capability then it has no access to the IPC layer at all.
82///
83/// This can be done to create groups of windows, based on their required system access, which can reduce
84/// impact of frontend vulnerabilities in less privileged windows.
85/// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
86/// A Window can have none, one, or multiple associated capabilities.
87///
88/// ## Example
89///
90/// ```json
91/// {
92///   "identifier": "main-user-files-write",
93///   "description": "This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
94///   "windows": [
95///     "main"
96///   ],
97///   "permissions": [
98///     "core:default",
99///     "dialog:open",
100///     {
101///       "identifier": "fs:allow-write-text-file",
102///       "allow": [{ "path": "$HOME/test.txt" }]
103///     },
104///   ],
105///   "platforms": ["macOS","windows"]
106/// }
107/// ```
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110pub struct Capability {
111  /// Identifier of the capability.
112  ///
113  /// ## Example
114  ///
115  /// `main-user-files-write`
116  ///
117  pub identifier: String,
118  /// Description of what the capability is intended to allow on associated windows.
119  ///
120  /// It should contain a description of what the grouped permissions should allow.
121  ///
122  /// ## Example
123  ///
124  /// This capability allows the `main` window access to `filesystem` write related
125  /// commands and `dialog` commands to enable programmatic access to files selected by the user.
126  #[serde(default)]
127  pub description: String,
128  /// Configure remote URLs that can use the capability permissions.
129  ///
130  /// This setting is optional and defaults to not being set, as our
131  /// default use case is that the content is served from our local application.
132  ///
133  /// :::caution
134  /// Make sure you understand the security implications of providing remote
135  /// sources with local system access.
136  /// :::
137  ///
138  /// ## Example
139  ///
140  /// ```json
141  /// {
142  ///   "urls": ["https://*.mydomain.dev"]
143  /// }
144  /// ```
145  #[serde(default, skip_serializing_if = "Option::is_none")]
146  pub remote: Option<CapabilityRemote>,
147  /// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
148  #[serde(default = "default_capability_local")]
149  pub local: bool,
150  /// List of windows that are affected by this capability. Can be a glob pattern.
151  ///
152  /// If a window label matches any of the patterns in this list,
153  /// the capability will be enabled on all the webviews of that window,
154  /// regardless of the value of [`Self::webviews`].
155  ///
156  /// On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]
157  /// for a fine grained access control.
158  ///
159  /// ## Example
160  ///
161  /// `["main"]`
162  #[serde(default, skip_serializing_if = "Vec::is_empty")]
163  pub windows: Vec<String>,
164  /// List of webviews that are affected by this capability. Can be a glob pattern.
165  ///
166  /// The capability will be enabled on all the webviews
167  /// whose label matches any of the patterns in this list,
168  /// regardless of whether the webview's window label matches a pattern in [`Self::windows`].
169  ///
170  /// ## Example
171  ///
172  /// `["sub-webview-one", "sub-webview-two"]`
173  #[serde(default, skip_serializing_if = "Vec::is_empty")]
174  pub webviews: Vec<String>,
175  /// List of permissions attached to this capability.
176  ///
177  /// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
178  /// For commands directly implemented in the application itself only `${permission-name}`
179  /// is required.
180  ///
181  /// ## Example
182  ///
183  /// ```json
184  /// [
185  ///   "core:default",
186  ///   "shell:allow-open",
187  ///   "dialog:open",
188  ///   {
189  ///     "identifier": "fs:allow-write-text-file",
190  ///     "allow": [{ "path": "$HOME/test.txt" }]
191  ///   }
192  /// ]
193  /// ```
194  #[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
195  pub permissions: Vec<PermissionEntry>,
196  /// Limit which target platforms this capability applies to.
197  ///
198  /// By default all platforms are targeted.
199  ///
200  /// ## Example
201  ///
202  /// `["macOS","windows"]`
203  #[serde(skip_serializing_if = "Option::is_none")]
204  pub platforms: Option<Vec<Target>>,
205}
206
207impl Capability {
208  /// Whether this capability should be active based on the platform target or not.
209  pub fn is_active(&self, target: &Target) -> bool {
210    self
211      .platforms
212      .as_ref()
213      .map(|platforms| platforms.contains(target))
214      .unwrap_or(true)
215  }
216}
217
218#[cfg(feature = "schema")]
219fn unique_permission(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
220  let items = serde_json::Value::from(generator.subschema_for::<PermissionEntry>());
221  schemars::json_schema!({
222    "type": "array",
223    "uniqueItems": true,
224    "items": items
225  })
226}
227
228fn default_capability_local() -> bool {
229  true
230}
231
232/// Configuration for remote URLs that are associated with the capability.
233#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235#[serde(rename_all = "camelCase")]
236pub struct CapabilityRemote {
237  /// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
238  ///
239  /// ## Examples
240  ///
241  /// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
242  /// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
243  pub urls: Vec<String>,
244}
245
246/// Capability formats accepted in a capability file.
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
248#[cfg_attr(feature = "schema", schemars(untagged))]
249#[cfg_attr(test, derive(Debug, PartialEq))]
250pub enum CapabilityFile {
251  /// A single capability.
252  Capability(Capability),
253  /// A list of capabilities.
254  List(Vec<Capability>),
255  /// A list of capabilities.
256  NamedList {
257    /// The list of capabilities.
258    capabilities: Vec<Capability>,
259  },
260}
261
262impl CapabilityFile {
263  /// Load the given capability file.
264  pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
265    let path = path.as_ref();
266    let capability_file =
267      std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
268    let ext = path.extension().unwrap().to_string_lossy().to_string();
269    let file: Self = match ext.as_str() {
270      "toml" => toml::from_str(&capability_file)?,
271      "json" => serde_json::from_str(&capability_file)?,
272      #[cfg(feature = "config-json5")]
273      "json5" => json5::from_str(&capability_file)?,
274      _ => return Err(super::Error::UnknownCapabilityFormat(ext)),
275    };
276    Ok(file)
277  }
278}
279
280impl<'de> Deserialize<'de> for CapabilityFile {
281  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
282  where
283    D: Deserializer<'de>,
284  {
285    UntaggedEnumVisitor::new()
286      .seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
287      .map(|map| {
288        #[derive(Deserialize)]
289        struct CapabilityNamedList {
290          capabilities: Vec<Capability>,
291        }
292
293        let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
294        if value.contains_key("capabilities") {
295          serde_json::from_value::<CapabilityNamedList>(value.into())
296            .map(|named| Self::NamedList {
297              capabilities: named.capabilities,
298            })
299            .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
300        } else {
301          serde_json::from_value::<Capability>(value.into())
302            .map(Self::Capability)
303            .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
304        }
305      })
306      .deserialize(deserializer)
307  }
308}
309
310impl FromStr for CapabilityFile {
311  type Err = super::Error;
312
313  fn from_str(s: &str) -> Result<Self, Self::Err> {
314    serde_json::from_str(s)
315      .or_else(|_| toml::from_str(s))
316      .map_err(Into::into)
317  }
318}
319
320#[cfg(any(feature = "build", feature = "build-2"))]
321mod build {
322  use std::convert::identity;
323
324  use proc_macro2::TokenStream;
325  use quote::{ToTokens, TokenStreamExt, quote};
326
327  use super::*;
328  use crate::{literal_struct, tokens::*};
329
330  impl ToTokens for CapabilityRemote {
331    fn to_tokens(&self, tokens: &mut TokenStream) {
332      let urls = vec_lit(&self.urls, str_lit);
333      literal_struct!(
334        tokens,
335        ::tauri::utils::acl::capability::CapabilityRemote,
336        urls
337      );
338    }
339  }
340
341  impl ToTokens for PermissionEntry {
342    fn to_tokens(&self, tokens: &mut TokenStream) {
343      let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
344
345      tokens.append_all(match self {
346        Self::PermissionRef(id) => {
347          quote! { #prefix::PermissionRef(#id) }
348        }
349        Self::ExtendedPermission { identifier, scope } => {
350          quote! { #prefix::ExtendedPermission {
351            identifier: #identifier,
352            scope: #scope
353          } }
354        }
355      });
356    }
357  }
358
359  impl ToTokens for Capability {
360    fn to_tokens(&self, tokens: &mut TokenStream) {
361      let identifier = str_lit(&self.identifier);
362      let description = str_lit(&self.description);
363      let remote = opt_lit(self.remote.as_ref());
364      let local = self.local;
365      let windows = vec_lit(&self.windows, str_lit);
366      let webviews = vec_lit(&self.webviews, str_lit);
367      let permissions = vec_lit(&self.permissions, identity);
368      let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
369
370      literal_struct!(
371        tokens,
372        ::tauri::utils::acl::capability::Capability,
373        identifier,
374        description,
375        remote,
376        local,
377        windows,
378        webviews,
379        permissions,
380        platforms
381      );
382    }
383  }
384}
385
386#[cfg(test)]
387mod tests {
388  use crate::acl::{Identifier, Scopes};
389
390  use super::{Capability, CapabilityFile, PermissionEntry};
391
392  #[test]
393  fn permission_entry_de() {
394    let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
395    let identifier_json = serde_json::to_string(&identifier).unwrap();
396    assert_eq!(
397      serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
398      PermissionEntry::PermissionRef(identifier.clone())
399    );
400
401    assert_eq!(
402      serde_json::from_value::<PermissionEntry>(serde_json::json!({
403        "identifier": identifier,
404        "allow": [],
405        "deny": null
406      }))
407      .unwrap(),
408      PermissionEntry::ExtendedPermission {
409        identifier,
410        scope: Scopes {
411          allow: Some(vec![]),
412          deny: None
413        }
414      }
415    );
416  }
417
418  #[test]
419  fn capability_file_de() {
420    let capability = Capability {
421      identifier: "test".into(),
422      description: "".into(),
423      remote: None,
424      local: true,
425      windows: vec![],
426      webviews: vec![],
427      permissions: vec![],
428      platforms: None,
429    };
430    let capability_json = serde_json::to_string(&capability).unwrap();
431
432    assert_eq!(
433      serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
434      CapabilityFile::Capability(capability.clone())
435    );
436
437    assert_eq!(
438      serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
439      CapabilityFile::List(vec![capability.clone()])
440    );
441
442    assert_eq!(
443      serde_json::from_str::<CapabilityFile>(&format!(
444        "{{ \"capabilities\": [{capability_json}] }}"
445      ))
446      .unwrap(),
447      CapabilityFile::NamedList {
448        capabilities: vec![capability]
449      }
450    );
451  }
452}