From 079386cd806a942883f520db92920e4c4ab127a9 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:30:56 +0200 Subject: [PATCH] Rework permissions. Better validation. Remove ffi and high_resolution_time --- Cargo.lock | 7 + dev_plugin/gauntlet.toml | 10 +- rust/server/Cargo.toml | 1 + rust/server/src/plugins/data_db_repository.rs | 27 +- rust/server/src/plugins/js/clipboard.rs | 13 +- rust/server/src/plugins/js/mod.rs | 70 ++--- rust/server/src/plugins/js/permissions.rs | 140 ++++++++++ rust/server/src/plugins/loader.rs | 242 ++++++++++++++---- rust/server/src/plugins/mod.rs | 24 +- tools | 2 +- 10 files changed, 404 insertions(+), 132 deletions(-) create mode 100644 rust/server/src/plugins/js/permissions.rs diff --git a/Cargo.lock b/Cargo.lock index 8d0d889..1c21a2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7762,6 +7762,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", + "typed-path", "ureq", "utils", "uuid", @@ -9681,6 +9682,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +[[package]] +name = "typed-path" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c0c7479c430935701ff2532e3091e6680ec03f2f89ffcd9988b08e885b90a5" + [[package]] name = "typenum" version = "1.17.0" diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 9511ee9..73bdea1 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -157,4 +157,12 @@ environment = ["RUST_LOG"] system = ["systemMemoryInfo"] network = ["upload.wikimedia.org", "api.github.com"] clipboard = ["read", "write", "clear"] -main_search_bar = ["read"] \ No newline at end of file +main_search_bar = ["read"] + +[permissions.filesystem] +read = ["C:\\ProgramFiles", "C:/ProgramFiles", "/home/exidex"] +write = ["/home/exidex/.test"] + +[permissions.exec] +command = ["ls"] +executable = ["/usr/bin/ls"] \ No newline at end of file diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index ba60e20..e158b8f 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -38,6 +38,7 @@ arboard = "3.4.0" global-hotkey = "0.4.2" ureq = "2.10.0" bytes = "1.6.0" +typed-path = "0.9" scenario_runner = { path = "../scenario_runner", optional = true } itertools = "0.10.5" diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 187412f..b92baa0 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -10,6 +10,7 @@ use sqlx::{Error, Executor, Pool, Row, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; +use typed_path::TypedPathBuf; use uuid::Uuid; use common::model::{PhysicalKey, PhysicalShortcut}; use common::dirs::Dirs; @@ -117,17 +118,11 @@ pub struct DbPluginPermissions { #[serde(default)] pub environment: Vec, #[serde(default)] - pub high_resolution_time: bool, - #[serde(default)] pub network: Vec, #[serde(default)] - pub ffi: Vec, - #[serde(default)] - pub fs_read_access: Vec, - #[serde(default)] - pub fs_write_access: Vec, + pub filesystem: DbPluginPermissionsFileSystem, #[serde(default)] - pub run_subprocess: Vec, + pub exec: DbPluginPermissionsExec, #[serde(default)] pub system: Vec, #[serde(default)] @@ -136,6 +131,22 @@ pub struct DbPluginPermissions { pub main_search_bar: Vec, } +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct DbPluginPermissionsFileSystem { + #[serde(default)] + pub read: Vec, + #[serde(default)] + pub write: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct DbPluginPermissionsExec { + #[serde(default)] + pub command: Vec, + #[serde(default)] + pub executable: Vec, +} + #[derive(Debug, Deserialize, Serialize)] pub enum DbPluginClipboardPermissions { #[serde(rename = "read")] diff --git a/rust/server/src/plugins/js/clipboard.rs b/rust/server/src/plugins/js/clipboard.rs index a013193..350f31c 100644 --- a/rust/server/src/plugins/js/clipboard.rs +++ b/rust/server/src/plugins/js/clipboard.rs @@ -7,7 +7,8 @@ use deno_core::{op, OpState}; use image::RgbaImage; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; -use crate::plugins::js::{PluginClipboardPermissions, PluginData}; +use crate::plugins::js::permissions::PluginPermissionsClipboard; +use crate::plugins::js::PluginData; fn unknown_err_clipboard(err: arboard::Error) -> anyhow::Error { anyhow!("UNKNOWN_ERROR: {:?}", err) @@ -36,7 +37,7 @@ async fn clipboard_read(state: Rc>) -> anyhow::Result() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Read); + .contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -98,7 +99,7 @@ async fn clipboard_read_text(state: Rc>) -> anyhow::Result() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Read); + .contains(&PluginPermissionsClipboard::Read); if !allow { return Err(anyhow!("Plugin doesn't have 'read' permission for clipboard")); @@ -134,7 +135,7 @@ async fn clipboard_write(state: Rc>, data: ClipboardData) -> an .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Write); + .contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -186,7 +187,7 @@ async fn clipboard_write_text(state: Rc>, data: String) -> anyh .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Write); + .contains(&PluginPermissionsClipboard::Write); if !allow { return Err(anyhow!("Plugin doesn't have 'write' permission for clipboard")); @@ -213,7 +214,7 @@ async fn clipboard_clear(state: Rc>) -> anyhow::Result<()> { .borrow::() .permissions() .clipboard - .contains(&PluginClipboardPermissions::Clear); + .contains(&PluginPermissionsClipboard::Clear); if !allow { return Err(anyhow!("Plugin doesn't have 'clear' permission for clipboard")); diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 6bf4b0b..d0b8757 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -1,20 +1,23 @@ use std::cell::RefCell; use std::collections::HashMap; use std::fs::File; +use std::hash::Hash; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::rc::Rc; +use std::str::FromStr; use std::time::Duration; use anyhow::{anyhow, Context}; -use deno_core::{FastString, futures, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, op, OpState, ResolutionKind, serde_v8, StaticModuleLoader, v8}; -use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::futures::executor::block_on; +use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter}; +use deno_core::{futures, op, serde_v8, v8, FastString, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, OpState, ResolutionKind, StaticModuleLoader}; +use deno_runtime::BootstrapOptions; use deno_runtime::deno_core::ModuleSpecifier; use deno_runtime::deno_io::{Stdio, StdioPipe}; -use deno_runtime::permissions::{Permissions, PermissionsContainer, PermissionsOptions}; +use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, PermissionsOptions, ReadDescriptor, UnaryPermission, WriteDescriptor}; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use indexmap::IndexMap; @@ -27,16 +30,17 @@ use tokio_util::sync::CancellationToken; use common::dirs::Dirs; use common::model::{EntrypointId, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; -use component_model::{Children, Component, create_component_model, Property, PropertyType, SharedType}; +use component_model::{create_component_model, Children, Component, Property, PropertyType, SharedType}; use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; -use crate::plugins::applications::{DesktopEntry, get_apps}; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint, DbPluginClipboardPermissions}; +use crate::plugins::applications::{get_apps, DesktopEntry}; +use crate::plugins::data_db_repository::{db_entrypoint_from_str, DataDbRepository, DbPluginClipboardPermissions, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::plugins::js::permissions::{permissions_to_deno, PluginPermissions, PluginPermissionsClipboard}; use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::plugins::numbat::{run_numbat, NumbatContext}; use crate::plugins::js::plugins::settings::open_settings; @@ -54,6 +58,7 @@ mod preferences; mod search; mod command_generators; mod clipboard; +pub mod permissions; pub struct PluginRuntimeData { pub id: PluginId, @@ -73,37 +78,11 @@ pub struct PluginCode { pub js: HashMap, } -pub struct PluginPermissions { - pub environment: Vec, - pub high_resolution_time: bool, - pub network: Vec, - pub ffi: Vec, - pub fs_read_access: Vec, - pub fs_write_access: Vec, - pub run_subprocess: Vec, - pub system: Vec, - pub clipboard: Vec, - pub main_search_bar: Vec, -} - #[derive(Clone, Debug)] pub struct PluginRuntimePermissions { - pub clipboard: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PluginClipboardPermissions { - Read, - Write, - Clear + pub clipboard: Vec, } -#[derive(Clone, Debug)] -pub enum PluginMainSearchBarPermissions { - Read, -} - - #[derive(Clone, Debug)] pub enum PluginCommand { One { @@ -306,25 +285,6 @@ async fn start_js_runtime( icon_cache: IconCache, dirs: Dirs, ) -> anyhow::Result<()> { - let permissions_container = PermissionsContainer::new(Permissions::from_options(&PermissionsOptions { - allow_env: if permissions.environment.is_empty() { None } else { Some(permissions.environment) }, - deny_env: None, - allow_hrtime: permissions.high_resolution_time, - deny_hrtime: false, - allow_net: if permissions.network.is_empty() { None } else { Some(permissions.network) }, - deny_net: None, - allow_ffi: if permissions.ffi.is_empty() { None } else { Some(permissions.ffi) }, - deny_ffi: None, - allow_read: if permissions.fs_read_access.is_empty() { None } else { Some(permissions.fs_read_access) }, - deny_read: None, - allow_run: if permissions.run_subprocess.is_empty() { None } else { Some(permissions.run_subprocess) }, - deny_run: None, - allow_sys: if permissions.system.is_empty() { None } else { Some(permissions.system) }, - deny_sys: None, - allow_write: if permissions.fs_write_access.is_empty() { None } else { Some(permissions.fs_write_access) }, - deny_write: None, - prompt: false, - })?); let dev_plugin = plugin_id.to_string().starts_with("file://"); @@ -360,6 +320,8 @@ async fn start_js_runtime( None }; + let permissions_container = permissions_to_deno(&permissions); + let runtime_permissions = PluginRuntimePermissions { clipboard: permissions.clipboard, }; @@ -368,6 +330,10 @@ async fn start_js_runtime( unused_url, permissions_container, WorkerOptions { + bootstrap: BootstrapOptions { + is_tty: false, + ..Default::default() + }, module_loader: Rc::new(CustomModuleLoader::new(code, dev_plugin)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), diff --git a/rust/server/src/plugins/js/permissions.rs b/rust/server/src/plugins/js/permissions.rs new file mode 100644 index 0000000..565713d --- /dev/null +++ b/rust/server/src/plugins/js/permissions.rs @@ -0,0 +1,140 @@ +use deno_runtime::permissions::{Descriptor, EnvDescriptor, NetDescriptor, Permissions, PermissionsContainer, ReadDescriptor, RunDescriptor, SysDescriptor, UnaryPermission, WriteDescriptor}; +use std::collections::HashSet; +use std::hash::Hash; +use std::path::PathBuf; +use std::str::FromStr; + +pub struct PluginPermissions { + pub environment: Vec, + pub network: Vec, + pub filesystem: PluginPermissionsFileSystem, + pub exec: PluginPermissionsExec, + pub system: Vec, + pub clipboard: Vec, + pub main_search_bar: Vec, +} + +pub struct PluginPermissionsFileSystem { + pub read: Vec, + pub write: Vec, +} + +pub struct PluginPermissionsExec { + pub command: Vec, + pub executable: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PluginPermissionsClipboard { + Read, + Write, + Clear +} + +#[derive(Clone, Debug)] +pub enum PluginPermissionsMainSearchBar { + Read, +} + +pub fn permissions_to_deno(permissions: &PluginPermissions) -> PermissionsContainer { + PermissionsContainer::new(Permissions { + read: path_permission(&permissions.filesystem.read, ReadDescriptor), + write: path_permission(&permissions.filesystem.write, WriteDescriptor), + net: net_permission(&permissions.network), + env: env_permission(&permissions.environment), + sys: sys_permission(&permissions.system), + run: run_permission(&permissions.exec), + ffi: Permissions::new_ffi(&None, &None, false).expect("new_ffi should always succeed"), + hrtime: Permissions::new_hrtime(true, false), + }) +} + +fn path_permission( + paths: &[String], + to_permission: fn(PathBuf) -> T +) -> UnaryPermission { + let granted = paths + .into_iter() + .map(|path| to_permission(PathBuf::from(path))) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn net_permission(domain_and_ports: &[String]) -> UnaryPermission { + let granted = domain_and_ports + .into_iter() + .map(|domain_and_port| { + NetDescriptor::from_str(&domain_and_port) + .expect("should be validated when loading") + }) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn env_permission(envs: &[String]) -> UnaryPermission { + let granted = envs + .into_iter() + .map(|env| EnvDescriptor::new(env)) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn sys_permission(system: &[String]) -> UnaryPermission { + let granted = system + .into_iter() + .map(|system| SysDescriptor(system.to_owned())) + .collect(); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} + +fn run_permission(permissions: &PluginPermissionsExec) -> UnaryPermission { + let granted_executable = permissions.executable + .iter() + .map(|path| RunDescriptor::Path(PathBuf::from(path))) + .collect::>(); + + let granted_command = permissions.command + .iter() + .map(|cmd| RunDescriptor::Name(cmd.to_owned())) + .collect::>(); + + let mut granted = HashSet::new(); + granted.extend(granted_executable); + granted.extend(granted_command); + + UnaryPermission { + prompt: false, + granted_global: false, + flag_denied_global: false, + granted_list: granted, + ..Default::default() + } +} diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index 81fbe7f..a727537 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -6,16 +6,19 @@ use std::path::{Path, PathBuf}; use std::thread; use anyhow::{anyhow, Context}; +use deno_core::url; use include_dir::Dir; use serde::{Deserialize, Serialize}; use uuid::Uuid; use walkdir::WalkDir; use itertools::Itertools; use tracing_subscriber::fmt::format; +use typed_path::{TypedPathBuf, Utf8TypedPath, Utf8UnixComponent, Utf8WindowsComponent}; use common::model::{DownloadStatus, PluginId}; use crate::model::ActionShortcutKey; -use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; +use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_to_str, db_plugin_type_to_str, DbCode, DbPluginAction, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPermissions, DbPluginPreference, DbPluginPreferenceUserData, DbPluginType, DbPreferenceEnumValue, DbWritePlugin, DbWritePluginAssetData, DbWritePluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions, DbPluginPermissionsFileSystem, DbPluginPermissionsExec}; use crate::plugins::download_status::DownloadStatusHolder; +use crate::plugins::js::permissions::{PluginPermissionsExec, PluginPermissionsFileSystem}; pub struct PluginLoader { db_repository: DataDbRepository, @@ -213,46 +216,8 @@ impl PluginLoader { tracing::debug!("Plugin config read: {:?}", plugin_manifest); - let permissions = &plugin_manifest.permissions; - - let env_exists = !permissions.environment.is_empty(); - let ffi_exists = !permissions.ffi.is_empty(); - let fs_read_exists = !permissions.fs_read_access.is_empty(); - let fs_write_exists = !permissions.fs_write_access.is_empty(); - let run_exists = !permissions.run_subprocess.is_empty(); - let system_exists = !permissions.system.is_empty(); - - let os_required = env_exists || ffi_exists || fs_read_exists || fs_write_exists || run_exists || system_exists; - - if os_required { - let current_system = if cfg!(target_os = "linux") { - PluginManifestSupportedSystem::Linux - } else if cfg!(target_os = "macos") { - PluginManifestSupportedSystem::MacOS - } else if cfg!(target_os = "windows") { - PluginManifestSupportedSystem::Windows - } else { - panic!("OS not supported") - }; - - let supported_system = &plugin_manifest.supported_system; - if !supported_system.contains(¤t_system) { - let supported_system = supported_system.iter().format(", "); - return Err(anyhow!("Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", supported_system)) - } - } - - let has_inline_view = plugin_manifest.entrypoint - .iter() - .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) - .is_some(); - - if has_inline_view { - let main_search_bar = &permissions.main_search_bar; - if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { - return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) - } - } + // todo path permissions variables + Self::validate_manifest(&plugin_manifest)?; let plugin_name = plugin_manifest.gauntlet.name; let plugin_description = plugin_manifest.gauntlet.description; @@ -357,12 +322,15 @@ impl PluginLoader { let permissions = DbPluginPermissions { environment: plugin_manifest.permissions.environment, - high_resolution_time: plugin_manifest.permissions.high_resolution_time, network: plugin_manifest.permissions.network, - ffi: plugin_manifest.permissions.ffi, - fs_read_access: plugin_manifest.permissions.fs_read_access, - fs_write_access: plugin_manifest.permissions.fs_write_access, - run_subprocess: plugin_manifest.permissions.run_subprocess, + filesystem: DbPluginPermissionsFileSystem { + read: plugin_manifest.permissions.filesystem.read, + write: plugin_manifest.permissions.filesystem.write, + }, + exec: DbPluginPermissionsExec { + command: plugin_manifest.permissions.exec.command, + executable: plugin_manifest.permissions.exec.executable, + }, system: plugin_manifest.permissions.system, clipboard, main_search_bar, @@ -382,6 +350,162 @@ impl PluginLoader { preferences_user_data: HashMap::new() }) } + + fn validate_manifest(plugin_manifest: &PluginManifest) -> anyhow::Result<()> { + let supported_systems = &plugin_manifest.supported_system; + let supported_systems_str = supported_systems.iter().format(", "); + + let supports_linux = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Linux)); + let supports_macos = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::MacOS)); + let supports_windows = &supported_systems.iter().any(|system| matches!(system, PluginManifestSupportedSystem::Windows)); + + let permissions = &plugin_manifest.permissions; + + Self::validate_string_permissions(&permissions.environment)?; + Self::validate_network_permissions(&permissions.network)?; + Self::validate_path_permissions(&permissions.filesystem.read, supports_linux, supports_macos, supports_windows)?; + Self::validate_path_permissions(&permissions.filesystem.write, supports_linux, supports_macos, supports_windows)?; + Self::validate_string_permissions(&permissions.exec.command)?; + Self::validate_path_permissions(&permissions.exec.executable, supports_linux, supports_macos, supports_windows)?; + + // even though system accepts a list of predefined values + // unknown values are ignored to allow for easier + // adoption to breaking changes in deno + // TODO do a warning + Self::validate_string_permissions(&permissions.system)?; + + let env_exists = !permissions.environment.is_empty(); + let fs_read_exists = !permissions.filesystem.read.is_empty(); + let fs_write_exists = !permissions.filesystem.write.is_empty(); + let command_exists = !permissions.exec.command.is_empty(); + let executable_exists = !permissions.exec.executable.is_empty(); + let system_exists = !permissions.system.is_empty(); + + let os_required = env_exists || fs_read_exists || fs_write_exists || command_exists || executable_exists || system_exists; + + if os_required { + let current_system = if cfg!(target_os = "linux") { + PluginManifestSupportedSystem::Linux + } else if cfg!(target_os = "macos") { + PluginManifestSupportedSystem::MacOS + } else if cfg!(target_os = "windows") { + PluginManifestSupportedSystem::Windows + } else { + panic!("OS not supported") + }; + + if !supported_systems.contains(¤t_system) { + return Err(anyhow!("Plugin doesn't support current operating system. Operating systems supported by plugin: [{}]", supported_systems_str)) + } + } + + let has_inline_view = plugin_manifest.entrypoint + .iter() + .find(|entrypoint| matches!(entrypoint.entrypoint_type, PluginManifestEntrypointTypes::InlineView)) + .is_some(); + + if has_inline_view { + let main_search_bar = &permissions.main_search_bar; + if !main_search_bar.contains(&PluginManifestMainSearchBarPermissions::Read) { + return Err(anyhow!("Plugin uses entrypoint type 'inline-view' but doesn't specify main search bar 'read' permission")) + } + } + + Ok(()) + } + + fn validate_path_permissions(paths: &[String], supports_linux: &bool, supports_macos: &bool, supports_windows: &bool) -> anyhow::Result<()> { + for path in paths { + if path.is_empty() { + Err(anyhow!("Empty path is not allowed in permissions"))? + } + + let path = Utf8TypedPath::derive(path); + + if !path.is_absolute() { + Err(anyhow!("Relative path is not allowed in permissions: {}", path))? + } + + match path { + Utf8TypedPath::Unix(path) => { + if !supports_macos && !supports_linux { + Err(anyhow!("When using unix-style path in permissions, plugin is required to include \"linux\" or \"macos\" in \"supported_system\" manifest property: {}", path))? + } + + if !path.is_valid() { + Err(anyhow!("Path is not valid: {}", path))? + } + + for component in path.components() { + match component { + Utf8UnixComponent::Normal(_) | Utf8UnixComponent::RootDir => {} + Utf8UnixComponent::CurDir => { + Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + } + Utf8UnixComponent::ParentDir => { + Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + } + } + } + } + Utf8TypedPath::Windows(path) => { + if !supports_windows { + Err(anyhow!("When using windows-style path in permissions, plugin is required to include \"windows\" in \"supported_system\" manifest property: {}", path))? + } + + if !path.is_valid() { + Err(anyhow!("Path is not valid: {}", path))? + } + + for component in path.components() { + match component { + Utf8WindowsComponent::Normal(_) | Utf8WindowsComponent::RootDir | Utf8WindowsComponent::Prefix(_) => {} + Utf8WindowsComponent::CurDir => { + Err(anyhow!("Current directory '.' segment is not allowed in permission path: {}", path))? + } + Utf8WindowsComponent::ParentDir => { + Err(anyhow!("Parent directory '..' segment is not allowed in permission path: {}", path))? + } + } + } + } + } + } + + Ok(()) + } + + fn validate_string_permissions(values: &[String]) -> anyhow::Result<()> { + for value in values { + if value.is_empty() { + Err(anyhow!("Empty string value is not allowed in permissions"))? + } + } + + Ok(()) + } + + fn validate_network_permissions(values: &[String]) -> anyhow::Result<()> { + for value in values { + if value.is_empty() { + Err(anyhow!("Empty string value is not allowed in permissions"))? + } + + let url = url::Url::parse(&format!("http://{value}"))?; + + let contains_username = !url.username().is_empty(); + let contains_password = matches!(url.password(), Some(_)); + let contains_path = url.path() != "/"; + let contains_query = matches!(url.query(), Some(_)); + let contains_fragment = matches!(url.fragment(), Some(_)); + + // allow only domain and optional port + if contains_username || contains_password || contains_path || contains_query || contains_fragment { + Err(anyhow!("Network permission can only contain domain and optionally port: {}", value))? + } + } + Ok(()) + } } struct PluginDownloadData { @@ -844,17 +968,11 @@ pub struct PluginManifestPermissions { #[serde(default)] environment: Vec, #[serde(default)] - high_resolution_time: bool, - #[serde(default)] network: Vec, #[serde(default)] - ffi: Vec, - #[serde(default)] - fs_read_access: Vec, + filesystem: PluginManifestPermissionsFileSystem, #[serde(default)] - fs_write_access: Vec, - #[serde(default)] - run_subprocess: Vec, + exec: PluginManifestPermissionsExec, #[serde(default)] system: Vec, #[serde(default)] @@ -863,6 +981,22 @@ pub struct PluginManifestPermissions { main_search_bar: Vec, } +#[derive(Debug, Deserialize, Default)] +pub struct PluginManifestPermissionsFileSystem { + #[serde(default)] + pub read: Vec, + #[serde(default)] + pub write: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct PluginManifestPermissionsExec { + #[serde(default)] + pub command: Vec, + #[serde(default)] + pub executable: Vec, +} + #[derive(Debug, Deserialize)] pub enum PluginManifestClipboardPermissions { #[serde(rename = "read")] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 76df4e7..7035a4c 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -20,7 +20,8 @@ use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginActionShortcutKind, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPluginEntrypoint, DbPluginClipboardPermissions, DbPluginMainSearchBarPermissions}; use crate::plugins::global_shortcut::{convert_physical_shortcut_to_hotkey, register_listener}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginPermissions, PluginRuntimeData, start_plugin_runtime, PluginClipboardPermissions, PluginMainSearchBarPermissions}; +use crate::plugins::js::{AllPluginCommandData, OnePluginCommandData, PluginCode, PluginCommand, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::permissions::{PluginPermissions, PluginPermissionsClipboard, PluginPermissionsExec, PluginPermissionsFileSystem, PluginPermissionsMainSearchBar}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::SearchIndex; @@ -564,9 +565,9 @@ impl ApplicationManager { .clipboard .into_iter() .map(|permission| match permission { - DbPluginClipboardPermissions::Read => PluginClipboardPermissions::Read, - DbPluginClipboardPermissions::Write => PluginClipboardPermissions::Write, - DbPluginClipboardPermissions::Clear => PluginClipboardPermissions::Clear, + DbPluginClipboardPermissions::Read => PluginPermissionsClipboard::Read, + DbPluginClipboardPermissions::Write => PluginPermissionsClipboard::Write, + DbPluginClipboardPermissions::Clear => PluginPermissionsClipboard::Clear, }) .collect(); @@ -574,7 +575,7 @@ impl ApplicationManager { .main_search_bar .into_iter() .map(|permission| match permission { - DbPluginMainSearchBarPermissions::Read => PluginMainSearchBarPermissions::Read, + DbPluginMainSearchBarPermissions::Read => PluginPermissionsMainSearchBar::Read, }) .collect(); @@ -585,12 +586,15 @@ impl ApplicationManager { inline_view_entrypoint_id, permissions: PluginPermissions { environment: plugin.permissions.environment, - high_resolution_time: plugin.permissions.high_resolution_time, network: plugin.permissions.network, - ffi: plugin.permissions.ffi, - fs_read_access: plugin.permissions.fs_read_access, - fs_write_access: plugin.permissions.fs_write_access, - run_subprocess: plugin.permissions.run_subprocess, + filesystem: PluginPermissionsFileSystem { + read: plugin.permissions.filesystem.read, + write: plugin.permissions.filesystem.write, + }, + exec: PluginPermissionsExec { + command: plugin.permissions.exec.command, + executable: plugin.permissions.exec.executable, + }, system: plugin.permissions.system, clipboard: clipboard_permissions, main_search_bar: main_search_bar_permissions diff --git a/tools b/tools index e441fa0..6fc6230 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit e441fa089a42f2bb4192a2fe7de0d8e3b7bb2f32 +Subproject commit 6fc6230976ff22719e39a746617da7c1ad901d61