Compare commits

...

14 Commits

Author SHA1 Message Date
Lucas Nogueira
4dfba14fbb fix tests 2023-07-31 13:03:55 -03:00
Lucas Nogueira
f514d655b6 make description optional 2023-07-31 12:55:57 -03:00
Lucas Nogueira
e729a9c83f use btreemap for manifests 2023-07-31 12:52:44 -03:00
Lucas Nogueira
41961e7de0 allow the app capability json to contain an array 2023-07-31 12:50:23 -03:00
Lucas Nogueira
cb78c12db8 resolve conflicts on capability ids 2023-07-31 09:37:26 -03:00
Lucas Nogueira
13a774e0ca automatically define a capability for each plugin feature 2023-07-31 08:00:15 -03:00
Lucas Nogueira
d894ad2896 allow the capability JSON to have a list of capabilities 2023-07-31 07:54:14 -03:00
Lucas Nogueira
b382a4e08f allow core to define plugin manifests 2023-07-30 17:53:26 -03:00
Lucas Nogueira
b82899b96b allow the app to define custom capabilities 2023-07-30 17:36:20 -03:00
Lucas Nogueira
708cb9fa28 initial draft for runtime authority 2023-07-26 09:01:37 -03:00
Lucas Nogueira
8467f138bb move types to tauri-utils 2023-07-25 12:51:35 -03:00
Lucas Nogueira
50aaf4ec14 initial lockfile format and generation script 2023-07-24 22:11:38 -03:00
Lucas Nogueira
f8d98db9ff initial namespace config definition 2023-07-24 22:11:18 -03:00
Lucas Nogueira
0811f512c5 initial plugin metadata implementation 2023-07-24 15:37:08 -03:00
30 changed files with 2409 additions and 593 deletions

View File

