extension_host: Add capability for downloading files (#35141)
This PR adds a new capability for downloading files in extensions. Currently all file downloads are allowed. Release Notes: - N/A
This commit is contained in:
parent
d7b403e981
commit
6a9a539b10
6 changed files with 165 additions and 12 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5385,11 +5385,13 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"pretty_assertions",
|
||||||
"semantic_version",
|
"semantic_version",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"task",
|
"task",
|
||||||
"toml 0.8.20",
|
"toml 0.8.20",
|
||||||
|
"url",
|
||||||
"util",
|
"util",
|
||||||
"wasm-encoder 0.221.3",
|
"wasm-encoder 0.221.3",
|
||||||
"wasmparser 0.221.3",
|
"wasmparser 0.221.3",
|
||||||
|
|
|
@ -32,7 +32,11 @@ serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
wasm-encoder.workspace = true
|
wasm-encoder.workspace = true
|
||||||
wasmparser.workspace = true
|
wasmparser.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
|
|
@ -11,6 +11,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// This is the old version of the extension manifest, from when it was `extension.json`.
|
/// This is the old version of the extension manifest, from when it was `extension.json`.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||||
|
@ -103,6 +104,7 @@ impl ExtensionManifest {
|
||||||
ExtensionCapability::ProcessExec(capability) => {
|
ExtensionCapability::ProcessExec(capability) => {
|
||||||
capability.allows(desired_command, desired_args)
|
capability.allows(desired_command, desired_args)
|
||||||
}
|
}
|
||||||
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if !is_allowed {
|
if !is_allowed {
|
||||||
|
@ -133,10 +135,11 @@ pub fn build_debug_adapter_schema_path(
|
||||||
|
|
||||||
/// A capability for an extension.
|
/// A capability for an extension.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum ExtensionCapability {
|
pub enum ExtensionCapability {
|
||||||
#[serde(rename = "process:exec")]
|
#[serde(rename = "process:exec")]
|
||||||
ProcessExec(ProcessExecCapability),
|
ProcessExec(ProcessExecCapability),
|
||||||
|
DownloadFile(DownloadFileCapability),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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)]
|
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||||
pub struct LibManifestEntry {
|
pub struct LibManifestEntry {
|
||||||
pub kind: Option<ExtensionLibraryKind>,
|
pub kind: Option<ExtensionLibraryKind>,
|
||||||
|
@ -329,6 +377,8 @@ fn manifest_from_old_manifest(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn extension_manifest() -> ExtensionManifest {
|
fn extension_manifest() -> ExtensionManifest {
|
||||||
|
@ -380,7 +430,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_allow_exact_match() {
|
fn test_allow_exec_exact_match() {
|
||||||
let manifest = ExtensionManifest {
|
let manifest = ExtensionManifest {
|
||||||
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
||||||
command: "ls".to_string(),
|
command: "ls".to_string(),
|
||||||
|
@ -395,7 +445,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_allow_wildcard_arg() {
|
fn test_allow_exec_wildcard_arg() {
|
||||||
let manifest = ExtensionManifest {
|
let manifest = ExtensionManifest {
|
||||||
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
||||||
command: "git".to_string(),
|
command: "git".to_string(),
|
||||||
|
@ -411,7 +461,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_allow_double_wildcard() {
|
fn test_allow_exec_double_wildcard() {
|
||||||
let manifest = ExtensionManifest {
|
let manifest = ExtensionManifest {
|
||||||
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
||||||
command: "cargo".to_string(),
|
command: "cargo".to_string(),
|
||||||
|
@ -431,7 +481,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_allow_mixed_wildcards() {
|
fn test_allow_exec_mixed_wildcards() {
|
||||||
let manifest = ExtensionManifest {
|
let manifest = ExtensionManifest {
|
||||||
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
||||||
command: "docker".to_string(),
|
command: "docker".to_string(),
|
||||||
|
@ -454,4 +504,71 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use extension::{ExtensionCapability, ExtensionManifest};
|
use extension::{ExtensionCapability, ExtensionManifest};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
pub struct CapabilityGranter {
|
pub struct CapabilityGranter {
|
||||||
granted_capabilities: Vec<ExtensionCapability>,
|
granted_capabilities: Vec<ExtensionCapability>,
|
||||||
|
@ -33,6 +34,7 @@ impl CapabilityGranter {
|
||||||
ExtensionCapability::ProcessExec(capability) => {
|
ExtensionCapability::ProcessExec(capability) => {
|
||||||
capability.allows(desired_command, desired_args)
|
capability.allows(desired_command, desired_args)
|
||||||
}
|
}
|
||||||
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if !is_allowed {
|
if !is_allowed {
|
||||||
|
@ -43,6 +45,24 @@ impl CapabilityGranter {
|
||||||
|
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -7,9 +7,9 @@ use async_trait::async_trait;
|
||||||
use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
|
use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
|
||||||
use extension::{
|
use extension::{
|
||||||
CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
|
CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
|
||||||
DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate,
|
DebugTaskDefinition, DownloadFileCapability, ExtensionCapability, ExtensionHostProxy,
|
||||||
ProcessExecCapability, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion,
|
KeyValueStoreDelegate, ProcessExecCapability, ProjectDelegate, SlashCommand,
|
||||||
SlashCommandOutput, Symbol, WorktreeDelegate,
|
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
|
||||||
};
|
};
|
||||||
use fs::{Fs, normalize_path};
|
use fs::{Fs, normalize_path};
|
||||||
use futures::future::LocalBoxFuture;
|
use futures::future::LocalBoxFuture;
|
||||||
|
@ -576,10 +576,16 @@ impl WasmHost {
|
||||||
node_runtime,
|
node_runtime,
|
||||||
proxy,
|
proxy,
|
||||||
release_channel: ReleaseChannel::global(cx),
|
release_channel: ReleaseChannel::global(cx),
|
||||||
granted_capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
|
granted_capabilities: vec![
|
||||||
command: "*".to_string(),
|
ExtensionCapability::ProcessExec(ProcessExecCapability {
|
||||||
args: vec!["**".to_string()],
|
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_task: task,
|
||||||
main_thread_message_tx: tx,
|
main_thread_message_tx: tx,
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,6 +30,7 @@ use std::{
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
};
|
};
|
||||||
use task::{SpawnInTerminal, ZedDebugConfig};
|
use task::{SpawnInTerminal, ZedDebugConfig};
|
||||||
|
use url::Url;
|
||||||
use util::{archive::extract_zip, fs::make_file_executable, maybe};
|
use util::{archive::extract_zip, fs::make_file_executable, maybe};
|
||||||
use wasmtime::component::{Linker, Resource};
|
use wasmtime::component::{Linker, Resource};
|
||||||
|
|
||||||
|
@ -1011,6 +1012,9 @@ impl ExtensionImports for WasmState {
|
||||||
file_type: DownloadedFileType,
|
file_type: DownloadedFileType,
|
||||||
) -> wasmtime::Result<Result<(), String>> {
|
) -> wasmtime::Result<Result<(), String>> {
|
||||||
maybe!(async {
|
maybe!(async {
|
||||||
|
let parsed_url = Url::parse(&url)?;
|
||||||
|
self.capability_granter.grant_download_file(&parsed_url)?;
|
||||||
|
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue