mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-28 21:45:00 +03:00
feat(ipc): improve error message when plugin command is not allowed (#8681)
* feat(ipc): improve error message when plugin command is not allowed * reuse literal_struct macro
This commit is contained in:
parent
38b8e67237
commit
9af90ca7f3
@ -200,6 +200,10 @@ pub enum ExecutionContext {
|
||||
|
||||
#[cfg(feature = "build")]
|
||||
mod build_ {
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
@ -219,4 +223,57 @@ mod build_ {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Commands {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, str_lit);
|
||||
let deny = vec_lit(&self.deny, str_lit);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Scopes {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = opt_vec_lit(self.allow.as_ref(), identity);
|
||||
let deny = opt_vec_lit(self.deny.as_ref(), identity);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Permission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
let identifier = str_lit(&self.identifier);
|
||||
let description = opt_str_lit(self.description.as_ref());
|
||||
let commands = &self.commands;
|
||||
let scope = &self.scope;
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::Permission,
|
||||
version,
|
||||
identifier,
|
||||
description,
|
||||
commands,
|
||||
scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionSet {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let identifier = str_lit(&self.identifier);
|
||||
let description = str_lit(&self.description);
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::PermissionSet,
|
||||
identifier,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,6 @@ pub struct PermissionFile {
|
||||
#[serde(default)]
|
||||
pub set: Vec<PermissionSet>,
|
||||
|
||||
/// Test something!!
|
||||
pub test: Option<PermissionSet>,
|
||||
|
||||
/// A list of inlined permissions
|
||||
#[serde(default)]
|
||||
pub permission: Vec<Permission>,
|
||||
@ -110,3 +107,63 @@ impl Manifest {
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "build")]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for DefaultPermission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
let description = opt_str_lit(self.description.as_ref());
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::plugin::DefaultPermission,
|
||||
version,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Manifest {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default_permission = opt_lit(self.default_permission.as_ref());
|
||||
|
||||
let permissions = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permissions,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
let permission_sets = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permission_sets,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
let global_scope_schema =
|
||||
opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit));
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::plugin::Manifest,
|
||||
default_permission,
|
||||
permissions,
|
||||
permission_sets,
|
||||
global_scope_schema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, BTreeMap, HashSet},
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
@ -22,15 +23,37 @@ use super::{
|
||||
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
|
||||
pub type ScopeKey = usize;
|
||||
|
||||
/// Metadata for what referenced a [`ResolvedCommand`].
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct ResolvedCommandReference {
|
||||
/// Identifier of the capability.
|
||||
pub capability: String,
|
||||
/// Identifier of the permission.
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
/// A resolved command permission.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedCommand {
|
||||
/// The list of window label patterns that is allowed to run this command.
|
||||
/// The list of capability/permission that referenced this command.
|
||||
#[cfg(debug_assertions)]
|
||||
pub referenced_by: Vec<ResolvedCommandReference>,
|
||||
/// The list of window label patterns that was resolved for this command.
|
||||
pub windows: Vec<glob::Pattern>,
|
||||
/// The reference of the scope that is associated with this command. See [`Resolved#structfield.scopes`].
|
||||
pub scope: Option<ScopeKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ResolvedCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ResolvedCommand")
|
||||
.field("windows", &self.windows)
|
||||
.field("scope", &self.scope)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved scope. Merges all scopes defined for a single command.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResolvedScope {
|
||||
@ -51,8 +74,11 @@ pub struct CommandKey {
|
||||
}
|
||||
|
||||
/// Resolved access control list.
|
||||
#[derive(Debug)]
|
||||
#[derive(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`].
|
||||
@ -63,6 +89,17 @@ 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(
|
||||
@ -139,6 +176,8 @@ impl Resolved {
|
||||
format!("plugin:{plugin_name}|{allowed_command}"),
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission,
|
||||
);
|
||||
}
|
||||
|
||||
@ -148,6 +187,8 @@ impl Resolved {
|
||||
format!("plugin:{plugin_name}|{denied_command}"),
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -205,12 +246,16 @@ impl Resolved {
|
||||
.collect();
|
||||
|
||||
let resolved = Self {
|
||||
#[cfg(debug_assertions)]
|
||||
acl,
|
||||
allowed_commands: allowed_commands
|
||||
.into_iter()
|
||||
.map(|(key, cmd)| {
|
||||
Ok((
|
||||
key,
|
||||
ResolvedCommand {
|
||||
#[cfg(debug_assertions)]
|
||||
referenced_by: cmd.referenced_by,
|
||||
windows: parse_window_patterns(cmd.windows)?,
|
||||
scope: cmd.resolved_scope_key,
|
||||
},
|
||||
@ -223,6 +268,8 @@ impl Resolved {
|
||||
Ok((
|
||||
key,
|
||||
ResolvedCommand {
|
||||
#[cfg(debug_assertions)]
|
||||
referenced_by: cmd.referenced_by,
|
||||
windows: parse_window_patterns(cmd.windows)?,
|
||||
scope: cmd.resolved_scope_key,
|
||||
},
|
||||
@ -247,6 +294,8 @@ fn parse_window_patterns(windows: HashSet<String>) -> Result<Vec<glob::Pattern>,
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolvedCommandTemp {
|
||||
#[cfg(debug_assertions)]
|
||||
pub referenced_by: Vec<ResolvedCommandReference>,
|
||||
pub windows: HashSet<String>,
|
||||
pub scope: Vec<usize>,
|
||||
pub resolved_scope_key: Option<usize>,
|
||||
@ -257,6 +306,7 @@ fn resolve_command(
|
||||
command: String,
|
||||
capability: &Capability,
|
||||
scope_id: Option<usize>,
|
||||
#[cfg(debug_assertions)] permission: &Permission,
|
||||
) {
|
||||
let contexts = match &capability.context {
|
||||
CapabilityContext::Local => {
|
||||
@ -279,6 +329,12 @@ fn resolve_command(
|
||||
})
|
||||
.or_default();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
resolved.referenced_by.push(ResolvedCommandReference {
|
||||
capability: capability.identifier.clone(),
|
||||
permission: permission.identifier.clone(),
|
||||
});
|
||||
|
||||
resolved.windows.extend(capability.windows.clone());
|
||||
if let Some(id) = scope_id {
|
||||
resolved.scope.push(id);
|
||||
@ -347,37 +403,63 @@ mod build {
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::tokens::*;
|
||||
|
||||
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
|
||||
///
|
||||
/// All fields must represent a binding of the same name that implements `ToTokens`.
|
||||
macro_rules! literal_struct {
|
||||
($tokens:ident, $struct:ident, $($field:ident),+) => {
|
||||
$tokens.append_all(quote! {
|
||||
::tauri::utils::acl::resolved::$struct {
|
||||
$($field: #$field),+
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for CommandKey {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let name = str_lit(&self.name);
|
||||
let context = &self.context;
|
||||
literal_struct!(tokens, CommandKey, name, context)
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::CommandKey,
|
||||
name,
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl ToTokens for ResolvedCommandReference {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let capability = str_lit(&self.capability);
|
||||
let permission = str_lit(&self.permission);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommandReference,
|
||||
capability,
|
||||
permission
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ResolvedCommand {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
#[cfg(debug_assertions)]
|
||||
let referenced_by = vec_lit(&self.referenced_by, identity);
|
||||
|
||||
let windows = vec_lit(&self.windows, |window| {
|
||||
let w = window.as_str();
|
||||
quote!(#w.parse().unwrap())
|
||||
});
|
||||
let scope = opt_lit(self.scope.as_ref());
|
||||
literal_struct!(tokens, ResolvedCommand, windows, scope)
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
referenced_by,
|
||||
windows,
|
||||
scope
|
||||
)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
windows,
|
||||
scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,12 +467,25 @@ mod build {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, identity);
|
||||
let deny = vec_lit(&self.deny, identity);
|
||||
literal_struct!(tokens, ResolvedScope, allow, deny)
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedScope,
|
||||
allow,
|
||||
deny
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -419,9 +514,22 @@ 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,
|
||||
Resolved,
|
||||
::tauri::utils::acl::resolved::Resolved,
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
|
@ -2141,24 +2141,11 @@ fn default_build() -> BuildConfig {
|
||||
#[cfg(feature = "build")]
|
||||
mod build {
|
||||
use super::*;
|
||||
use crate::tokens::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::convert::identity;
|
||||
|
||||
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
|
||||
///
|
||||
/// All fields must represent a binding of the same name that implements `ToTokens`.
|
||||
macro_rules! literal_struct {
|
||||
($tokens:ident, $struct:ident, $($field:ident),+) => {
|
||||
$tokens.append_all(quote! {
|
||||
::tauri::utils::config::$struct {
|
||||
$($field: #$field),+
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
impl ToTokens for WebviewUrl {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::config::WebviewUrl };
|
||||
@ -2200,7 +2187,14 @@ mod build {
|
||||
let radius = opt_lit(self.radius.as_ref());
|
||||
let color = opt_lit(self.color.as_ref());
|
||||
|
||||
literal_struct!(tokens, WindowEffectsConfig, effects, state, radius, color)
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::config::WindowEffectsConfig,
|
||||
effects,
|
||||
state,
|
||||
radius,
|
||||
color
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2309,7 +2303,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
WindowConfig,
|
||||
::tauri::utils::config::WindowConfig,
|
||||
label,
|
||||
url,
|
||||
user_agent,
|
||||
@ -2415,7 +2409,13 @@ mod build {
|
||||
let pubkey = str_lit(&self.pubkey);
|
||||
let windows = &self.windows;
|
||||
|
||||
literal_struct!(tokens, UpdaterConfig, active, pubkey, windows);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::config::UpdaterConfig,
|
||||
active,
|
||||
pubkey,
|
||||
windows
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2437,7 +2437,7 @@ mod build {
|
||||
let rpm = quote!(Default::default());
|
||||
let dmg = quote!(Default::default());
|
||||
let macos = quote!(Default::default());
|
||||
let external_bin = opt_vec_str_lit(self.external_bin.as_ref());
|
||||
let external_bin = opt_vec_lit(self.external_bin.as_ref(), str_lit);
|
||||
let windows = &self.windows;
|
||||
let ios = quote!(Default::default());
|
||||
let android = quote!(Default::default());
|
||||
@ -2445,7 +2445,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
BundleConfig,
|
||||
::tauri::utils::config::BundleConfig,
|
||||
active,
|
||||
identifier,
|
||||
publisher,
|
||||
@ -2500,7 +2500,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
BuildConfig,
|
||||
::tauri::utils::config::BuildConfig,
|
||||
runner,
|
||||
dev_path,
|
||||
dist_dir,
|
||||
@ -2528,7 +2528,11 @@ mod build {
|
||||
impl ToTokens for UpdaterWindowsConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let install_mode = &self.install_mode;
|
||||
literal_struct!(tokens, UpdaterWindowsConfig, install_mode);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::config::UpdaterWindowsConfig,
|
||||
install_mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2596,7 +2600,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
RemoteDomainAccessScope,
|
||||
::tauri::utils::config::RemoteDomainAccessScope,
|
||||
scheme,
|
||||
domain,
|
||||
windows,
|
||||
@ -2615,7 +2619,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
SecurityConfig,
|
||||
::tauri::utils::config::SecurityConfig,
|
||||
csp,
|
||||
dev_csp,
|
||||
freeze_prototype,
|
||||
@ -2635,7 +2639,7 @@ mod build {
|
||||
let tooltip = opt_str_lit(self.tooltip.as_ref());
|
||||
literal_struct!(
|
||||
tokens,
|
||||
TrayIconConfig,
|
||||
::tauri::utils::config::TrayIconConfig,
|
||||
id,
|
||||
icon_path,
|
||||
icon_as_template,
|
||||
@ -2683,7 +2687,7 @@ mod build {
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
TauriConfig,
|
||||
::tauri::utils::config::TauriConfig,
|
||||
pattern,
|
||||
windows,
|
||||
bundle,
|
||||
@ -2711,7 +2715,12 @@ mod build {
|
||||
let product_name = opt_str_lit(self.product_name.as_ref());
|
||||
let version = opt_str_lit(self.version.as_ref());
|
||||
|
||||
literal_struct!(tokens, PackageConfig, product_name, version);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::config::PackageConfig,
|
||||
product_name,
|
||||
version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2723,7 +2732,15 @@ mod build {
|
||||
let build = &self.build;
|
||||
let plugins = &self.plugins;
|
||||
|
||||
literal_struct!(tokens, Config, schema, package, tauri, build, plugins);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::config::Config,
|
||||
schema,
|
||||
package,
|
||||
tauri,
|
||||
build,
|
||||
plugins
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,20 @@ use quote::{quote, ToTokens};
|
||||
use serde_json::Value as JsonValue;
|
||||
use url::Url;
|
||||
|
||||
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
|
||||
///
|
||||
/// All fields must represent a binding of the same name that implements `ToTokens`.
|
||||
#[macro_export]
|
||||
macro_rules! literal_struct {
|
||||
($tokens:ident, $struct:path, $($field:ident),+) => {
|
||||
$tokens.append_all(quote! {
|
||||
$struct {
|
||||
$($field: #$field),+
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a `String` constructor `TokenStream`.
|
||||
///
|
||||
/// e.g. `"Hello World" -> String::from("Hello World").
|
||||
@ -28,14 +42,28 @@ pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type.
|
||||
pub fn opt_lit_owned(item: Option<impl ToTokens>) -> TokenStream {
|
||||
match item {
|
||||
None => quote! { ::core::option::Option::None },
|
||||
Some(item) => quote! { ::core::option::Option::Some(#item) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with `str_lit`.
|
||||
pub fn opt_str_lit(item: Option<impl AsRef<str>>) -> TokenStream {
|
||||
opt_lit(item.map(str_lit).as_ref())
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with a list of `str_lit`
|
||||
pub fn opt_vec_str_lit(item: Option<impl IntoIterator<Item = impl AsRef<str>>>) -> TokenStream {
|
||||
opt_lit(item.map(|list| vec_lit(list, str_lit)).as_ref())
|
||||
pub fn opt_vec_lit<Raw, Tokens>(
|
||||
item: Option<impl IntoIterator<Item = Raw>>,
|
||||
map: impl Fn(Raw) -> Tokens,
|
||||
) -> TokenStream
|
||||
where
|
||||
Tokens: ToTokens,
|
||||
{
|
||||
opt_lit(item.map(|list| vec_lit(list, map)).as_ref())
|
||||
}
|
||||
|
||||
/// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s.
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{collections::BTreeMap, ops::Deref};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
@ -21,6 +21,8 @@ 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>,
|
||||
pub(crate) scope_manager: ScopeManager,
|
||||
@ -37,6 +39,15 @@ pub enum Origin {
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for Origin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Local => write!(f, "local"),
|
||||
Self::Remote { domain } => write!(f, "remote: {domain}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
fn matches(&self, context: &ExecutionContext) -> bool {
|
||||
match (self, context) {
|
||||
@ -53,30 +64,160 @@ impl Origin {
|
||||
}
|
||||
|
||||
impl RuntimeAuthority {
|
||||
pub(crate) fn new(acl: Resolved) -> Self {
|
||||
let command_cache = acl
|
||||
pub(crate) fn new(resolved_acl: Resolved) -> Self {
|
||||
let command_cache = resolved_acl
|
||||
.command_scope
|
||||
.keys()
|
||||
.map(|key| (*key, <TypeMap![Send + Sync]>::new()))
|
||||
.collect();
|
||||
Self {
|
||||
allowed_commands: acl.allowed_commands,
|
||||
denied_commands: acl.denied_commands,
|
||||
#[cfg(debug_assertions)]
|
||||
acl: resolved_acl.acl,
|
||||
allowed_commands: resolved_acl.allowed_commands,
|
||||
denied_commands: resolved_acl.denied_commands,
|
||||
scope_manager: ScopeManager {
|
||||
command_scope: acl.command_scope,
|
||||
global_scope: acl.global_scope,
|
||||
command_scope: resolved_acl.command_scope,
|
||||
global_scope: resolved_acl.global_scope,
|
||||
command_cache,
|
||||
global_scope_cache: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn resolve_access_message(
|
||||
&self,
|
||||
plugin: &str,
|
||||
command_name: &str,
|
||||
window: &str,
|
||||
origin: &Origin,
|
||||
) -> String {
|
||||
fn print_references(resolved: &ResolvedCommand) -> String {
|
||||
resolved
|
||||
.referenced_by
|
||||
.iter()
|
||||
.map(|r| format!("capability: {}, permission: {}", r.capability, r.permission))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" || ")
|
||||
}
|
||||
|
||||
fn has_permissions_allowing_command(
|
||||
manifest: &crate::utils::acl::plugin::Manifest,
|
||||
set: &crate::utils::acl::PermissionSet,
|
||||
command: &str,
|
||||
) -> bool {
|
||||
for permission_id in &set.permissions {
|
||||
if permission_id == "default" {
|
||||
if let Some(default) = &manifest.default_permission {
|
||||
if has_permissions_allowing_command(manifest, default, command) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if let Some(ref_set) = manifest.permission_sets.get(permission_id) {
|
||||
if has_permissions_allowing_command(manifest, ref_set, command) {
|
||||
return true;
|
||||
}
|
||||
} else if let Some(permission) = manifest.permissions.get(permission_id) {
|
||||
if permission.commands.allow.contains(&command.into()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
let command = format!("plugin:{plugin}|{command_name}");
|
||||
if let Some((_cmd, resolved)) = self
|
||||
.denied_commands
|
||||
.iter()
|
||||
.find(|(cmd, _)| cmd.name == command && origin.matches(&cmd.context))
|
||||
{
|
||||
format!(
|
||||
"{plugin}.{command_name} denied on origin {origin}, referenced by: {}",
|
||||
print_references(resolved)
|
||||
)
|
||||
} else {
|
||||
let command_matches = self
|
||||
.allowed_commands
|
||||
.iter()
|
||||
.filter(|(cmd, _)| cmd.name == command)
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
if let Some((_cmd, resolved)) = command_matches
|
||||
.iter()
|
||||
.find(|(cmd, _)| origin.matches(&cmd.context))
|
||||
{
|
||||
if resolved.windows.iter().any(|w| w.matches(window)) {
|
||||
"allowed".to_string()
|
||||
} else {
|
||||
format!("{plugin}.{command_name} not allowed on window {window}, expected one of {}, referenced by {}", resolved.windows.iter().map(|w| w.as_str()).collect::<Vec<_>>().join(", "), print_references(resolved))
|
||||
}
|
||||
} else {
|
||||
let permission_error_detail = if let Some(manifest) = self.acl.get(plugin) {
|
||||
let mut permissions_referencing_command = Vec::new();
|
||||
|
||||
if let Some(default) = &manifest.default_permission {
|
||||
if has_permissions_allowing_command(manifest, default, command_name) {
|
||||
permissions_referencing_command.push("default".into());
|
||||
}
|
||||
}
|
||||
for set in manifest.permission_sets.values() {
|
||||
if has_permissions_allowing_command(manifest, set, command_name) {
|
||||
permissions_referencing_command.push(set.identifier.clone());
|
||||
}
|
||||
}
|
||||
for permission in manifest.permissions.values() {
|
||||
if permission.commands.allow.contains(&command_name.into()) {
|
||||
permissions_referencing_command.push(permission.identifier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
permissions_referencing_command.sort();
|
||||
|
||||
format!(
|
||||
"Permissions associated with this command: {}",
|
||||
permissions_referencing_command
|
||||
.iter()
|
||||
.map(|p| format!("{plugin}:{p}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
} else {
|
||||
"Plugin did not define its manifest".to_string()
|
||||
};
|
||||
|
||||
if command_matches.is_empty() {
|
||||
format!("{plugin}.{command_name} not allowed. {permission_error_detail}")
|
||||
} else {
|
||||
format!(
|
||||
"{plugin}.{command_name} not allowed on origin [{}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}",
|
||||
origin,
|
||||
command_matches
|
||||
.iter()
|
||||
.map(|(cmd, resolved)| {
|
||||
let context = match &cmd.context {
|
||||
ExecutionContext::Local => "[local]".to_string(),
|
||||
ExecutionContext::Remote { domain } => format!("[remote: {}]", domain.as_str()),
|
||||
};
|
||||
format!(
|
||||
"- context: {context}, referenced by: {}",
|
||||
print_references(resolved)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the given IPC execution is allowed and returns the [`ResolvedCommand`] if it is.
|
||||
pub fn resolve_access(
|
||||
&self,
|
||||
command: &str,
|
||||
window: &str,
|
||||
origin: Origin,
|
||||
origin: &Origin,
|
||||
) -> Option<&ResolvedCommand> {
|
||||
if self
|
||||
.denied_commands
|
||||
@ -89,8 +230,8 @@ impl RuntimeAuthority {
|
||||
.allowed_commands
|
||||
.iter()
|
||||
.find(|(cmd, _)| cmd.name == command && origin.matches(&cmd.context))
|
||||
.map(|(_cmd, allowed)| allowed)
|
||||
.filter(|allowed| allowed.windows.iter().any(|w| w.matches(window)))
|
||||
.map(|(_cmd, resolved)| resolved)
|
||||
.filter(|resolved| resolved.windows.iter().any(|w| w.matches(window)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -329,7 +470,7 @@ mod tests {
|
||||
|
||||
let resolved_cmd = ResolvedCommand {
|
||||
windows: vec![Pattern::new(window).unwrap()],
|
||||
scope: None,
|
||||
..Default::default()
|
||||
};
|
||||
let allowed_commands = [(command.clone(), resolved_cmd.clone())]
|
||||
.into_iter()
|
||||
@ -337,16 +478,14 @@ mod tests {
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
allowed_commands,
|
||||
denied_commands: Default::default(),
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
&command.name,
|
||||
&window.replace('*', "something"),
|
||||
Origin::Local
|
||||
&Origin::Local
|
||||
),
|
||||
Some(&resolved_cmd)
|
||||
);
|
||||
@ -366,6 +505,7 @@ mod tests {
|
||||
let resolved_cmd = ResolvedCommand {
|
||||
windows: vec![Pattern::new(window).unwrap()],
|
||||
scope: None,
|
||||
..Default::default()
|
||||
};
|
||||
let allowed_commands = [(command.clone(), resolved_cmd.clone())]
|
||||
.into_iter()
|
||||
@ -373,16 +513,14 @@ mod tests {
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
allowed_commands,
|
||||
denied_commands: Default::default(),
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
&command.name,
|
||||
window,
|
||||
Origin::Remote {
|
||||
&Origin::Remote {
|
||||
domain: domain.into()
|
||||
}
|
||||
),
|
||||
@ -404,6 +542,7 @@ mod tests {
|
||||
let resolved_cmd = ResolvedCommand {
|
||||
windows: vec![Pattern::new(window).unwrap()],
|
||||
scope: None,
|
||||
..Default::default()
|
||||
};
|
||||
let allowed_commands = [(command.clone(), resolved_cmd.clone())]
|
||||
.into_iter()
|
||||
@ -411,16 +550,14 @@ mod tests {
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
allowed_commands,
|
||||
denied_commands: Default::default(),
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
authority.resolve_access(
|
||||
&command.name,
|
||||
window,
|
||||
Origin::Remote {
|
||||
&Origin::Remote {
|
||||
domain: domain.replace('*', "studio")
|
||||
}
|
||||
),
|
||||
@ -439,6 +576,7 @@ mod tests {
|
||||
let resolved_cmd = ResolvedCommand {
|
||||
windows: vec![Pattern::new(window).unwrap()],
|
||||
scope: None,
|
||||
..Default::default()
|
||||
};
|
||||
let allowed_commands = [(command.clone(), resolved_cmd.clone())]
|
||||
.into_iter()
|
||||
@ -446,16 +584,14 @@ mod tests {
|
||||
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
allowed_commands,
|
||||
denied_commands: Default::default(),
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(authority
|
||||
.resolve_access(
|
||||
&command.name,
|
||||
window,
|
||||
Origin::Remote {
|
||||
&Origin::Remote {
|
||||
domain: "tauri.app".into()
|
||||
}
|
||||
)
|
||||
@ -474,7 +610,7 @@ mod tests {
|
||||
command.clone(),
|
||||
ResolvedCommand {
|
||||
windows: windows.clone(),
|
||||
scope: None,
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
@ -483,7 +619,7 @@ mod tests {
|
||||
command.clone(),
|
||||
ResolvedCommand {
|
||||
windows: windows.clone(),
|
||||
scope: None,
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
@ -492,12 +628,11 @@ mod tests {
|
||||
let authority = RuntimeAuthority::new(Resolved {
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope: Default::default(),
|
||||
global_scope: Default::default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(authority
|
||||
.resolve_access(&command.name, window, Origin::Local)
|
||||
.resolve_access(&command.name, window, &Origin::Local)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +125,8 @@ 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(),
|
||||
|
@ -1093,22 +1093,19 @@ fn main() {
|
||||
request.headers,
|
||||
);
|
||||
|
||||
let acl_origin = if is_local {
|
||||
Origin::Local
|
||||
} else {
|
||||
Origin::Remote {
|
||||
domain: current_url
|
||||
.domain()
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
};
|
||||
let resolved_acl = manager
|
||||
.runtime_authority
|
||||
.resolve_access(
|
||||
&request.cmd,
|
||||
&message.webview.webview.label,
|
||||
if is_local {
|
||||
Origin::Local
|
||||
} else {
|
||||
Origin::Remote {
|
||||
domain: current_url
|
||||
.domain()
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
},
|
||||
)
|
||||
.resolve_access(&request.cmd, &message.webview.webview.label, &acl_origin)
|
||||
.cloned();
|
||||
|
||||
let mut invoke = Invoke {
|
||||
@ -1117,20 +1114,33 @@ fn main() {
|
||||
acl: resolved_acl,
|
||||
};
|
||||
|
||||
if request.cmd.starts_with("plugin:") {
|
||||
if let Some((plugin, command_name)) = request.cmd.strip_prefix("plugin:").map(|raw_command| {
|
||||
let mut tokens = raw_command.split('|');
|
||||
// safe to unwrap: split always has a least one item
|
||||
let plugin = tokens.next().unwrap();
|
||||
let command = tokens.next().map(|c| c.to_string()).unwrap_or_default();
|
||||
(plugin, command)
|
||||
}) {
|
||||
if request.cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND && invoke.acl.is_none() {
|
||||
invoke.resolver.reject("NOT ALLOWED");
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
invoke
|
||||
.resolver
|
||||
.reject(manager.runtime_authority.resolve_access_message(
|
||||
plugin,
|
||||
&command_name,
|
||||
&invoke.message.webview.webview.label,
|
||||
&acl_origin,
|
||||
));
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
invoke
|
||||
.resolver
|
||||
.reject(format!("Command {} not allowed by ACL", request.cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
let command = invoke.message.command.replace("plugin:", "");
|
||||
let mut tokens = command.split('|');
|
||||
// safe to unwrap: split always has a least one item
|
||||
let plugin = tokens.next().unwrap();
|
||||
invoke.message.command = tokens
|
||||
.next()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
invoke.message.command = command_name;
|
||||
|
||||
let command = invoke.message.command.clone();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user