@@ -17,13 +17,18 @@ pub use anyhow::Result;
use cargo_toml::Manifest;
use heck::AsShoutySnakeCase;
use serde::Deserialize;
use tauri_utils::{
config::Config,
namespace::{MemberResolution, NamespaceLockFile},
plugin::{Capability, CapabilityOrList},
resources::{external_binaries, resource_relpath, ResourcePaths},
};
use std::{
collections::HashMap,
env::var_os,
fs::write,
path::{Path, PathBuf},
};
@@ -34,6 +39,8 @@ mod codegen;
pub mod config;
/// Mobile build functions.
pub mod mobile;
/// Build scripts for Tauri plugins.
pub mod plugin;
mod static_vcruntime;
#[cfg(feature = "codegen")]
@@ -227,6 +234,7 @@ impl WindowsAttributes {
pub struct Attributes {
#[allow(dead_code)]
windows_attributes: WindowsAttributes,
capabilities: Vec<Capability>,
}
impl Attributes {
@@ -241,6 +249,36 @@ impl Attributes {
self.windows_attributes = windows_attributes;
self
}
/// Appends a capability JSON. See [`Capability`].
#[must_use]
pub fn capability_json(self, capability: impl AsRef<str>) -> Self {
let capability: CapabilityOrList =
serde_json::from_str(capability.as_ref()).expect("failed to deserialize capability");
match capability {
CapabilityOrList::Single(capability) => self.capability(capability),
CapabilityOrList::List(l) => self.capabilities(l),
}
}
/// Appends a [`Capability`].
#[must_use]
pub fn capability(mut self, capability: Capability) -> Self {
assert!(
!capability.id.is_empty(),
"capability must have an identifier"
);
self.capabilities.push(capability);
self
}
/// Appends the given list of capabilities. See [`Self::capability`].
pub fn capabilities<I: IntoIterator<Item = Capability>>(mut self, capabilities: I) -> Self {
for capability in capabilities {
self = self.capability(capability);
}
self
}
}
/// Run all build time helpers for your Tauri Application.
@@ -293,9 +331,9 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
cfg_alias("desktop", !mobile);
cfg_alias("mobile", mobile);
let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(
std::env::current_dir().unwrap(),
)?)?;
let (config, config_path) =
tauri_utils::config::parse::read_from(std::env::current_dir().unwrap())?;
let mut config = serde_json::from_value(config)?;
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
json_patch::merge(&mut config, &merge_config);
@@ -473,10 +511,95 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
}
}
let mut manifests = plugin::manifests();
const APP_MANIFEST_KEY: &str = "__app__";
manifests.insert(
APP_MANIFEST_KEY.into(),
tauri_utils::plugin::Manifest {
plugin: "".into(),
default_capability: None,
capabilities: attributes.capabilities,
features: Vec::new(),
scope_type: Vec::new(),
},
);
let mut resolution = HashMap::<String, MemberResolution>::new();
for namespace in &config.namespaces {
for member in &namespace.members {
let member_resolution =
resolution
.entry(member.clone())
.or_insert_with(|| MemberResolution {
member: member.clone(),
commands: Default::default(),
});
for capability in &namespace.capabilities {
let (target_plugin, capability_id) = capability
.split_once(':')
.map(|(plugin, id)| (Some(plugin), id))
.unwrap_or_else(|| (None, capability.as_str()));
let capabilities = manifests.find_capability(capability_id);
if capabilities.is_empty() {
panic!("could not find capability specification matching id {capability}")
} else {
let (plugin, capability) = if let Some(target) = target_plugin {
capabilities
.into_iter()
.find(|(p, _)| p == target)
.unwrap_or_else(|| {
panic!("failed to find capability matching id {capability_id} for plugin {target}")
})
} else if capabilities.len() > 1 {
panic!(
"found a conflict on capability id {capability}, please use one of the [{}] prefixes",
capabilities
.iter()
.map(|(p, _)| format!("'{p}:'"))
.collect::<Vec<String>>()
.join(", ")
);
} else {
// already checked that the capabilities aren't empty
capabilities.into_iter().next().unwrap()
};
if plugin == APP_MANIFEST_KEY {
member_resolution.commands.extend(capability.features);
} else {
member_resolution.commands.extend(
capability
.features
.into_iter()
.map(|f| format!("plugin:{plugin}|{f}"))
.collect::<Vec<_>>(),
);
}
}
}
}
}
let lockfile = NamespaceLockFile {
version: 1,
namespaces: config.namespaces,
plugins: manifests,
resolution: resolution.into_values().collect(),
};
write(
config_path.parent().unwrap().join("tauri.namespace.lock"),
serde_json::to_string_pretty(&lockfile)?,
)
.context("failed to write namespace lockfile")?;
Ok(())
}
#[derive(serde::Deserialize)]
#[derive(Deserialize)]
struct CargoMetadata {
workspace_root: PathBuf,
}

View File

@@ -0,0 +1,70 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri_utils::plugin::Capability;
pub use tauri_utils::plugin::{Manifest, ManifestMap, ScopeType};
use std::{
collections::BTreeMap,
env::{var_os, vars_os},
fs::{read_to_string, write},
path::PathBuf,
};
const PLUGIN_METADATA_KEY: &str = "PLUGIN_MANIFEST_PATH";
pub fn set_manifest(mut manifest: Manifest) {
for feature in &manifest.features {
let feature_capability_id = format!("allow-{feature}");
if !manifest
.capabilities
.iter()
.any(|c| c.id == feature_capability_id)
{
manifest.capabilities.push(Capability {
id: feature_capability_id,
component: None,
description: Some(format!("Allows the {feature} functionality")),
features: vec![feature.clone()],
scope: Default::default(),
});
}
}
let manifest_str = serde_json::to_string(&manifest).expect("failed to serialize plugin manifest");
let manifest_path = var_os("OUT_DIR")
.map(PathBuf::from)
.expect(
"missing OUT_DIR environment variable.. are you sure you are running this on a build script?",
)
.join(format!("{}-plugin-manifest.json", manifest.plugin));
write(&manifest_path, manifest_str).expect("failed to save manifest file");
println!(
"cargo:{}_{PLUGIN_METADATA_KEY}={}",
manifest.plugin,
manifest_path.display()
);
}
pub(crate) fn manifests() -> ManifestMap {
let mut manifests = BTreeMap::new();
for (key, value) in vars_os() {
let key = key.to_string_lossy();
if let Some(_plugin_crate_name) = key
.strip_prefix("DEP_")
.and_then(|v| v.strip_suffix(&format!("_{PLUGIN_METADATA_KEY}")))
{
let plugin_manifest_path = PathBuf::from(value);
let plugin_manifest_str =
read_to_string(&plugin_manifest_path).expect("failed to read plugin manifest");
let manifest: Manifest =
serde_json::from_str(&plugin_manifest_str).expect("failed to deserialize plugin manifest");
manifests.insert(manifest.plugin.clone(), manifest);
}
}
manifests.into()
}

