diff --git a/Cargo.lock b/Cargo.lock index 2e5517ecbf..d1ea9cc32f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5385,11 +5385,13 @@ dependencies = [ "log", "lsp", "parking_lot", + "pretty_assertions", "semantic_version", "serde", "serde_json", "task", "toml 0.8.20", + "url", "util", "wasm-encoder 0.221.3", "wasmparser 0.221.3", diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 4fc7da2dca..42189f20b3 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -32,7 +32,11 @@ serde.workspace = true serde_json.workspace = true task.workspace = true toml.workspace = true +url.workspace = true util.workspace = true wasm-encoder.workspace = true wasmparser.workspace = true workspace-hack.workspace = true + +[dev-dependencies] +pretty_assertions.workspace = true diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index eb09090e2a..68d4b9b835 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -11,6 +11,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use url::Url; /// This is the old version of the extension manifest, from when it was `extension.json`. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -103,6 +104,7 @@ impl ExtensionManifest { ExtensionCapability::ProcessExec(capability) => { capability.allows(desired_command, desired_args) } + _ => false, }); if !is_allowed { @@ -133,10 +135,11 @@ pub fn build_debug_adapter_schema_path( /// A capability for an extension. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(tag = "kind")] +#[serde(tag = "kind", rename_all = "snake_case")] pub enum ExtensionCapability { #[serde(rename = "process:exec")] ProcessExec(ProcessExecCapability), + DownloadFile(DownloadFileCapability), } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -182,6 +185,51 @@ impl ProcessExecCapability { } } +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct DownloadFileCapability { + pub host: String, + pub path: Vec, +} + +impl DownloadFileCapability { + /// Returns whether the capability allows downloading a file from the given URL. + pub fn allows(&self, url: &Url) -> bool { + let Some(desired_host) = url.host_str() else { + return false; + }; + + let Some(desired_path) = url.path_segments() else { + return false; + }; + let desired_path = desired_path.collect::>(); + + if self.host != desired_host && self.host != "*" { + return false; + } + + for (ix, path_segment) in self.path.iter().enumerate() { + if path_segment == "**" { + return true; + } + + if ix >= desired_path.len() { + return false; + } + + if path_segment != "*" && path_segment != desired_path[ix] { + return false; + } + } + + if self.path.len() < desired_path.len() { + return false; + } + + true + } +} + #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct LibManifestEntry { pub kind: Option, @@ -329,6 +377,8 @@ fn manifest_from_old_manifest( #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; + use super::*; fn extension_manifest() -> ExtensionManifest { @@ -380,7 +430,7 @@ mod tests { } #[test] - fn test_allow_exact_match() { + fn test_allow_exec_exact_match() { let manifest = ExtensionManifest { capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { command: "ls".to_string(), @@ -395,7 +445,7 @@ mod tests { } #[test] - fn test_allow_wildcard_arg() { + fn test_allow_exec_wildcard_arg() { let manifest = ExtensionManifest { capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { command: "git".to_string(), @@ -411,7 +461,7 @@ mod tests { } #[test] - fn test_allow_double_wildcard() { + fn test_allow_exec_double_wildcard() { let manifest = ExtensionManifest { capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { command: "cargo".to_string(), @@ -431,7 +481,7 @@ mod tests { } #[test] - fn test_allow_mixed_wildcards() { + fn test_allow_exec_mixed_wildcards() { let manifest = ExtensionManifest { capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { command: "docker".to_string(), @@ -454,4 +504,71 @@ mod tests { ); assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg } + + #[test] + fn test_download_file_capability_allows() { + let capability = DownloadFileCapability { + host: "*".to_string(), + path: vec!["**".to_string()], + }; + assert_eq!( + capability.allows(&"https://example.com/some/path".parse().unwrap()), + true + ); + + let capability = DownloadFileCapability { + host: "github.com".to_string(), + path: vec!["**".to_string()], + }; + assert_eq!( + capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), + true + ); + assert_eq!( + capability.allows( + &"https://fake-github.com/some-owner/some-repo" + .parse() + .unwrap() + ), + false + ); + + let capability = DownloadFileCapability { + host: "github.com".to_string(), + path: vec!["specific-owner".to_string(), "*".to_string()], + }; + assert_eq!( + capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), + false + ); + assert_eq!( + capability.allows( + &"https://github.com/specific-owner/some-repo" + .parse() + .unwrap() + ), + true + ); + + let capability = DownloadFileCapability { + host: "github.com".to_string(), + path: vec!["specific-owner".to_string(), "*".to_string()], + }; + assert_eq!( + capability.allows( + &"https://github.com/some-owner/some-repo/extra" + .parse() + .unwrap() + ), + false + ); + assert_eq!( + capability.allows( + &"https://github.com/specific-owner/some-repo/extra" + .parse() + .unwrap() + ), + false + ); + } } diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 3496403fcd..42a6244003 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use anyhow::{Result, bail}; use extension::{ExtensionCapability, ExtensionManifest}; +use url::Url; pub struct CapabilityGranter { granted_capabilities: Vec, @@ -33,6 +34,7 @@ impl CapabilityGranter { ExtensionCapability::ProcessExec(capability) => { capability.allows(desired_command, desired_args) } + _ => false, }); if !is_allowed { @@ -43,6 +45,24 @@ impl CapabilityGranter { Ok(()) } + + pub fn grant_download_file(&self, desired_url: &Url) -> Result<()> { + let is_allowed = self + .granted_capabilities + .iter() + .any(|capability| match capability { + ExtensionCapability::DownloadFile(capability) => capability.allows(desired_url), + _ => false, + }); + + if !is_allowed { + bail!( + "capability for download_file {desired_url} is not granted by the extension host", + ); + } + + Ok(()) + } } #[cfg(test)] diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index dd215eb07e..59ecf2ec32 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -7,9 +7,9 @@ use async_trait::async_trait; use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest}; use extension::{ CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, - DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate, - ProcessExecCapability, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, - SlashCommandOutput, Symbol, WorktreeDelegate, + DebugTaskDefinition, DownloadFileCapability, ExtensionCapability, ExtensionHostProxy, + KeyValueStoreDelegate, ProcessExecCapability, ProjectDelegate, SlashCommand, + SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, }; use fs::{Fs, normalize_path}; use futures::future::LocalBoxFuture; @@ -576,10 +576,16 @@ impl WasmHost { node_runtime, proxy, release_channel: ReleaseChannel::global(cx), - granted_capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "*".to_string(), - args: vec!["**".to_string()], - })], + granted_capabilities: vec![ + ExtensionCapability::ProcessExec(ProcessExecCapability { + command: "*".to_string(), + args: vec!["**".to_string()], + }), + ExtensionCapability::DownloadFile(DownloadFileCapability { + host: "*".to_string(), + path: vec!["**".to_string()], + }), + ], _main_thread_message_task: task, main_thread_message_tx: tx, }) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index f943cf489e..7ff28d691f 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -30,6 +30,7 @@ use std::{ sync::{Arc, OnceLock}, }; use task::{SpawnInTerminal, ZedDebugConfig}; +use url::Url; use util::{archive::extract_zip, fs::make_file_executable, maybe}; use wasmtime::component::{Linker, Resource}; @@ -1011,6 +1012,9 @@ impl ExtensionImports for WasmState { file_type: DownloadedFileType, ) -> wasmtime::Result> { maybe!(async { + let parsed_url = Url::parse(&url)?; + self.capability_granter.grant_download_file(&parsed_url)?; + let path = PathBuf::from(path); let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());