refactor(core): allow referencing capabilities on the Tauri config file (#8797)

* refactor(core): capabilities must be referenced on the Tauri config file

* add all capabilities by default

* refactor(cli): reference all capabilities by default
This commit is contained in:
Lucas Fernandes Nogueira 2024-02-18 10:42:09 -03:00 committed by GitHub
parent 0cb0a15ce2
commit 83a68deb56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 744 additions and 15 deletions

View File

@ -0,0 +1,7 @@
---
"tauri-build": patch:breaking
"tauri-utils": patch:enhance
"tauri-codegen": patch:enhance
---
Added a new configuration option `tauri.conf.json > app > security > capabilities` to reference existing capabilities and inline new ones. If it is empty, all capabilities are still included preserving the current behavior.

View File

@ -0,0 +1,6 @@
---
"@tauri-apps/cli": patch:enhance
"tauri-cli": patch:enhance
---
Update app template following capabilities configuration change.

View File

@ -567,6 +567,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
out_dir.join(PLUGIN_MANIFESTS_FILE_NAME),
serde_json::to_string(&plugin_manifests)?,
)?;
let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern {
parse_capabilities(pattern)?
} else {

View File

@ -15,7 +15,7 @@ use tauri_utils::acl::capability::Capability;
use tauri_utils::acl::plugin::Manifest;
use tauri_utils::acl::resolved::Resolved;
use tauri_utils::assets::AssetKey;
use tauri_utils::config::{Config, FrontendDist, PatternKind};
use tauri_utils::config::{CapabilityEntry, Config, FrontendDist, PatternKind};
use tauri_utils::html::{
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
};
@ -381,7 +381,8 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
};
let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
let capabilities: BTreeMap<String, Capability> = if capabilities_file_path.exists() {
let mut capabilities_from_files: BTreeMap<String, Capability> = if capabilities_file_path.exists()
{
let capabilities_file =
std::fs::read_to_string(capabilities_file_path).expect("failed to read capabilities");
serde_json::from_str(&capabilities_file).expect("failed to parse capabilities")
@ -389,6 +390,26 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
Default::default()
};
let capabilities = if config.app.security.capabilities.is_empty() {
capabilities_from_files
} else {
let mut capabilities = BTreeMap::new();
for capability_entry in &config.app.security.capabilities {
match capability_entry {
CapabilityEntry::Inlined(capability) => {
capabilities.insert(capability.identifier.clone(), capability.clone());
}
CapabilityEntry::Reference(id) => {
let capability = capabilities_from_files
.remove(id)
.unwrap_or_else(|| panic!("capability with identifier {id} not found"));
capabilities.insert(id.clone(), capability);
}
}
}
capabilities
};
let resolved_acl = Resolved::resolve(acl, capabilities, target).expect("failed to resolve ACL");
Ok(quote!({

View File

@ -40,6 +40,7 @@
"enable": false,
"scope": []
},
"capabilities": [],
"dangerousDisableAssetCspModification": false,
"freezePrototype": false,
"pattern": {
@ -158,6 +159,7 @@
"enable": false,
"scope": []
},
"capabilities": [],
"dangerousDisableAssetCspModification": false,
"freezePrototype": false,
"pattern": {
@ -878,6 +880,14 @@
"$ref": "#/definitions/PatternKind"
}
]
},
"capabilities": {
"description": "List of capabilities that are enabled on the application.\n\nIf the list is empty, all capabilities are included.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/CapabilityEntry"
}
}
},
"additionalProperties": false
@ -1040,6 +1050,264 @@
}
]
},
"CapabilityEntry": {
"description": "A capability entry which can be either an inlined capability or a reference to a capability defined on its own file.",
"anyOf": [
{
"description": "An inlined capability.",
"allOf": [
{
"$ref": "#/definitions/Capability"
}
]
},
{
"description": "Reference to a capability identifier.",
"type": "string"
}
]
},
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"type": "object",
"required": [
"identifier",
"permissions",
"windows"
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"default": "",
"type": "string"
},
"context": {
"description": "Execution context of the capability.\n\nAt runtime, Tauri filters the IPC command together with the context to determine whether it is allowed or not and its scope.",
"default": "local",
"allOf": [
{
"$ref": "#/definitions/CapabilityContext"
}
]
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms applies.",
"default": [
"linux",
"macOS",
"windows",
"android",
"iOS"
],
"type": "array",
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"CapabilityContext": {
"description": "Context of the capability.",
"oneOf": [
{
"description": "Capability refers to local URL usage.",
"type": "string",
"enum": [
"local"
]
},
{
"description": "Capability refers to remote usage.",
"type": "object",
"required": [
"remote"
],
"properties": {
"remote": {
"type": "object",
"required": [
"domains"
],
"properties": {
"domains": {
"description": "Remote domains this capability refers to. Can use glob patterns.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"additionalProperties": false
}
]
},
"PermissionEntry": {
"description": "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.",
"anyOf": [
{
"description": "Reference a permission or permission set by identifier.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
},
{
"description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
},
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
}
]
},
"Identifier": {
"type": "string"
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"TrayIconConfig": {
"description": "Configuration for application tray icon.\n\nSee more: <https://tauri.app/v1/api/config#trayiconconfig>",
"type": "object",

View File

@ -11,7 +11,7 @@ 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, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PermissionEntry {
@ -46,7 +46,7 @@ impl PermissionEntry {
///
/// This can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows.
/// Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Capability {
/// Identifier of the capability.
@ -100,3 +100,70 @@ pub enum CapabilityContext {
domains: Vec<String>,
},
}
#[cfg(feature = "build")]
mod build {
use std::convert::identity;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use super::*;
use crate::{literal_struct, tokens::*};
impl ToTokens for CapabilityContext {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::capability::CapabilityContext };
tokens.append_all(match self {
Self::Remote { domains } => {
let domains = vec_lit(domains, str_lit);
quote! { #prefix::Remote { domains: #domains } }
}
Self::Local => {
quote! { #prefix::Local }
}
});
}
}
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 context = &self.context;
let windows = vec_lit(&self.windows, str_lit);
let permissions = vec_lit(&self.permissions, identity);
let platforms = vec_lit(&self.platforms, identity);
literal_struct!(
tokens,
::tauri::utils::acl::capability::Capability,
identifier,
description,
context,
windows,
permissions,
platforms
);
}
}
}

View File

@ -17,13 +17,28 @@ const MAX_LEN_BASE: usize = 64;
const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;
/// Plugin identifier.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identifier {
inner: String,
separator: Option<NonZeroU8>,
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for Identifier {
fn schema_name() -> String {
"Identifier".to_string()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
// Include the module, in case a type with the same name is in another module/crate
std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(gen)
}
}
impl AsRef<str> for Identifier {
#[inline(always)]
fn as_ref(&self) -> &str {
@ -262,3 +277,19 @@ mod tests {
assert_eq!(ident("base").unwrap().get_prefix(), None);
}
}
#[cfg(feature = "build")]
mod build {
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use super::*;
impl ToTokens for Identifier {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = self.get();
tokens
.append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
}
}
}

View File

@ -131,7 +131,7 @@ pub struct Commands {
/// It can be of any serde serializable type and is used for allowing or preventing certain actions inside a Tauri command.
///
/// The scope is passed to the command and handled/enforced by the command itself.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Scopes {
/// Data that defines what is allowed by the scope.

View File

@ -11,7 +11,7 @@ use std::fmt::Debug;
use serde::{Deserialize, Serialize};
/// A valid ACL number.
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialOrd, PartialEq)]
#[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone, PartialOrd)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Number {
@ -37,7 +37,7 @@ impl From<f64> for Number {
}
/// All supported ACL values.
#[derive(Debug, Serialize, Deserialize, Clone, PartialOrd, PartialEq)]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, PartialOrd)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Value {

View File

@ -34,7 +34,7 @@ use std::{
/// Items to help with parsing content into a [`Config`].
pub mod parse;
use crate::{TitleBarStyle, WindowEffect, WindowEffectState};
use crate::{acl::capability::Capability, TitleBarStyle, WindowEffect, WindowEffectState};
pub use self::parse::parse;
@ -1519,7 +1519,7 @@ pub struct AssetProtocolConfig {
///
/// See more: <https://tauri.app/v1/api/config#securityconfig>
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SecurityConfig {
@ -1558,6 +1558,22 @@ pub struct SecurityConfig {
/// The pattern to use.
#[serde(default)]
pub pattern: PatternKind,
/// List of capabilities that are enabled on the application.
///
/// If the list is empty, all capabilities are included.
#[serde(default)]
pub capabilities: Vec<CapabilityEntry>,
}
/// A capability entry which can be either an inlined capability or a reference to a capability defined on its own file.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", untagged)]
pub enum CapabilityEntry {
/// An inlined capability.
Inlined(Capability),
/// Reference to a capability identifier.
Reference(String),
}
/// The application pattern.
@ -2450,6 +2466,22 @@ mod build {
}
}
impl ToTokens for CapabilityEntry {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::config::CapabilityEntry };
tokens.append_all(match self {
Self::Inlined(capability) => {
quote! { #prefix::Inlined(#capability) }
}
Self::Reference(id) => {
let id = str_lit(id);
quote! { #prefix::Reference(#id) }
}
});
}
}
impl ToTokens for SecurityConfig {
fn to_tokens(&self, tokens: &mut TokenStream) {
let csp = opt_lit(self.csp.as_ref());
@ -2458,6 +2490,7 @@ mod build {
let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;
let asset_protocol = &self.asset_protocol;
let pattern = &self.pattern;
let capabilities = vec_lit(&self.capabilities, identity);
literal_struct!(
tokens,
@ -2467,7 +2500,8 @@ mod build {
freeze_prototype,
dangerous_disable_asset_csp_modification,
asset_protocol,
pattern
pattern,
capabilities
);
}
}
@ -2606,6 +2640,7 @@ mod test {
dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
asset_protocol: AssetProtocolConfig::default(),
pattern: Default::default(),
capabilities: Vec::new(),
},
tray_icon: None,
macos_private_api: false,

View File

@ -284,3 +284,25 @@ pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<Path
res
}
#[cfg(feature = "build")]
mod build {
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use super::*;
impl ToTokens for Target {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::platform::Target };
tokens.append_all(match self {
Self::MacOS => quote! { #prefix::MacOS },
Self::Linux => quote! { #prefix::Linux },
Self::Windows => quote! { #prefix::Windows },
Self::Android => quote! { #prefix::Android },
Self::Ios => quote! { #prefix::Ios },
});
}
}
}

View File

@ -2,7 +2,10 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "run-app",
"description": "permissions to run the app",
"windows": ["main", "main-*"],
"windows": [
"main",
"main-*"
],
"permissions": [
"app-menu:default",
"sample:allow-ping-scoped",

View File

@ -40,6 +40,7 @@
"enable": false,
"scope": []
},
"capabilities": [],
"dangerousDisableAssetCspModification": false,
"freezePrototype": false,
"pattern": {
@ -158,6 +159,7 @@
"enable": false,
"scope": []
},
"capabilities": [],
"dangerousDisableAssetCspModification": false,
"freezePrototype": false,
"pattern": {
@ -878,6 +880,14 @@
"$ref": "#/definitions/PatternKind"
}
]
},
"capabilities": {
"description": "List of capabilities that are enabled on the application.\n\nIf the list is empty, all capabilities are included.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/CapabilityEntry"
}
}
},
"additionalProperties": false
@ -1040,6 +1050,264 @@
}
]
},
"CapabilityEntry": {
"description": "A capability entry which can be either an inlined capability or a reference to a capability defined on its own file.",
"anyOf": [
{
"description": "An inlined capability.",
"allOf": [
{
"$ref": "#/definitions/Capability"
}
]
},
{
"description": "Reference to a capability identifier.",
"type": "string"
}
]
},
"Capability": {
"description": "a grouping and boundary mechanism developers can use to separate windows or plugins functionality from each other at runtime.\n\nIf a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows. Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.",
"type": "object",
"required": [
"identifier",
"permissions",
"windows"
],
"properties": {
"identifier": {
"description": "Identifier of the capability.",
"type": "string"
},
"description": {
"description": "Description of the capability.",
"default": "",
"type": "string"
},
"context": {
"description": "Execution context of the capability.\n\nAt runtime, Tauri filters the IPC command together with the context to determine whether it is allowed or not and its scope.",
"default": "local",
"allOf": [
{
"$ref": "#/definitions/CapabilityContext"
}
]
},
"windows": {
"description": "List of windows that uses this capability. Can be a glob pattern.",
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"description": "List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"
}
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms applies.",
"default": [
"linux",
"macOS",
"windows",
"android",
"iOS"
],
"type": "array",
"items": {
"$ref": "#/definitions/Target"
}
}
}
},
"CapabilityContext": {
"description": "Context of the capability.",
"oneOf": [
{
"description": "Capability refers to local URL usage.",
"type": "string",
"enum": [
"local"
]
},
{
"description": "Capability refers to remote usage.",
"type": "object",
"required": [
"remote"
],
"properties": {
"remote": {
"type": "object",
"required": [
"domains"
],
"properties": {
"domains": {
"description": "Remote domains this capability refers to. Can use glob patterns.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"additionalProperties": false
}
]
},
"PermissionEntry": {
"description": "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.",
"anyOf": [
{
"description": "Reference a permission or permission set by identifier.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
},
{
"description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
},
"allow": {
"description": "Data that defines what is allowed by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
},
"deny": {
"description": "Data that defines what is denied by the scope.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Value"
}
}
}
}
]
},
"Identifier": {
"type": "string"
},
"Value": {
"description": "All supported ACL values.",
"anyOf": [
{
"description": "Represents a null JSON value.",
"type": "null"
},
{
"description": "Represents a [`bool`].",
"type": "boolean"
},
{
"description": "Represents a valid ACL [`Number`].",
"allOf": [
{
"$ref": "#/definitions/Number"
}
]
},
{
"description": "Represents a [`String`].",
"type": "string"
},
{
"description": "Represents a list of other [`Value`]s.",
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
},
{
"description": "Represents a map of [`String`] keys to [`Value`]s.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Value"
}
}
]
},
"Number": {
"description": "A valid ACL number.",
"anyOf": [
{
"description": "Represents an [`i64`].",
"type": "integer",
"format": "int64"
},
{
"description": "Represents a [`f64`].",
"type": "number",
"format": "double"
}
]
},
"Target": {
"description": "Platform target.",
"oneOf": [
{
"description": "MacOS.",
"type": "string",
"enum": [
"macOS"
]
},
{
"description": "Windows.",
"type": "string",
"enum": [
"windows"
]
},
{
"description": "Linux.",
"type": "string",
"enum": [
"linux"
]
},
{
"description": "Android.",
"type": "string",
"enum": [
"android"
]
},
{
"description": "iOS.",
"type": "string",
"enum": [
"iOS"
]
}
]
},
"TrayIconConfig": {
"description": "Configuration for application tray icon.\n\nSee more: <https://tauri.app/v1/api/config#trayiconconfig>",
"type": "object",

View File

@ -1,6 +1,6 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default-plugins",
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main"],
"permissions": [