View File

@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
use std::path::{Path, PathBuf};
use std::{ffi::OsStr, str::FromStr};
use std::{ffi::OsStr, fs::read_to_string, str::FromStr};
use base64::Engine;
use proc_macro2::TokenStream;
@@ -15,6 +15,7 @@ use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
use tauri_utils::html::{
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
};
use tauri_utils::namespace::NamespaceLockFile;
use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
@@ -421,6 +422,15 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
}
};
let lockfile_path = config_parent.join("tauri.namespace.lock");
let lockfile: NamespaceLockFile = if lockfile_path.exists() {
let lockfile = read_to_string(&lockfile_path)?;
serde_json::from_str(&lockfile)?
} else {
Default::default()
};
let runtime_authority = runtime_authority_codegen(&root, lockfile);
Ok(quote!({
#[allow(unused_mut, clippy::let_and_return)]
let mut context = #root::Context::new(
@@ -431,12 +441,30 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
#package_info,
#info_plist,
#pattern,
#runtime_authority
);
#with_system_tray_icon_code
context
}))
}
fn runtime_authority_codegen(root: &TokenStream, lockfile: NamespaceLockFile) -> TokenStream {
let add_members = lockfile.resolution.iter().map(|r| {
let member = &r.member;
let commands = &r.commands;
let resolution = quote!(#root::runtime_authority::MemberResolution {
member: #member.into(),
commands: vec![#(#commands.into(),)*]
});
quote!(authority.add_member(#resolution);)
});
quote!({
let mut authority = #root::runtime_authority::RuntimeAuthority::new();
#(#add_members)*
authority
})
}
fn ico_icon<P: AsRef<Path>>(
root: &TokenStream,
out_dir: &Path,

View File

@@ -59,6 +59,12 @@ pub enum EmbeddedAssetsError {
#[error("version error: {0}")]
Version(#[from] semver::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
/// Represent a directory of assets that are compressed and embedded.

View File

@@ -45,7 +45,7 @@ pub enum CodegenConfigError {
ConfigError(#[from] ConfigError),
}
/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
/// Get the [`Config`] from the passed path and merge it with the value from the `TAURI_CONFIG` environment variable.
///
/// If the passed path is relative, it should be relative to the current working directory of the
/// compiling crate.
@@ -67,7 +67,8 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError>
// it is impossible for the content of two separate configs to get mixed up. The chances are
// already unlikely unless the developer goes out of their way to run the cli on a different
// project than the target crate.
let mut config = serde_json::from_value(tauri_utils::config::parse::read_from(parent.clone())?)?;
let (config, config_path) = tauri_utils::config::parse::read_from(parent.clone())?;
let mut config = serde_json::from_value(config)?;
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value =
serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?;
@@ -83,5 +84,11 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError>
// Reset working directory.
std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?;
Ok((config, parent))
Ok((
config,
config_path
.parent()
.map(ToOwned::to_owned)
.ok_or_else(|| CodegenConfigError::Parent(config_path))?,
))
}

View File

@@ -108,6 +108,14 @@
"$ref": "#/definitions/PluginConfig"
}
]
},
"namespaces": {
"description": "The namespaces defining what capabilities are enabled.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Namespace"
}
}
},
"additionalProperties": false,
@@ -2258,6 +2266,43 @@
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\nSee more: https://tauri.app/v1/api/config#pluginconfig",
"type": "object",
"additionalProperties": true
},
"Namespace": {
"description": "A namespace defining a set of capabilities that are enabled for a given window.",
"type": "object",
"required": [
"capabilities",
"id",
"members"
],
"properties": {
"id": {
"description": "Identifier of this namespace. Must be unique.\n\nIt is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.",
"type": "string"
},
"description": {
"description": "Describes the namespace in a human readable format.",
"type": [
"string",
"null"
]
},
"members": {
"description": "The windows that can use the configuration of this namespace.",
"type": "array",
"items": {
"type": "string"
}
},
"capabilities": {
"description": "List of capabilities attached to this namespace.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
}
}

View File

@@ -1916,6 +1916,27 @@ pub struct Config {
/// The plugins config.
#[serde(default)]
pub plugins: PluginConfig,
/// The namespaces defining what capabilities are enabled.
#[serde(default)]
pub namespaces: Vec<Namespace>,
}
/// A namespace defining a set of capabilities that are enabled for a given window.
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Namespace {
/// Identifier of this namespace. Must be unique.
///
/// It is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.
pub id: String,
/// Describes the namespace in a human readable format.
pub description: Option<String>,
/// The windows that can use the configuration of this namespace.
pub members: Vec<String>,
/// List of capabilities attached to this namespace.
pub capabilities: Vec<String>,
}
/// The plugin configs holds a HashMap mapping a plugin name to its configuration object.
@@ -2646,6 +2667,17 @@ mod build {
}
}
impl ToTokens for Namespace {
fn to_tokens(&self, tokens: &mut TokenStream) {
let id = str_lit(&self.id);
let description = opt_str_lit(self.description.as_ref());
let members = vec_lit(&self.members, str_lit);
let capabilities = vec_lit(&self.capabilities, str_lit);
literal_struct!(tokens, Namespace, id, description, members, capabilities);
}
}
impl ToTokens for Config {
fn to_tokens(&self, tokens: &mut TokenStream) {
let schema = quote!(None);
@@ -2653,8 +2685,9 @@ mod build {
let tauri = &self.tauri;
let build = &self.build;
let plugins = &self.plugins;
let namespaces = vec_lit(&self.namespaces, identity);
literal_struct!(tokens, Config, schema, package, tauri, build, plugins);
literal_struct!(tokens, Config, schema, package, tauri, build, plugins, namespaces);
}
}
}

