client/apps/tauri/src-tauri/vendor/tauri-utils/src/acl/capability.rs
2026-05-01 19:57:03 +02:00

452 lines
14 KiB
Rust

// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! End-user abstraction for selecting permissions a window has access to.
use std::{path::Path, str::FromStr};
use crate::{acl::Identifier, platform::Target};
use serde::{
Deserialize, Deserializer, Serialize,
de::{Error, IntoDeserializer},
};
use serde_untagged::UntaggedEnumVisitor;
use super::Scopes;
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
/// or an object that references a permission and extends its scope.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PermissionEntry {
/// Reference a permission or permission set by identifier.
PermissionRef(Identifier),
/// Reference a permission or permission set by identifier and extends its scope.
ExtendedPermission {
/// Identifier of the permission or permission set.
identifier: Identifier,
/// Scope to append to the existing permission scope.
#[serde(default, flatten)]
scope: Scopes,
},
}
impl PermissionEntry {
/// The identifier of the permission referenced in this entry.
pub fn identifier(&self) -> &Identifier {
match self {
Self::PermissionRef(identifier) => identifier,
Self::ExtendedPermission {
identifier,
scope: _,
} => identifier,
}
}
}
impl<'de> Deserialize<'de> for PermissionEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct ExtendedPermissionStruct {
identifier: Identifier,
#[serde(default, flatten)]
scope: Scopes,
}
UntaggedEnumVisitor::new()
.string(|string| {
let de = string.into_deserializer();
Identifier::deserialize(de).map(Self::PermissionRef)
})
.map(|map| {
let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
Ok(Self::ExtendedPermission {
identifier: ext_perm.identifier,
scope: ext_perm.scope,
})
})
.deserialize(deserializer)
}
}
/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
///
/// It controls application windows' and webviews' fine grained access
/// to the Tauri core, application, or plugin commands.
/// If a webview or its window is not matching any capability then it has no access to the IPC layer at all.
///
/// This can be done to create groups of windows, based on their required system access, which can reduce
/// impact of frontend vulnerabilities in less privileged windows.
/// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
/// A Window can have none, one, or multiple associated capabilities.
///
/// ## Example
///
/// ```json
/// {
/// "identifier": "main-user-files-write",
/// "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.",
/// "windows": [
/// "main"
/// ],
/// "permissions": [
/// "core:default",
/// "dialog:open",
/// {
/// "identifier": "fs:allow-write-text-file",
/// "allow": [{ "path": "$HOME/test.txt" }]
/// },
/// ],
/// "platforms": ["macOS","windows"]
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Capability {
/// Identifier of the capability.
///
/// ## Example
///
/// `main-user-files-write`
///
pub identifier: String,
/// Description of what the capability is intended to allow on associated windows.
///
/// It should contain a description of what the grouped permissions should allow.
///
/// ## Example
///
/// This capability allows the `main` window access to `filesystem` write related
/// commands and `dialog` commands to enable programmatic access to files selected by the user.
#[serde(default)]
pub description: String,
/// Configure remote URLs that can use the capability permissions.
///
/// This setting is optional and defaults to not being set, as our
/// default use case is that the content is served from our local application.
///
/// :::caution
/// Make sure you understand the security implications of providing remote
/// sources with local system access.
/// :::
///
/// ## Example
///
/// ```json
/// {
/// "urls": ["https://*.mydomain.dev"]
/// }
/// ```
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote: Option<CapabilityRemote>,
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
#[serde(default = "default_capability_local")]
pub local: bool,
/// List of windows that are affected by this capability. Can be a glob pattern.
///
/// If a window label matches any of the patterns in this list,
/// the capability will be enabled on all the webviews of that window,
/// regardless of the value of [`Self::webviews`].
///
/// On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]
/// for a fine grained access control.
///
/// ## Example
///
/// `["main"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub windows: Vec<String>,
/// List of webviews that are affected by this capability. Can be a glob pattern.
///
/// The capability will be enabled on all the webviews
/// whose label matches any of the patterns in this list,
/// regardless of whether the webview's window label matches a pattern in [`Self::windows`].
///
/// ## Example
///
/// `["sub-webview-one", "sub-webview-two"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub webviews: Vec<String>,
/// List of permissions attached to this capability.
///
/// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
/// For commands directly implemented in the application itself only `${permission-name}`
/// is required.
///
/// ## Example
///
/// ```json
/// [
/// "core:default",
/// "shell:allow-open",
/// "dialog:open",
/// {
/// "identifier": "fs:allow-write-text-file",
/// "allow": [{ "path": "$HOME/test.txt" }]
/// }
/// ]
/// ```
#[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
pub permissions: Vec<PermissionEntry>,
/// Limit which target platforms this capability applies to.
///
/// By default all platforms are targeted.
///
/// ## Example
///
/// `["macOS","windows"]`
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<Target>>,
}
impl Capability {
/// Whether this capability should be active based on the platform target or not.
pub fn is_active(&self, target: &Target) -> bool {
self
.platforms
.as_ref()
.map(|platforms| platforms.contains(target))
.unwrap_or(true)
}
}
#[cfg(feature = "schema")]
fn unique_permission(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let items = serde_json::Value::from(generator.subschema_for::<PermissionEntry>());
schemars::json_schema!({
"type": "array",
"uniqueItems": true,
"items": items
})
}
fn default_capability_local() -> bool {
true
}
/// Configuration for remote URLs that are associated with the capability.
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRemote {
/// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
///
/// ## Examples
///
/// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
/// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
pub urls: Vec<String>,
}
/// Capability formats accepted in a capability file.
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(untagged))]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum CapabilityFile {
/// A single capability.
Capability(Capability),
/// A list of capabilities.
List(Vec<Capability>),
/// A list of capabilities.
NamedList {
/// The list of capabilities.
capabilities: Vec<Capability>,
},
}
impl CapabilityFile {
/// Load the given capability file.
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
let path = path.as_ref();
let capability_file =
std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
let ext = path.extension().unwrap().to_string_lossy().to_string();
let file: Self = match ext.as_str() {
"toml" => toml::from_str(&capability_file)?,
"json" => serde_json::from_str(&capability_file)?,
#[cfg(feature = "config-json5")]
"json5" => json5::from_str(&capability_file)?,
_ => return Err(super::Error::UnknownCapabilityFormat(ext)),
};
Ok(file)
}
}
impl<'de> Deserialize<'de> for CapabilityFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
.map(|map| {
#[derive(Deserialize)]
struct CapabilityNamedList {
capabilities: Vec<Capability>,
}
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
if value.contains_key("capabilities") {
serde_json::from_value::<CapabilityNamedList>(value.into())
.map(|named| Self::NamedList {
capabilities: named.capabilities,
})
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
} else {
serde_json::from_value::<Capability>(value.into())
.map(Self::Capability)
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
}
})
.deserialize(deserializer)
}
}
impl FromStr for CapabilityFile {
type Err = super::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
.or_else(|_| toml::from_str(s))
.map_err(Into::into)
}
}
#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
use std::convert::identity;
use proc_macro2::TokenStream;
use quote::{ToTokens, TokenStreamExt, quote};
use super::*;
use crate::{literal_struct, tokens::*};
impl ToTokens for CapabilityRemote {
fn to_tokens(&self, tokens: &mut TokenStream) {
let urls = vec_lit(&self.urls, str_lit);
literal_struct!(
tokens,
::tauri::utils::acl::capability::CapabilityRemote,
urls
);
}
}
impl ToTokens for PermissionEntry {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
tokens.append_all(match self {
Self::PermissionRef(id) => {
quote! { #prefix::PermissionRef(#id) }
}
Self::ExtendedPermission { identifier, scope } => {
quote! { #prefix::ExtendedPermission {
identifier: #identifier,
scope: #scope
} }
}
});
}
}
impl ToTokens for Capability {
fn to_tokens(&self, tokens: &mut TokenStream) {
let identifier = str_lit(&self.identifier);
let description = str_lit(&self.description);
let remote = opt_lit(self.remote.as_ref());
let local = self.local;
let windows = vec_lit(&self.windows, str_lit);
let webviews = vec_lit(&self.webviews, str_lit);
let permissions = vec_lit(&self.permissions, identity);
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
literal_struct!(
tokens,
::tauri::utils::acl::capability::Capability,
identifier,
description,
remote,
local,
windows,
webviews,
permissions,
platforms
);
}
}
}
#[cfg(test)]
mod tests {
use crate::acl::{Identifier, Scopes};
use super::{Capability, CapabilityFile, PermissionEntry};
#[test]
fn permission_entry_de() {
let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
let identifier_json = serde_json::to_string(&identifier).unwrap();
assert_eq!(
serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
PermissionEntry::PermissionRef(identifier.clone())
);
assert_eq!(
serde_json::from_value::<PermissionEntry>(serde_json::json!({
"identifier": identifier,
"allow": [],
"deny": null
}))
.unwrap(),
PermissionEntry::ExtendedPermission {
identifier,
scope: Scopes {
allow: Some(vec![]),
deny: None
}
}
);
}
#[test]
fn capability_file_de() {
let capability = Capability {
identifier: "test".into(),
description: "".into(),
remote: None,
local: true,
windows: vec![],
webviews: vec![],
permissions: vec![],
platforms: None,
};
let capability_json = serde_json::to_string(&capability).unwrap();
assert_eq!(
serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
CapabilityFile::Capability(capability.clone())
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
CapabilityFile::List(vec![capability.clone()])
);
assert_eq!(
serde_json::from_str::<CapabilityFile>(&format!(
"{{ \"capabilities\": [{capability_json}] }}"
))
.unwrap(),
CapabilityFile::NamedList {
capabilities: vec![capability]
}
);
}
}