feat: update to tauri beta, add permissions (#862)

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
Tillmann
2024-02-04 03:14:41 +09:00
committed by GitHub
parent 506ce4835b
commit d198c01486
387 changed files with 21883 additions and 943 deletions
+17 -6
View File
@@ -6,7 +6,10 @@ use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
use encoding_rs::Encoding;
use serde::{Deserialize, Serialize};
use tauri::{ipc::Channel, Manager, Runtime, State, Window};
use tauri::{
ipc::{Channel, CommandScope, GlobalScope},
Manager, Runtime, State, Window,
};
use crate::{
open::Program,
@@ -91,6 +94,7 @@ fn default_env() -> Option<HashMap<String, String>> {
Some(HashMap::default())
}
#[allow(clippy::too_many_arguments)]
#[tauri::command]
pub fn execute<R: Runtime>(
window: Window<R>,
@@ -99,14 +103,23 @@ pub fn execute<R: Runtime>(
args: ExecuteArgs,
on_event: Channel,
options: CommandOptions,
command_scope: CommandScope<'_, crate::scope::ScopeAllowedCommand>,
global_scope: GlobalScope<'_, crate::scope::ScopeAllowedCommand>,
) -> crate::Result<ChildId> {
let scope = crate::scope::ShellScope {
scopes: command_scope
.allows()
.iter()
.chain(global_scope.allows())
.collect(),
};
let mut command = if options.sidecar {
let program = PathBuf::from(program);
let program_as_string = program.display().to_string();
let program_no_ext_as_string = program.with_extension("").display().to_string();
let configured_sidecar = window
.config()
.tauri
.bundle
.external_bin
.as_ref()
@@ -116,14 +129,12 @@ pub fn execute<R: Runtime>(
})
.cloned();
if let Some(sidecar) = configured_sidecar {
shell
.scope
.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)?
scope.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)?
} else {
return Err(crate::Error::SidecarNotAllowed(program));
}
} else {
match shell.scope.prepare(&program, args) {
match scope.prepare(&program, args) {
Ok(cmd) => cmd,
Err(e) => {
#[cfg(debug_assertions)]
+1 -114
View File
@@ -2,130 +2,17 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use serde::{de::Error as DeError, Deserialize, Deserializer};
use serde::Deserialize;
/// Configuration for the shell plugin.
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Config {
/// Access scope for the binary execution APIs.
/// Sidecars are automatically enabled.
#[serde(default)]
pub scope: ShellAllowlistScope,
/// Open URL with the user's default application.
#[serde(default)]
pub open: ShellAllowlistOpen,
}
/// A command allowed to be executed by the webview API.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ShellAllowedCommand {
/// The name for this allowed shell command configuration.
///
/// This name will be used inside of the webview API to call this command along with
/// any specified arguments.
pub name: String,
/// The command name.
/// It can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
// use default just so the schema doesn't flag it as required
pub command: PathBuf,
/// The allowed arguments for the command execution.
pub args: ShellAllowedArgs,
/// If this command is a sidecar command.
pub sidecar: bool,
}
impl<'de> Deserialize<'de> for ShellAllowedCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct InnerShellAllowedCommand {
name: String,
#[serde(rename = "cmd")]
command: Option<PathBuf>,
#[serde(default)]
args: ShellAllowedArgs,
#[serde(default)]
sidecar: bool,
}
let config = InnerShellAllowedCommand::deserialize(deserializer)?;
if !config.sidecar && config.command.is_none() {
return Err(DeError::custom(
"The shell scope `command` value is required.",
));
}
Ok(ShellAllowedCommand {
name: config.name,
command: config.command.unwrap_or_default(),
args: config.args,
sidecar: config.sidecar,
})
}
}
/// A set of command arguments allowed to be executed by the webview API.
///
/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
/// be passed to the attached command configuration.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArgs {
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
Flag(bool),
/// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
List(Vec<ShellAllowedArg>),
}
impl Default for ShellAllowedArgs {
fn default() -> Self {
Self::Flag(false)
}
}
/// A command argument allowed to be executed by the webview API.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArg {
/// A non-configurable argument that is passed to the command in the order it was specified.
Fixed(String),
/// A variable that is set while calling the command from the webview API.
///
Var {
/// [regex] validator to require passed values to conform to an expected input.
///
/// This will require the argument value passed to this variable to match the `validator` regex
/// before it will be executed.
///
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
validator: String,
},
}
/// Shell scope definition.
/// It is a list of command names and associated CLI arguments that restrict the API access from the webview.
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
/// Defines the `shell > open` api scope.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
+3
View File
@@ -24,6 +24,9 @@ pub enum Error {
ProgramNotAllowed(PathBuf),
#[error("unknown encoding {0}")]
UnknownEncoding(String),
/// JSON error.
#[error(transparent)]
Json(#[from] serde_json::Error),
}
impl Serialize for Error {
+12 -49
View File
@@ -20,7 +20,6 @@ use std::{
use process::{Command, CommandChild};
use regex::Regex;
use scope::{Scope, ScopeAllowedCommand, ScopeConfig};
use tauri::{
plugin::{Builder, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime,
@@ -32,8 +31,8 @@ mod error;
mod open;
pub mod process;
mod scope;
mod scope_entry;
use config::{Config, ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope};
pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
@@ -41,7 +40,7 @@ type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
pub struct Shell<R: Runtime> {
#[allow(dead_code)]
app: AppHandle<R>,
scope: Scope,
open_scope: scope::OpenScope,
children: ChildStore,
}
@@ -63,7 +62,7 @@ impl<R: Runtime> Shell<R> {
///
/// See [`crate::api::shell::open`] for how it handles security-related measures.
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(&self.scope, path.into(), with).map_err(Into::into)
open::open(&self.open_scope, path.into(), with).map_err(Into::into)
}
}
@@ -77,11 +76,11 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
let mut init_script = include_str!("init-iife.js").to_string();
init_script.push_str(include_str!("api-iife.js"));
Builder::<R, Option<Config>>::new("shell")
Builder::<R, Option<config::Config>>::new("shell")
.js_init_script(init_script)
.invoke_handler(tauri::generate_handler![
commands::execute,
@@ -90,12 +89,12 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
commands::open
])
.setup(|app, api| {
let default_config = Config::default();
let default_config = config::Config::default();
let config = api.config().as_ref().unwrap_or(&default_config);
app.manage(Shell {
app: app.clone(),
children: Default::default(),
scope: Scope::new(app, shell_scope(config.scope.clone(), &config.open)),
open_scope: open_scope(&config.open),
});
Ok(())
})
@@ -114,56 +113,20 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
.build()
}
fn shell_scope(scope: ShellAllowlistScope, open: &ShellAllowlistOpen) -> ScopeConfig {
let shell_scopes = get_allowed_clis(scope);
fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
let shell_scope_open = match open {
ShellAllowlistOpen::Flag(false) => None,
ShellAllowlistOpen::Flag(true) => {
config::ShellAllowlistOpen::Flag(false) => None,
config::ShellAllowlistOpen::Flag(true) => {
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
}
ShellAllowlistOpen::Validate(validator) => {
config::ShellAllowlistOpen::Validate(validator) => {
let validator =
Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
Some(validator)
}
};
ScopeConfig {
scope::OpenScope {
open: shell_scope_open,
scopes: shell_scopes,
}
}
fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap<String, ScopeAllowedCommand> {
scope
.0
.into_iter()
.map(|scope| {
let args = match scope.args {
ShellAllowedArgs::Flag(true) => None,
ShellAllowedArgs::Flag(false) => Some(Vec::new()),
ShellAllowedArgs::List(list) => {
let list = list.into_iter().map(|arg| match arg {
ShellAllowedArg::Fixed(fixed) => scope::ScopeAllowedArg::Fixed(fixed),
ShellAllowedArg::Var { validator } => {
let validator = Regex::new(&validator)
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
scope::ScopeAllowedArg::Var { validator }
}
});
Some(list.collect())
}
};
(
scope.name,
ScopeAllowedCommand {
command: scope.command,
args,
sidecar: scope.sidecar,
},
)
})
.collect()
}
+2 -2
View File
@@ -6,7 +6,7 @@
use serde::{Deserialize, Deserializer};
use crate::scope::Scope;
use crate::scope::OpenScope;
use std::str::FromStr;
/// Program to use on the [`open()`] call.
@@ -117,6 +117,6 @@ impl Program {
/// Ok(())
/// });
/// ```
pub fn open<P: AsRef<str>>(scope: &Scope, path: P, with: Option<Program>) -> crate::Result<()> {
pub fn open<P: AsRef<str>>(scope: &OpenScope, path: P, with: Option<Program>) -> crate::Result<()> {
scope.open(path.as_ref(), with).map_err(Into::into)
}
+82 -48
View File
@@ -4,11 +4,10 @@
use crate::open::Program;
use crate::process::Command;
use crate::{Manager, Runtime};
use regex::Regex;
use std::collections::HashMap;
use tauri::ipc::ScopeObject;
use tauri::Manager;
/// Allowed representation of `Execute` command arguments.
#[derive(Debug, Clone, serde::Deserialize)]
@@ -55,19 +54,12 @@ impl From<Vec<String>> for ExecuteArgs {
}
}
/// Shell scope configuration.
#[derive(Debug, Clone)]
pub struct ScopeConfig {
/// The validation regex that `shell > open` paths must match against.
pub open: Option<Regex>,
/// All allowed commands, using their unique command name as the keys.
pub scopes: HashMap<String, ScopeAllowedCommand>,
}
/// A configured scoped shell command.
#[derive(Debug, Clone)]
pub struct ScopeAllowedCommand {
/// Name of the command (key).
pub name: String,
/// The shell command to be called.
pub command: std::path::PathBuf,
@@ -78,6 +70,47 @@ pub struct ScopeAllowedCommand {
pub sidecar: bool,
}
impl ScopeObject for ScopeAllowedCommand {
type Error = crate::Error;
fn deserialize<R: tauri::Runtime>(
app: &tauri::AppHandle<R>,
raw: tauri::utils::acl::Value,
) -> Result<Self, Self::Error> {
let scope = serde_json::from_value::<crate::scope_entry::Entry>(raw.into())?;
let args = match scope.args.clone() {
crate::scope_entry::ShellAllowedArgs::Flag(true) => None,
crate::scope_entry::ShellAllowedArgs::Flag(false) => Some(Vec::new()),
crate::scope_entry::ShellAllowedArgs::List(list) => {
let list = list.into_iter().map(|arg| match arg {
crate::scope_entry::ShellAllowedArg::Fixed(fixed) => {
crate::scope::ScopeAllowedArg::Fixed(fixed)
}
crate::scope_entry::ShellAllowedArg::Var { validator } => {
let validator = Regex::new(&validator)
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
crate::scope::ScopeAllowedArg::Var { validator }
}
});
Some(list.collect())
}
};
let command = if let Ok(path) = app.path().parse(&scope.command) {
path
} else {
scope.command.clone()
};
Ok(Self {
name: scope.name,
command,
args,
sidecar: scope.sidecar,
})
}
}
/// A configured argument to a scoped shell command.
#[derive(Debug, Clone)]
pub enum ScopeAllowedArg {
@@ -98,9 +131,18 @@ impl ScopeAllowedArg {
}
}
/// Scope for filesystem access.
/// Scope for the open command
pub struct OpenScope {
/// The validation regex that `shell > open` paths must match against.
pub open: Option<Regex>,
}
/// Scope for shell process spawning.
#[derive(Clone)]
pub struct Scope(ScopeConfig);
pub struct ShellScope<'a> {
/// All allowed commands, using their unique command name as the keys.
pub scopes: Vec<&'a ScopeAllowedCommand>,
}
/// All errors that can happen while validating a scoped command.
#[derive(Debug, thiserror::Error)]
@@ -147,17 +189,33 @@ pub enum Error {
Io(#[from] std::io::Error),
}
impl Scope {
/// Creates a new shell scope.
pub(crate) fn new<R: Runtime, M: Manager<R>>(manager: &M, mut scope: ScopeConfig) -> Self {
for cmd in scope.scopes.values_mut() {
if let Ok(path) = manager.path().parse(&cmd.command) {
cmd.command = path;
impl OpenScope {
/// Open a path in the default (or specified) browser.
///
/// The path is validated against the `plugins > shell > open` validation regex, which
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
// ensure we pass validation if the configuration has one
if let Some(regex) = &self.open {
if !regex.is_match(path) {
return Err(Error::Validation {
index: 0,
validation: regex.as_str().into(),
});
}
}
Self(scope)
}
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
match with.map(Program::name) {
Some(program) => ::open::with_detached(path, program),
None => ::open::that_detached(path),
}
.map_err(Into::into)
}
}
impl<'a> ShellScope<'a> {
/// Validates argument inputs and creates a Tauri sidecar [`Command`].
pub fn prepare_sidecar(
&self,
@@ -180,7 +238,7 @@ impl Scope {
args: ExecuteArgs,
sidecar: Option<&str>,
) -> Result<Command, Error> {
let command = match self.0.scopes.get(command_name) {
let command = match self.scopes.iter().find(|s| s.name == command_name) {
Some(command) => command,
None => return Err(Error::NotFound(command_name.into())),
};
@@ -245,28 +303,4 @@ impl Scope {
Ok(command.args(args))
}
/// Open a path in the default (or specified) browser.
///
/// The path is validated against the `plugins > shell > open` validation regex, which
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
// ensure we pass validation if the configuration has one
if let Some(regex) = &self.0.open {
if !regex.is_match(path) {
return Err(Error::Validation {
index: 0,
validation: regex.as_str().into(),
});
}
}
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
match with.map(Program::name) {
Some(program) => ::open::with_detached(path, program),
None => ::open::that_detached(path),
}
.map_err(Into::into)
}
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{de::Error as DeError, Deserialize, Deserializer};
use std::path::PathBuf;
/// A command allowed to be executed by the webview API.
#[derive(Debug, Clone, PartialEq, Eq, Hash, schemars::JsonSchema)]
pub struct Entry {
/// The name for this allowed shell command configuration.
///
/// This name will be used inside of the webview API to call this command along with
/// any specified arguments.
pub name: String,
/// The command name.
/// It can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
// use default just so the schema doesn't flag it as required
pub command: PathBuf,
/// The allowed arguments for the command execution.
pub args: ShellAllowedArgs,
/// If this command is a sidecar command.
pub sidecar: bool,
}
impl<'de> Deserialize<'de> for Entry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct InnerEntry {
name: String,
#[serde(rename = "cmd")]
command: Option<PathBuf>,
#[serde(default)]
args: ShellAllowedArgs,
#[serde(default)]
sidecar: bool,
}
let config = InnerEntry::deserialize(deserializer)?;
if !config.sidecar && config.command.is_none() {
return Err(DeError::custom(
"The shell scope `command` value is required.",
));
}
Ok(Entry {
name: config.name,
command: config.command.unwrap_or_default(),
args: config.args,
sidecar: config.sidecar,
})
}
}
/// A set of command arguments allowed to be executed by the webview API.
///
/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
/// be passed to the attached command configuration.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArgs {
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
Flag(bool),
/// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
List(Vec<ShellAllowedArg>),
}
impl Default for ShellAllowedArgs {
fn default() -> Self {
Self::Flag(false)
}
}
/// A command argument allowed to be executed by the webview API.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArg {
/// A non-configurable argument that is passed to the command in the order it was specified.
Fixed(String),
/// A variable that is set while calling the command from the webview API.
///
Var {
/// [regex] validator to require passed values to conform to an expected input.
///
/// This will require the argument value passed to this variable to match the `validator` regex
/// before it will be executed.
///
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
validator: String,
},
}