View File

@@ -192,12 +192,12 @@ pub fn is_configuration_file(path: &Path) -> bool {
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
///
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
pub fn read_from(root_dir: PathBuf) -> Result<Value, ConfigError> {
let mut config: Value = parse_value(root_dir.join("tauri.conf.json"))?.0;
pub fn read_from(root_dir: PathBuf) -> Result<(Value, PathBuf), ConfigError> {
let (mut config, path) = parse_value(root_dir.join("tauri.conf.json"))?;
if let Some((platform_config, _)) = read_platform(root_dir)? {
merge(&mut config, &platform_config);
}
Ok(config)
Ok((config, path))
}
/// Reads the platform-specific configuration file from the given root directory if it exists.

View File

@@ -26,7 +26,9 @@ pub mod config;
pub mod html;
pub mod io;
pub mod mime_type;
pub mod namespace;
pub mod platform;
pub mod plugin;
/// Prepare application resources and sidecars.
#[cfg(feature = "resources")]
pub mod resources;

View File

@@ -0,0 +1,31 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Namespace lock file and utilities for the runtime authority.
use serde::{Deserialize, Serialize};
use crate::{config::Namespace, plugin::ManifestMap};
/// Resolved data associated with a member.
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct MemberResolution {
/// Member id.
pub member: String,
/// List of commands enabled.
pub commands: Vec<String>,
}
/// Lock file of the namespaces configuration.
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct NamespaceLockFile {
/// Lock file version.
pub version: u8,
/// Configured namespaces.
pub namespaces: Vec<Namespace>,
/// Collection of plugins and their manifests.
pub plugins: ManifestMap,
/// Resolved data.
pub resolution: Vec<MemberResolution>,
}

View File

@@ -0,0 +1,201 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Plugin manifest types.
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
};
const DEFAULT_CAPABILITY_ID: &str = "default";
/// Scope type definition.
#[derive(Debug, Serialize, Deserialize)]
pub enum ScopeType {
/// String type.
String,
}
/// Scope of a given capability.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilityScope {
/// Explicitly allow something.
#[serde(default)]
pub allowed: Vec<serde_json::Value>,
/// Explicitly deny something. Takes precedence over [`Self::allowed`].
#[serde(default)]
pub blocked: Vec<serde_json::Value>,
}
/// A capability defines a set of features and scope enabled for the plugin.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Capability {
/// The identifier of the capability. Must be unique.
#[serde(default)]
pub id: String,
/// The component this capability refers to.
///
/// Currently the possible values are plugin names.
///
/// When no value is set, it referes to the application itself.
pub component: Option<String>,
/// Describes the capability in a human readable format.
pub description: Option<String>,
/// List of features enabled by this capability.
#[serde(default)]
pub features: Vec<String>,
/// Scope defined by this capability. Only applies to the given features.
#[serde(default)]
pub scope: CapabilityScope,
}
/// An enum used to do serde operations with a list or a single capability.
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum CapabilityOrList {
/// A single capability.
Single(Capability),
/// A list of capabilities.
List(Vec<Capability>),
}
/// Plugin manifest.
#[derive(Debug, Serialize, Deserialize)]
pub struct Manifest {
/// Plugin name.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub plugin: String,
/// Default capability.
pub default_capability: Option<Capability>,
/// List of capabilities defined by the plugin.
pub capabilities: Vec<Capability>,
/// List of features defined by the plugin.
pub features: Vec<String>,
/// Scope types.
pub scope_type: Vec<ScopeType>,
}
impl Manifest {
/// Creates a new empty plugin manifest.
pub fn new(plugin: impl Into<String>) -> Self {
Self {
plugin: plugin.into(),
default_capability: None,
capabilities: Vec::new(),
features: Vec::new(),
scope_type: Vec::new(),
}
}
/// Sets the plugin's default capability set from a JSON string.
pub fn default_capability_json(mut self, default_capability: impl AsRef<str>) -> Self {
let mut capability: Capability = serde_json::from_str(default_capability.as_ref())
.expect("failed to deserialize default capability");
assert!(
capability.id.is_empty(),
"default capability cannot have an identifier"
);
capability.id = DEFAULT_CAPABILITY_ID.into();
self.default_capability.replace(capability);
self
}
/// Appends a capability from a JSON string. The JSON can also include an array of capabilities instead of a single one. See [`Capability`].
pub fn capability_json(self, capability: impl AsRef<str>) -> Self {
let capability =
serde_json::from_str(capability.as_ref()).expect("failed to deserialize default capability");
match capability {
CapabilityOrList::Single(cap) => self.capability(cap),
CapabilityOrList::List(l) => self.capabilities(l),
}
}
/// Appends a [`Capability`].
pub fn capability(mut self, capability: Capability) -> Self {
assert!(
!capability.id.is_empty(),
"capability must have an identifier"
);
self.capabilities.push(capability);
self
}
/// Appends the given list of capabilities. See [`Self::capability`].
pub fn capabilities<I: IntoIterator<Item = Capability>>(mut self, capabilities: I) -> Self {
for capability in capabilities {
self = self.capability(capability);
}
self
}
/// Appends the given feature on the list of plugin's features.
pub fn feature(mut self, feature: impl Into<String>) -> Self {
self.features.push(feature.into());
self
}
/// Appends the given list of features.
pub fn features<I: IntoIterator<Item = S>, S: Into<String>>(mut self, features: I) -> Self {
for feature in features {
self = self.feature(feature);
}
self
}
/// Appends the given scope type.
pub fn scope_type(mut self, ty: ScopeType) -> Self {
self.scope_type.push(ty);
self
}
}
/// A collection mapping a plugin name to its manifest.
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct ManifestMap(BTreeMap<String, Manifest>);
impl Deref for ManifestMap {
type Target = BTreeMap<String, Manifest>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ManifestMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<BTreeMap<String, Manifest>> for ManifestMap {
fn from(value: BTreeMap<String, Manifest>) -> Self {
Self(value)
}
}
impl ManifestMap {
/// Finds the capability with the given identifier.
pub fn find_capability(&self, id: &str) -> Vec<(String, Capability)> {
let mut capabilities = Vec::new();
for (plugin, manifest) in &self.0 {
if id == format!("{DEFAULT_CAPABILITY_ID}-{plugin}") {
capabilities.push((
plugin.clone(),
manifest.default_capability.clone().unwrap_or_default(),
));
}
for capability in &manifest.capabilities {
if capability.id == id {
capabilities.push((plugin.clone(), capability.clone()));
}
}
}
capabilities
}
}

View File

@@ -131,6 +131,12 @@ fn main() {
println!("cargo:ios_library_path={}", lib_path.display());
}
}
tauri_build::plugin::set_manifest(
tauri_build::plugin::Manifest::new("event").features(["emit", "listen"]),
);
tauri_build::plugin::set_manifest(tauri_build::plugin::Manifest::new("path"));
}
fn add_manifest() {

View File

@@ -66,6 +66,7 @@ pub use cocoa;
pub use embed_plist;
/// The Tauri error enum.
pub use error::Error;
use runtime_authority::RuntimeAuthority;
#[cfg(target_os = "ios")]
#[doc(hidden)]
pub use swift_rs;
@@ -85,6 +86,7 @@ mod hooks;
mod manager;
mod pattern;
pub mod plugin;
pub mod runtime_authority;
mod vibrancy;
pub mod window;
use tauri_runtime as runtime;
@@ -389,6 +391,7 @@ pub struct Context<A: Assets> {
pub(crate) package_info: PackageInfo,
pub(crate) _info_plist: (),
pub(crate) pattern: Pattern,
pub(crate) runtime_authority: RuntimeAuthority,
}
impl<A: Assets> fmt::Debug for Context<A> {
@@ -487,6 +490,7 @@ impl<A: Assets> Context<A> {
package_info: PackageInfo,
info_plist: (),
pattern: Pattern,
runtime_authority: RuntimeAuthority,
) -> Self {
Self {
config,
@@ -498,6 +502,7 @@ impl<A: Assets> Context<A> {
package_info,
_info_plist: info_plist,
pattern,
runtime_authority,
}
}

View File

@@ -25,7 +25,6 @@ use tauri_utils::{
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
};
use crate::app::{GlobalMenuEventListener, WindowMenuEvent};
use crate::hooks::IpcJavascript;
#[cfg(feature = "isolation")]
use crate::hooks::IsolationJavascript;
@@ -51,6 +50,10 @@ use crate::{
Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
WindowEvent,
};
use crate::{
app::{GlobalMenuEventListener, WindowMenuEvent},
runtime_authority::RuntimeAuthority,
};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use crate::path::BaseDirectory;
@@ -234,6 +237,7 @@ pub struct InnerWindowManager<R: Runtime> {
invoke_initialization_script: String,
/// Application pattern.
pub(crate) pattern: Pattern,
pub(crate) runtime_authority: RuntimeAuthority,
}
impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
@@ -334,6 +338,7 @@ impl<R: Runtime> WindowManager<R> {
window_event_listeners: Arc::new(window_event_listeners),
invoke_responder,
invoke_initialization_script,
runtime_authority: context.runtime_authority,
}),
}
}

