mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-08-16 19:30:27 +03:00
* refactor(core): capabilities must be referenced on the Tauri config file * add all capabilities by default * feat(codegen): allow defining additional capabilities, closes #8798 * undo example * lint * move add_capability to runtime authority * add change files * go through code review * fix tests * remove tokens option
This commit is contained in:
parent
770051ae63
commit
8d16a80d2f
6
.changes/codegen-capabilities-attribute.md
Normal file
6
.changes/codegen-capabilities-attribute.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-macros": patch:enhance
|
||||
"tauri-codegen": patch:enhance
|
||||
---
|
||||
|
||||
The `generate_context` proc macro now accepts a `capabilities` attribute where the value is an array of file paths that can be [conditionally compiled](https://doc.rust-lang.org/reference/conditional-compilation.html). These capabilities are added to the application along the capabilities defined in the Tauri configuration file.
|
5
.changes/context-runtime-authority.md
Normal file
5
.changes/context-runtime-authority.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-utils": patch:enhance
|
||||
---
|
||||
|
||||
The `Context` struct now includes the runtime authority instead of the resolved ACL. This does not impact most applications.
|
5
.changes/tauri-build-codegen-capabilities.md
Normal file
5
.changes/tauri-build-codegen-capabilities.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-build": patch:enhance
|
||||
---
|
||||
|
||||
Added `CodegenContext::capability` to include a capability file dynamically.
|
5
.changes/tauri-utils-capability-refactor.md
Normal file
5
.changes/tauri-utils-capability-refactor.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-utils": patch:enhance
|
||||
---
|
||||
|
||||
Refactored the capability types and resolution algorithm.
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4149,6 +4149,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.49",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
@ -17,7 +17,10 @@ use schemars::{
|
||||
schema_for,
|
||||
};
|
||||
use tauri_utils::{
|
||||
acl::{build::CapabilityFile, capability::Capability, plugin::Manifest},
|
||||
acl::{
|
||||
capability::{Capability, CapabilityFile},
|
||||
plugin::Manifest,
|
||||
},
|
||||
platform::Target,
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
env::var,
|
||||
fs::{create_dir_all, File},
|
||||
io::{BufWriter, Write},
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri_codegen::{context_codegen, ContextData};
|
||||
use tauri_utils::config::FrontendDist;
|
||||
@ -20,6 +20,7 @@ pub struct CodegenContext {
|
||||
dev: bool,
|
||||
config_path: PathBuf,
|
||||
out_file: PathBuf,
|
||||
capabilities: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl Default for CodegenContext {
|
||||
@ -28,6 +29,7 @@ impl Default for CodegenContext {
|
||||
dev: false,
|
||||
config_path: PathBuf::from("tauri.conf.json"),
|
||||
out_file: PathBuf::from("tauri-build-context.rs"),
|
||||
capabilities: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,6 +76,16 @@ impl CodegenContext {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a capability file to the generated context.
|
||||
#[must_use]
|
||||
pub fn capability<P: AsRef<Path>>(mut self, path: P) -> Self {
|
||||
self
|
||||
.capabilities
|
||||
.get_or_insert_with(Default::default)
|
||||
.push(path.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the code and write it to the output file - returning the path it was saved to.
|
||||
///
|
||||
/// Unless you are doing something special with this builder, you don't need to do anything with
|
||||
@ -125,6 +137,7 @@ impl CodegenContext {
|
||||
// it's very hard to have a build script for unit tests, so assume this is always called from
|
||||
// outside the tauri crate, making the ::tauri root valid.
|
||||
root: quote::quote!(::tauri),
|
||||
capabilities: self.capabilities,
|
||||
})?;
|
||||
|
||||
// get the full output file path
|
||||
|
@ -17,7 +17,8 @@ sha2 = "0.10"
|
||||
base64 = "0.21"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
syn = "2"
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
serde_json = "1"
|
||||
tauri-utils = { version = "2.0.0-beta.1", path = "../tauri-utils", features = [ "build" ] }
|
||||
thiserror = "1"
|
||||
|
@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::identity;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{ffi::OsStr, str::FromStr};
|
||||
|
||||
@ -11,7 +12,7 @@ use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use tauri_utils::acl::capability::Capability;
|
||||
use tauri_utils::acl::capability::{Capability, CapabilityFile};
|
||||
use tauri_utils::acl::plugin::Manifest;
|
||||
use tauri_utils::acl::resolved::Resolved;
|
||||
use tauri_utils::assets::AssetKey;
|
||||
@ -20,6 +21,7 @@ use tauri_utils::html::{
|
||||
inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node,
|
||||
};
|
||||
use tauri_utils::platform::Target;
|
||||
use tauri_utils::tokens::{map_lit, str_lit};
|
||||
|
||||
use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
|
||||
|
||||
@ -32,6 +34,8 @@ pub struct ContextData {
|
||||
pub config: Config,
|
||||
pub config_parent: PathBuf,
|
||||
pub root: TokenStream,
|
||||
/// Additional capabilities to include.
|
||||
pub capabilities: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
fn map_core_assets(
|
||||
@ -126,6 +130,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
config,
|
||||
config_parent,
|
||||
root,
|
||||
capabilities: additional_capabilities,
|
||||
} = data;
|
||||
|
||||
let target = std::env::var("TARGET")
|
||||
@ -390,7 +395,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let capabilities = if config.app.security.capabilities.is_empty() {
|
||||
let mut capabilities = if config.app.security.capabilities.is_empty() {
|
||||
capabilities_from_files
|
||||
} else {
|
||||
let mut capabilities = BTreeMap::new();
|
||||
@ -410,7 +415,36 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
capabilities
|
||||
};
|
||||
|
||||
let resolved_acl = Resolved::resolve(acl, capabilities, target).expect("failed to resolve ACL");
|
||||
let acl_tokens = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&acl,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
if let Some(paths) = additional_capabilities {
|
||||
for path in paths {
|
||||
let capability = CapabilityFile::load(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
|
||||
match capability {
|
||||
CapabilityFile::Capability(c) => {
|
||||
capabilities.insert(c.identifier.clone(), c);
|
||||
}
|
||||
CapabilityFile::List {
|
||||
capabilities: capabilities_list,
|
||||
} => {
|
||||
capabilities.extend(
|
||||
capabilities_list
|
||||
.into_iter()
|
||||
.map(|c| (c.identifier.clone(), c)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
|
||||
let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
|
||||
|
||||
Ok(quote!({
|
||||
#[allow(unused_mut, clippy::let_and_return)]
|
||||
@ -422,7 +456,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
#package_info,
|
||||
#info_plist,
|
||||
#pattern,
|
||||
#resolved_acl
|
||||
#runtime_authority
|
||||
);
|
||||
#with_tray_icon_code
|
||||
context
|
||||
|
@ -4,11 +4,11 @@
|
||||
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use std::{env::VarError, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use syn::{
|
||||
parse::{Parse, ParseBuffer},
|
||||
punctuated::Punctuated,
|
||||
LitStr, PathArguments, PathSegment, Token,
|
||||
Expr, ExprLit, Lit, LitStr, Meta, PathArguments, PathSegment, Token,
|
||||
};
|
||||
use tauri_codegen::{context_codegen, get_config, ContextData};
|
||||
use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Target};
|
||||
@ -16,6 +16,7 @@ use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Targe
|
||||
pub(crate) struct ContextItems {
|
||||
config_file: PathBuf,
|
||||
root: syn::Path,
|
||||
capabilities: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl Parse for ContextItems {
|
||||
@ -26,21 +27,18 @@ impl Parse for ContextItems {
|
||||
.map(Target::from_triple)
|
||||
.unwrap_or_else(|_| Target::current());
|
||||
|
||||
let config_file = if input.is_empty() {
|
||||
std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join("tauri.conf.json"))
|
||||
} else {
|
||||
let raw: LitStr = input.parse()?;
|
||||
let mut root = None;
|
||||
let mut capabilities = None;
|
||||
let config_file = input.parse::<LitStr>().ok().map(|raw| {
|
||||
let _ = input.parse::<Token![,]>();
|
||||
let path = PathBuf::from(raw.value());
|
||||
if path.is_relative() {
|
||||
std::env::var("CARGO_MANIFEST_DIR").map(|m| PathBuf::from(m).join(path))
|
||||
std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(|m| PathBuf::from(m).join(path))
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
.map_err(|error| match error {
|
||||
VarError::NotPresent => "no CARGO_MANIFEST_DIR env var, this should be set by cargo".into(),
|
||||
VarError::NotUnicode(_) => "CARGO_MANIFEST_DIR env var contained invalid utf8".into(),
|
||||
})
|
||||
.and_then(|path| {
|
||||
if does_supported_file_name_exist(target, &path) {
|
||||
Ok(path)
|
||||
@ -51,9 +49,59 @@ impl Parse for ContextItems {
|
||||
))
|
||||
}
|
||||
})
|
||||
.map_err(|e| input.error(e))?;
|
||||
});
|
||||
|
||||
let context_path = if input.is_empty() {
|
||||
while let Ok(meta) = input.parse::<Meta>() {
|
||||
match meta {
|
||||
Meta::Path(p) => {
|
||||
root.replace(p);
|
||||
}
|
||||
Meta::NameValue(v) => {
|
||||
if *v.path.require_ident()? == "capabilities" {
|
||||
if let Expr::Array(array) = v.value {
|
||||
capabilities.replace(
|
||||
array
|
||||
.elems
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
if let Expr::Lit(ExprLit {
|
||||
attrs: _,
|
||||
lit: Lit::Str(s),
|
||||
}) = e
|
||||
{
|
||||
Ok(s.value().into())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected expression for capability",
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, syn::Error>>()?,
|
||||
);
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected value for capabilities",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Meta::List(_) => {
|
||||
return Err(syn::Error::new(input.span(), "unexpected list input"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config_file: config_file
|
||||
.unwrap_or_else(|| {
|
||||
std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(|m| PathBuf::from(m).join("tauri.conf.json"))
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.map_err(|e| input.error(e))?,
|
||||
root: root.unwrap_or_else(|| {
|
||||
let mut segments = Punctuated::new();
|
||||
segments.push(PathSegment {
|
||||
ident: Ident::new("tauri", Span::call_site()),
|
||||
@ -63,14 +111,8 @@ impl Parse for ContextItems {
|
||||
leading_colon: Some(Token![::](Span::call_site())),
|
||||
segments,
|
||||
}
|
||||
} else {
|
||||
let _: Token![,] = input.parse()?;
|
||||
input.call(syn::Path::parse_mod_style)?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config_file,
|
||||
root: context_path,
|
||||
}),
|
||||
capabilities,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -83,6 +125,7 @@ pub(crate) fn generate_context(context: ContextItems) -> TokenStream {
|
||||
config,
|
||||
config_parent,
|
||||
root: context.root.to_token_stream(),
|
||||
capabilities: context.capabilities,
|
||||
})
|
||||
.and_then(|data| context_codegen(data).map_err(|e| e.to_string()));
|
||||
|
||||
|
@ -16,9 +16,11 @@ use schemars::{
|
||||
schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation},
|
||||
schema_for,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{capability::Capability, plugin::PermissionFile};
|
||||
use super::{
|
||||
capability::{Capability, CapabilityFile},
|
||||
plugin::PermissionFile,
|
||||
};
|
||||
|
||||
/// Known name of the folder containing autogenerated permissions.
|
||||
pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
|
||||
@ -49,19 +51,6 @@ const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
|
||||
|
||||
const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
|
||||
|
||||
/// Capability formats accepted in a capability file.
|
||||
#[derive(Deserialize, schemars::JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum CapabilityFile {
|
||||
/// A single capability.
|
||||
Capability(Capability),
|
||||
/// A list of capabilities.
|
||||
List {
|
||||
/// The list of capabilities.
|
||||
capabilities: Vec<Capability>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
|
||||
pub fn define_permissions(
|
||||
pattern: &str,
|
||||
@ -143,15 +132,7 @@ pub fn parse_capabilities(
|
||||
// TODO: remove this before stable
|
||||
.filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
|
||||
{
|
||||
let capability_file = std::fs::read_to_string(&path).map_err(Error::ReadFile)?;
|
||||
let ext = path.extension().unwrap().to_string_lossy().to_string();
|
||||
let capability: CapabilityFile = match ext.as_str() {
|
||||
"toml" => toml::from_str(&capability_file)?,
|
||||
"json" => serde_json::from_str(&capability_file)?,
|
||||
_ => return Err(Error::UnknownCapabilityFormat(ext)),
|
||||
};
|
||||
|
||||
match capability {
|
||||
match CapabilityFile::load(&path)? {
|
||||
CapabilityFile::Capability(capability) => {
|
||||
capabilities_map.insert(capability.identifier.clone(), capability);
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
//! 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, Serialize};
|
||||
|
||||
@ -101,6 +103,47 @@ pub enum CapabilityContext {
|
||||
},
|
||||
}
|
||||
|
||||
/// Capability formats accepted in a capability file.
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum CapabilityFile {
|
||||
/// A single capability.
|
||||
Capability(Capability),
|
||||
/// A list of capabilities.
|
||||
List {
|
||||
/// 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(super::Error::ReadFile)?;
|
||||
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)?,
|
||||
_ => return Err(super::Error::UnknownCapabilityFormat(ext)),
|
||||
};
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CapabilityFile {
|
||||
type Err = super::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.chars().next() {
|
||||
Some('[') => toml::from_str(s).map_err(Into::into),
|
||||
Some('{') => serde_json::from_str(s).map_err(Into::into),
|
||||
_ => Err(super::Error::UnknownCapabilityFormat(s.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "build")]
|
||||
mod build {
|
||||
use std::convert::identity;
|
||||
|
@ -77,11 +77,8 @@ pub struct CommandKey {
|
||||
}
|
||||
|
||||
/// Resolved access control list.
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Resolved {
|
||||
/// ACL plugin manifests.
|
||||
#[cfg(debug_assertions)]
|
||||
pub acl: BTreeMap<String, Manifest>,
|
||||
/// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`].
|
||||
pub allowed_commands: BTreeMap<CommandKey, ResolvedCommand>,
|
||||
/// The commands that are denied. Map each command with its context to a [`ResolvedCommand`].
|
||||
@ -92,21 +89,10 @@ pub struct Resolved {
|
||||
pub global_scope: BTreeMap<String, ResolvedScope>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Resolved {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Resolved")
|
||||
.field("allowed_commands", &self.allowed_commands)
|
||||
.field("denied_commands", &self.denied_commands)
|
||||
.field("command_scope", &self.command_scope)
|
||||
.field("global_scope", &self.global_scope)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolved {
|
||||
/// Resolves the ACL for the given plugin permissions and app capabilities.
|
||||
pub fn resolve(
|
||||
acl: BTreeMap<String, Manifest>,
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
capabilities: BTreeMap<String, Capability>,
|
||||
target: Target,
|
||||
) -> Result<Self, Error> {
|
||||
@ -123,67 +109,25 @@ impl Resolved {
|
||||
continue;
|
||||
}
|
||||
|
||||
for permission_entry in &capability.permissions {
|
||||
let permission_id = permission_entry.identifier();
|
||||
let permission_name = permission_id.get_base();
|
||||
|
||||
if let Some(plugin_name) = permission_id.get_prefix() {
|
||||
let permissions = get_permissions(plugin_name, permission_name, &acl)?;
|
||||
|
||||
let mut resolved_scope = Scopes::default();
|
||||
let mut commands = Commands::default();
|
||||
|
||||
if let PermissionEntry::ExtendedPermission {
|
||||
identifier: _,
|
||||
with_resolved_permissions(
|
||||
capability,
|
||||
acl,
|
||||
|ResolvedPermission {
|
||||
plugin_name,
|
||||
permission_name,
|
||||
commands,
|
||||
scope,
|
||||
} = permission_entry
|
||||
{
|
||||
if let Some(allow) = scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
}
|
||||
|
||||
for permission in permissions {
|
||||
if let Some(allow) = permission.scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = permission.scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
|
||||
commands.allow.extend(permission.commands.allow.clone());
|
||||
commands.deny.extend(permission.commands.deny.clone());
|
||||
}
|
||||
|
||||
}| {
|
||||
if commands.allow.is_empty() && commands.deny.is_empty() {
|
||||
// global scope
|
||||
global_scope
|
||||
.entry(plugin_name.to_string())
|
||||
.or_default()
|
||||
.push(resolved_scope);
|
||||
.push(scope);
|
||||
} else {
|
||||
let has_scope = resolved_scope.allow.is_some() || resolved_scope.deny.is_some();
|
||||
if has_scope {
|
||||
let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
|
||||
current_scope_id += 1;
|
||||
command_scopes.insert(current_scope_id, resolved_scope);
|
||||
}
|
||||
|
||||
let scope_id = if has_scope {
|
||||
command_scopes.insert(current_scope_id, scope);
|
||||
Some(current_scope_id)
|
||||
} else {
|
||||
None
|
||||
@ -211,8 +155,8 @@ impl Resolved {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
// resolve scopes
|
||||
@ -264,8 +208,6 @@ impl Resolved {
|
||||
.collect();
|
||||
|
||||
let resolved = Self {
|
||||
#[cfg(debug_assertions)]
|
||||
acl,
|
||||
allowed_commands: allowed_commands
|
||||
.into_iter()
|
||||
.map(|(key, cmd)| {
|
||||
@ -316,6 +258,77 @@ fn parse_glob_patterns(raw: HashSet<String>) -> Result<Vec<glob::Pattern>, Error
|
||||
Ok(patterns)
|
||||
}
|
||||
|
||||
struct ResolvedPermission<'a> {
|
||||
plugin_name: &'a str,
|
||||
permission_name: &'a str,
|
||||
commands: Commands,
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>)>(
|
||||
capability: &Capability,
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
mut f: F,
|
||||
) -> Result<(), Error> {
|
||||
for permission_entry in &capability.permissions {
|
||||
let permission_id = permission_entry.identifier();
|
||||
let permission_name = permission_id.get_base();
|
||||
|
||||
if let Some(plugin_name) = permission_id.get_prefix() {
|
||||
let permissions = get_permissions(plugin_name, permission_name, acl)?;
|
||||
|
||||
let mut resolved_scope = Scopes::default();
|
||||
let mut commands = Commands::default();
|
||||
|
||||
if let PermissionEntry::ExtendedPermission {
|
||||
identifier: _,
|
||||
scope,
|
||||
} = permission_entry
|
||||
{
|
||||
if let Some(allow) = scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
}
|
||||
|
||||
for permission in permissions {
|
||||
if let Some(allow) = permission.scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = permission.scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
|
||||
commands.allow.extend(permission.commands.allow.clone());
|
||||
commands.deny.extend(permission.commands.deny.clone());
|
||||
}
|
||||
|
||||
f(ResolvedPermission {
|
||||
plugin_name,
|
||||
permission_name,
|
||||
commands,
|
||||
scope: resolved_scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolvedCommandTemp {
|
||||
#[cfg(debug_assertions)]
|
||||
@ -325,7 +338,6 @@ struct ResolvedCommandTemp {
|
||||
pub scope: Vec<ScopeKey>,
|
||||
pub resolved_scope_key: Option<ScopeKey>,
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
commands: &mut BTreeMap<CommandKey, ResolvedCommandTemp>,
|
||||
command: String,
|
||||
@ -511,14 +523,6 @@ mod build {
|
||||
|
||||
impl ToTokens for Resolved {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
#[cfg(debug_assertions)]
|
||||
let acl = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.acl,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
let allowed_commands = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.allowed_commands,
|
||||
@ -547,19 +551,6 @@ mod build {
|
||||
identity,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::Resolved,
|
||||
acl,
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
global_scope
|
||||
)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::Resolved,
|
||||
|
@ -8,6 +8,8 @@ use std::{collections::BTreeMap, ops::Deref};
|
||||
use serde::de::DeserializeOwned;
|
||||
use state::TypeMap;
|
||||
|
||||
use tauri_utils::acl::capability::CapabilityFile;
|
||||
use tauri_utils::acl::plugin::Manifest;
|
||||
use tauri_utils::acl::Value;
|
||||
use tauri_utils::acl::{
|
||||
resolved::{CommandKey, Resolved, ResolvedCommand, ResolvedScope, ScopeKey},
|
||||
@ -21,7 +23,6 @@ use super::{CommandArg, CommandItem};
|
||||
|
||||
/// The runtime authority used to authorize IPC execution based on the Access Control List.
|
||||
pub struct RuntimeAuthority {
|
||||
#[cfg(debug_assertions)]
|
||||
acl: BTreeMap<String, crate::utils::acl::plugin::Manifest>,
|
||||
allowed_commands: BTreeMap<CommandKey, ResolvedCommand>,
|
||||
denied_commands: BTreeMap<CommandKey, ResolvedCommand>,
|
||||
@ -64,15 +65,15 @@ impl Origin {
|
||||
}
|
||||
|
||||
impl RuntimeAuthority {
|
||||
pub(crate) fn new(resolved_acl: Resolved) -> Self {
|
||||
#[doc(hidden)]
|
||||
pub fn new(acl: BTreeMap<String, Manifest>, resolved_acl: Resolved) -> Self {
|
||||
let command_cache = resolved_acl
|
||||
.command_scope
|
||||
.keys()
|
||||
.map(|key| (*key, <TypeMap![Send + Sync]>::new()))
|
||||
.collect();
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
acl: resolved_acl.acl,
|
||||
acl,
|
||||
allowed_commands: resolved_acl.allowed_commands,
|
||||
denied_commands: resolved_acl.denied_commands,
|
||||
scope_manager: ScopeManager {
|
||||
@ -84,6 +85,93 @@ impl RuntimeAuthority {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn __allow_command(&mut self, command: String, context: ExecutionContext) {
|
||||
self.allowed_commands.insert(
|
||||
CommandKey {
|
||||
name: command,
|
||||
context,
|
||||
},
|
||||
ResolvedCommand {
|
||||
windows: vec!["*".parse().unwrap()],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds the given capability to the runtime authority.
|
||||
pub fn add_capability(&mut self, capability: CapabilityFile) -> crate::Result<()> {
|
||||
let mut capabilities = BTreeMap::new();
|
||||
match capability {
|
||||
CapabilityFile::Capability(c) => {
|
||||
capabilities.insert(c.identifier.clone(), c);
|
||||
}
|
||||
CapabilityFile::List {
|
||||
capabilities: capabilities_list,
|
||||
} => {
|
||||
capabilities.extend(
|
||||
capabilities_list
|
||||
.into_iter()
|
||||
.map(|c| (c.identifier.clone(), c)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let resolved = Resolved::resolve(
|
||||
&self.acl,
|
||||
capabilities,
|
||||
tauri_utils::platform::Target::current(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// fill global scope
|
||||
for (plugin, global_scope) in resolved.global_scope {
|
||||
let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default();
|
||||
|
||||
global_scope_entry.allow.extend(global_scope.allow);
|
||||
global_scope_entry.deny.extend(global_scope.deny);
|
||||
|
||||
self.scope_manager.global_scope_cache = Default::default();
|
||||
}
|
||||
|
||||
// denied commands
|
||||
for (cmd_key, resolved_cmd) in resolved.denied_commands {
|
||||
let entry = self.denied_commands.entry(cmd_key).or_default();
|
||||
|
||||
entry.windows.extend(resolved_cmd.windows);
|
||||
#[cfg(debug_assertions)]
|
||||
entry.referenced_by.extend(resolved_cmd.referenced_by);
|
||||
}
|
||||
|
||||
// allowed commands
|
||||
for (cmd_key, resolved_cmd) in resolved.allowed_commands {
|
||||
let entry = self.allowed_commands.entry(cmd_key).or_default();
|
||||
|
||||
entry.windows.extend(resolved_cmd.windows);
|
||||
#[cfg(debug_assertions)]
|
||||
entry.referenced_by.extend(resolved_cmd.referenced_by);
|
||||
|
||||
// fill command scope
|
||||
if let Some(scope_id) = resolved_cmd.scope {
|
||||
let command_scope = resolved.command_scope.get(&scope_id).unwrap();
|
||||
|
||||
let command_scope_entry = self
|
||||
.scope_manager
|
||||
.command_scope
|
||||
.entry(scope_id)
|
||||
.or_default();
|
||||
command_scope_entry
|
||||
.allow
|
||||
.extend(command_scope.allow.clone());
|
||||
command_scope_entry.deny.extend(command_scope.deny.clone());
|
||||
|
||||
self.scope_manager.command_cache.remove(&scope_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn resolve_access_message(
|
||||
&self,
|
||||
@ -488,10 +576,13 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
@ -522,10 +613,13 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
@ -559,10 +653,13 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
@ -598,10 +695,13 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
@ -634,10 +734,13 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert!(authority
|
||||
.resolve_access(
|
||||
@ -679,11 +782,14 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
let authority = RuntimeAuthority::new(
|
||||
Default::default(),
|
||||
Resolved {
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
..Default::default()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert!(authority
|
||||
.resolve_access(&command.name, window, webview, &Origin::Local)
|
||||
|
@ -69,6 +69,7 @@ pub use cocoa;
|
||||
#[doc(hidden)]
|
||||
pub use embed_plist;
|
||||
pub use error::{Error, Result};
|
||||
use ipc::RuntimeAuthority;
|
||||
pub use resources::{Resource, ResourceId, ResourceTable};
|
||||
#[cfg(target_os = "ios")]
|
||||
#[doc(hidden)]
|
||||
@ -193,7 +194,6 @@ use std::{
|
||||
fmt::{self, Debug},
|
||||
sync::MutexGuard,
|
||||
};
|
||||
use utils::acl::resolved::Resolved;
|
||||
|
||||
#[cfg(feature = "wry")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "wry")))]
|
||||
@ -432,7 +432,7 @@ pub struct Context<A: Assets> {
|
||||
pub(crate) package_info: PackageInfo,
|
||||
pub(crate) _info_plist: (),
|
||||
pub(crate) pattern: Pattern,
|
||||
pub(crate) resolved_acl: Resolved,
|
||||
pub(crate) runtime_authority: RuntimeAuthority,
|
||||
}
|
||||
|
||||
impl<A: Assets> fmt::Debug for Context<A> {
|
||||
@ -529,8 +529,8 @@ impl<A: Assets> Context<A> {
|
||||
/// This API is unstable.
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
pub fn resolved_acl(&mut self) -> &mut Resolved {
|
||||
&mut self.resolved_acl
|
||||
pub fn runtime_authority_mut(&mut self) -> &mut RuntimeAuthority {
|
||||
&mut self.runtime_authority
|
||||
}
|
||||
|
||||
/// Create a new [`Context`] from the minimal required items.
|
||||
@ -544,7 +544,7 @@ impl<A: Assets> Context<A> {
|
||||
package_info: PackageInfo,
|
||||
info_plist: (),
|
||||
pattern: Pattern,
|
||||
resolved_acl: Resolved,
|
||||
runtime_authority: RuntimeAuthority,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
@ -556,7 +556,7 @@ impl<A: Assets> Context<A> {
|
||||
package_info,
|
||||
_info_plist: info_plist,
|
||||
pattern,
|
||||
resolved_acl,
|
||||
runtime_authority,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ impl<R: Runtime> AppManager<R> {
|
||||
}
|
||||
|
||||
Self {
|
||||
runtime_authority: RuntimeAuthority::new(context.resolved_acl),
|
||||
runtime_authority: context.runtime_authority,
|
||||
window: window::WindowManager {
|
||||
windows: Mutex::default(),
|
||||
default_icon: context.default_window_icon,
|
||||
|
@ -55,7 +55,7 @@ use serde::Serialize;
|
||||
use std::{borrow::Cow, collections::HashMap, fmt::Debug};
|
||||
|
||||
use crate::{
|
||||
ipc::{InvokeBody, InvokeError, InvokeResponse},
|
||||
ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority},
|
||||
webview::InvokeRequest,
|
||||
App, Builder, Context, Pattern, Webview,
|
||||
};
|
||||
@ -126,14 +126,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
|
||||
},
|
||||
_info_plist: (),
|
||||
pattern: Pattern::Brownfield(std::marker::PhantomData),
|
||||
resolved_acl: Resolved {
|
||||
#[cfg(debug_assertions)]
|
||||
acl: Default::default(),
|
||||
allowed_commands: Default::default(),
|
||||
denied_commands: Default::default(),
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
},
|
||||
runtime_authority: RuntimeAuthority::new(Default::default(), Resolved::default()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ mod tests {
|
||||
let capabilities = parse_capabilities(&format!("{}/cap*", fixture_entry.path().display()))
|
||||
.expect("failed to parse capabilities");
|
||||
|
||||
let resolved = Resolved::resolve(manifests, capabilities, Target::current())
|
||||
let resolved = Resolved::resolve(&manifests, capabilities, Target::current())
|
||||
.expect("failed to resolve ACL");
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
|
1
examples/api/src-tauri/Cargo.lock
generated
1
examples/api/src-tauri/Cargo.lock
generated
@ -3767,6 +3767,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.48",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
@ -5,10 +5,7 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use tauri::{webview::PageLoadEvent, WebviewWindowBuilder};
|
||||
use tauri_utils::acl::{
|
||||
resolved::{CommandKey, ResolvedCommand},
|
||||
ExecutionContext,
|
||||
};
|
||||
use tauri_utils::acl::ExecutionContext;
|
||||
|
||||
fn main() {
|
||||
let mut context = tauri::generate_context!("../../examples/multiwindow/tauri.conf.json");
|
||||
@ -17,16 +14,9 @@ fn main() {
|
||||
"plugin:event|emit",
|
||||
"plugin:event|emit_to",
|
||||
] {
|
||||
context.resolved_acl().allowed_commands.insert(
|
||||
CommandKey {
|
||||
name: cmd.into(),
|
||||
context: ExecutionContext::Local,
|
||||
},
|
||||
ResolvedCommand {
|
||||
windows: vec!["*".parse().unwrap()],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
context
|
||||
.runtime_authority_mut()
|
||||
.__allow_command(cmd.to_string(), ExecutionContext::Local);
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
|
@ -5,10 +5,7 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use tauri::{webview::PageLoadEvent, WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri_utils::acl::{
|
||||
resolved::{CommandKey, ResolvedCommand},
|
||||
ExecutionContext,
|
||||
};
|
||||
use tauri_utils::acl::ExecutionContext;
|
||||
|
||||
fn main() {
|
||||
let mut context = tauri::generate_context!("../../examples/parent-window/tauri.conf.json");
|
||||
@ -16,16 +13,9 @@ fn main() {
|
||||
"plugin:event|listen",
|
||||
"plugin:webview|create_webview_window",
|
||||
] {
|
||||
context.resolved_acl().allowed_commands.insert(
|
||||
CommandKey {
|
||||
name: cmd.into(),
|
||||
context: ExecutionContext::Local,
|
||||
},
|
||||
ResolvedCommand {
|
||||
windows: vec!["*".parse().unwrap()],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
context
|
||||
.runtime_authority_mut()
|
||||
.__allow_command(cmd.to_string(), ExecutionContext::Local);
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
|
@ -19,7 +19,8 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"capabilities": ["default-plugins"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
Loading…
Reference in New Issue
Block a user