1use std::sync::Arc;
2
3use anyhow::{Result, bail};
4use extension::{ExtensionCapability, ExtensionManifest};
5use url::Url;
6
7pub struct CapabilityGranter {
8 granted_capabilities: Vec<ExtensionCapability>,
9 manifest: Arc<ExtensionManifest>,
10}
11
12impl CapabilityGranter {
13 pub fn new(
14 granted_capabilities: Vec<ExtensionCapability>,
15 manifest: Arc<ExtensionManifest>,
16 ) -> Self {
17 Self {
18 granted_capabilities,
19 manifest,
20 }
21 }
22
23 pub fn grant_exec(
24 &self,
25 desired_command: &str,
26 desired_args: &[impl AsRef<str> + std::fmt::Debug],
27 ) -> Result<()> {
28 self.manifest.allow_exec(desired_command, desired_args)?;
29
30 let is_allowed = self
31 .granted_capabilities
32 .iter()
33 .any(|capability| match capability {
34 ExtensionCapability::ProcessExec(capability) => {
35 capability.allows(desired_command, desired_args)
36 }
37 _ => false,
38 });
39
40 if !is_allowed {
41 bail!(
42 "capability for process:exec {desired_command} {desired_args:?} is not granted by the extension host",
43 );
44 }
45
46 Ok(())
47 }
48
49 pub fn grant_download_file(&self, desired_url: &Url) -> Result<()> {
50 let is_allowed = self
51 .granted_capabilities
52 .iter()
53 .any(|capability| match capability {
54 ExtensionCapability::DownloadFile(capability) => capability.allows(desired_url),
55 _ => false,
56 });
57
58 if !is_allowed {
59 bail!(
60 "capability for download_file {desired_url} is not granted by the extension host",
61 );
62 }
63
64 Ok(())
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use std::collections::BTreeMap;
71
72 use extension::{ProcessExecCapability, SchemaVersion};
73
74 use super::*;
75
76 fn extension_manifest() -> ExtensionManifest {
77 ExtensionManifest {
78 id: "test".into(),
79 name: "Test".to_string(),
80 version: "1.0.0".into(),
81 schema_version: SchemaVersion::ZERO,
82 description: None,
83 repository: None,
84 authors: vec![],
85 lib: Default::default(),
86 themes: vec![],
87 icon_themes: vec![],
88 languages: vec![],
89 grammars: BTreeMap::default(),
90 language_servers: BTreeMap::default(),
91 context_servers: BTreeMap::default(),
92 slash_commands: BTreeMap::default(),
93 indexed_docs_providers: BTreeMap::default(),
94 snippets: None,
95 capabilities: vec![],
96 debug_adapters: Default::default(),
97 debug_locators: Default::default(),
98 }
99 }
100
101 #[test]
102 fn test_grant_exec() {
103 let manifest = Arc::new(ExtensionManifest {
104 capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
105 command: "ls".to_string(),
106 args: vec!["-la".to_string()],
107 })],
108 ..extension_manifest()
109 });
110
111 // It returns an error when the extension host has no granted capabilities.
112 let granter = CapabilityGranter::new(Vec::new(), manifest.clone());
113 assert!(granter.grant_exec("ls", &["-la"]).is_err());
114
115 // It succeeds when the extension host has the exact capability.
116 let granter = CapabilityGranter::new(
117 vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
118 command: "ls".to_string(),
119 args: vec!["-la".to_string()],
120 })],
121 manifest.clone(),
122 );
123 assert!(granter.grant_exec("ls", &["-la"]).is_ok());
124
125 // It succeeds when the extension host has a wildcard capability.
126 let granter = CapabilityGranter::new(
127 vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
128 command: "*".to_string(),
129 args: vec!["**".to_string()],
130 })],
131 manifest.clone(),
132 );
133 assert!(granter.grant_exec("ls", &["-la"]).is_ok());
134 }
135}