View File

@@ -0,0 +1,30 @@
//! Runtime authority.
pub use tauri_utils::namespace::MemberResolution;
/// The runtime authority verifies if a given IPC call is authorized.
#[derive(Default)]
pub struct RuntimeAuthority {
members: Vec<MemberResolution>,
}
impl RuntimeAuthority {
/// Creates the default (empty) runtime authority.
pub fn new() -> Self {
Self::default()
}
/// Adds the given member resolution to this authority.
pub fn add_member(&mut self, member: MemberResolution) {
self.members.push(member);
}
/// Determines if the given command is allowed for the member.
pub fn is_allowed(&self, member: &str, command: &String) -> bool {
if let Some(member) = self.members.iter().find(|m| m.member == member) {
member.commands.contains(command)
} else {
false
}
}
}

View File

@@ -166,17 +166,27 @@ impl Scope {
#[cfg(test)]
mod tests {
use tauri_utils::namespace::MemberResolution;
use super::RemoteDomainAccessScope;
use crate::{
api::ipc::CallbackFn,
test::{assert_ipc_response, mock_app, MockRuntime},
test::{assert_ipc_response, mock_builder, mock_context, noop_assets, MockRuntime},
App, InvokePayload, Manager, Window, WindowBuilder,
};
const PLUGIN_NAME: &str = "test";
fn test_context(scopes: Vec<RemoteDomainAccessScope>) -> (App<MockRuntime>, Window<MockRuntime>) {
let app = mock_app();
let mut context = mock_context(noop_assets());
context.runtime_authority.add_member(MemberResolution {
member: "main".into(),
commands: vec![
"plugin:path|is_absolute".into(),
format!("plugin:{PLUGIN_NAME}|doSomething"),
],
});
let app = mock_builder().build(context).unwrap();
let window = WindowBuilder::new(&app, "main", Default::default())
.build()
.unwrap();

View File

@@ -132,6 +132,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
},
build: Default::default(),
plugins: Default::default(),
namespaces: Default::default(),
},
assets: Arc::new(assets),
default_window_icon: None,
@@ -147,6 +148,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
},
_info_plist: (),
pattern: Pattern::Brownfield(std::marker::PhantomData),
runtime_authority: Default::default(),
}
}

View File

@@ -1691,6 +1691,15 @@ impl<R: Runtime> Window<R> {
return Ok(());
}
if !manager
.inner
.runtime_authority
.is_allowed(self.label(), &payload.cmd)
{
invoke.resolver.reject("Not allowed");
return Ok(());
}
if payload.cmd.starts_with("plugin:") {
if !is_local {
let command = invoke.message.command.replace("plugin:", "");

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,15 @@
// SPDX-License-Identifier: MIT
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.capability_json(include_str!("./capabilities/allow-commands.json")),
)
.expect("failed to run tauri-build");
let mut codegen = tauri_build::CodegenContext::new();
if !cfg!(feature = "custom-protocol") {
codegen = codegen.dev();
}
codegen.build();
tauri_build::build();
}

View File

@@ -0,0 +1,8 @@
{
"id": "allow-all-api-commands",
"description": "Allows all application defined commands",
"features": [
"log_operation",
"perform_request"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,13 @@
// SPDX-License-Identifier: MIT
use std::process::exit;
use tauri_build::{
mobile::PluginBuilder,
plugin::{set_manifest, Manifest, ScopeType},
};
fn main() {
if let Err(error) = tauri_build::mobile::PluginBuilder::new()
if let Err(error) = PluginBuilder::new()
.android_path("android")
.ios_path("ios")
.run()
@@ -13,4 +17,12 @@ fn main() {
println!("{error:#}");
exit(1);
}
set_manifest(
Manifest::new("sample")
.default_capability_json(include_str!("capabilities/default.json"))
.capability_json(include_str!("capabilities/ping.json"))
.feature("ping")
.scope_type(ScopeType::String),
);
}

View File

@@ -0,0 +1,8 @@
{
"features": [],
"description": "Default empty capability set",
"scope": {
"allowed": [],
"denied": []
}
}

View File

@@ -0,0 +1,7 @@
{
"id": "allow-ping",
"description": "Allows the ping command",
"features": [
"ping"
]
}

View File

@@ -106,5 +106,11 @@
"iconAsTemplate": true,
"menuOnLeftClick": false
}
}
},
"namespaces": [{
"id": "main",
"description": "Main window namespace",
"members": ["main"],
"capabilities": ["allow-all-api-commands", "allow-ping", "allow-emit", "allow-listen"]
}]
}

View File

@@ -0,0 +1,127 @@
{
"version": 1,
"namespaces": [
{
"id": "main",
"description": "Main window namespace",
"members": [
"main"
],
"capabilities": [
"allow-all-api-commands",
"allow-ping",
"allow-emit",
"allow-listen"
]
}
],
"plugins": {
"__app__": {
"default_capability": null,
"capabilities": [
{
"id": "allow-all-api-commands",
"component": null,
"description": "Allows all application defined commands",
"features": [
"log_operation",
"perform_request"
],
"scope": {
"allowed": [],
"blocked": []
}
}
],
"features": [],
"scope_type": []
},
"event": {
"plugin": "event",
"default_capability": null,
"capabilities": [
{
"id": "allow-emit",
"component": null,
"description": "Allows the emit functionality",
"features": [
"emit"
],
"scope": {
"allowed": [],
"blocked": []
}
},
{
"id": "allow-listen",
"component": null,
"description": "Allows the listen functionality",
"features": [
"listen"
],
"scope": {
"allowed": [],
"blocked": []
}
}
],
"features": [
"emit",
"listen"
],
"scope_type": []
},
"path": {
"plugin": "path",
"default_capability": null,
"capabilities": [],
"features": [],
"scope_type": []
},
"sample": {
"plugin": "sample",
"default_capability": {
"id": "default",
"component": null,
"description": "Default empty capability set",
"features": [],
"scope": {
"allowed": [],
"blocked": []
}
},
"capabilities": [
{
"id": "allow-ping",
"component": null,
"description": "Allows the ping command",
"features": [
"ping"
],
"scope": {
"allowed": [],
"blocked": []
}
}
],
"features": [
"ping"
],
"scope_type": [
"String"
]
}
},
"resolution": [
{
"member": "main",
"commands": [
"log_operation",
"perform_request",
"plugin:sample|ping",
"plugin:event|emit",
"plugin:event|listen"
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -3907,9 +3907,9 @@ dependencies = [
[[package]]
name = "tar"
version = "0.4.38"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
checksum = "ec96d2ffad078296368d46ff1cb309be1c23c513b4ab0e22a45de0185275ac96"
dependencies = [
"filetime",
"libc",

View File

@@ -108,6 +108,14 @@
"$ref": "#/definitions/PluginConfig"
}
]
},
"namespaces": {
"description": "The namespaces defining what capabilities are enabled.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Namespace"
}
}
},
"additionalProperties": false,
@@ -2258,6 +2266,43 @@
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\nSee more: https://tauri.app/v1/api/config#pluginconfig",
"type": "object",
"additionalProperties": true
},
"Namespace": {
"description": "A namespace defining a set of capabilities that are enabled for a given window.",
"type": "object",
"required": [
"capabilities",
"id",
"members"
],
"properties": {
"id": {
"description": "Identifier of this namespace. Must be unique.\n\nIt is recommended to use `drop-` or `allow-` prefixes to ensure the rule can be easily categorized.",
"type": "string"
},
"description": {
"description": "Describes the namespace in a human readable format.",
"type": [
"string",
"null"
]
},
"members": {
"description": "The windows that can use the configuration of this namespace.",
"type": "array",
"items": {
"type": "string"
}
},
"capabilities": {
"description": "List of capabilities attached to this namespace.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
}
}