Skip to main content

dscode_extension_host/
permissions.rs

1/**
2 * Extension Permission System
3 *
4 * Implements capability-based security for extensions.
5 * Each extension declares its required permissions in manifest.
6 */
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
11#[serde(rename_all = "camelCase")]
12pub enum Permission {
13    // File System
14    FileSystemRead,
15    FileSystemWrite,
16    FileSystemDelete,
17
18    // Workspace
19    WorkspaceRead,
20    WorkspaceWrite,
21    WorkspaceExecute,
22
23    // Network
24    NetworkHttp,
25    NetworkHttps,
26    NetworkWebSocket,
27
28    // System
29    ClipboardRead,
30    ClipboardWrite,
31
32    // Secrets
33    SecretsRead,
34    SecretsWrite,
35
36    // UI
37    ShowNotifications,
38    ShowDialogs,
39    ShowQuickPick,
40
41    // Editor
42    TextEditorRead,
43    TextEditorWrite,
44    TextEditorSelection,
45
46    // Debug
47    DebugStart,
48    DebugStop,
49    DebugBreakpoints,
50
51    // Terminal
52    TerminalCreate,
53    TerminalSend,
54
55    // Tasks
56    TasksExecute,
57
58    // Commands
59    CommandsExecute,
60
61    // Authentication
62    AuthenticationGetSession,
63    AuthenticationCreateSession,
64}
65
66#[derive(Debug, Clone)]
67pub struct ExtensionPermissions {
68    extension_id: String,
69    granted_permissions: HashSet<Permission>,
70}
71
72impl ExtensionPermissions {
73    pub fn new(extension_id: String, permissions: Vec<Permission>) -> Self {
74        Self { extension_id, granted_permissions: permissions.into_iter().collect() }
75    }
76
77    pub fn from_manifest(
78        extension_id: String, manifest_permissions: Option<Vec<String>>,
79    ) -> Result<Self, String> {
80        let permissions = match manifest_permissions {
81            Some(perms) => {
82                let mut granted = HashSet::new();
83                for perm_str in perms {
84                    let perm = Self::parse_permission(&perm_str)
85                        .ok_or_else(|| format!("Invalid permission: {}", perm_str))?;
86                    granted.insert(perm);
87                }
88                granted
89            }
90            None => HashSet::new(), // No permissions by default
91        };
92
93        Ok(Self { extension_id, granted_permissions: permissions })
94    }
95
96    pub fn has_permission(&self, permission: &Permission) -> bool {
97        self.granted_permissions.contains(permission)
98    }
99
100    pub fn check_permission(&self, permission: &Permission) -> Result<(), String> {
101        if self.has_permission(permission) {
102            Ok(())
103        } else {
104            Err(format!(
105                "Extension '{}' does not have permission: {:?}",
106                self.extension_id, permission
107            ))
108        }
109    }
110
111    fn parse_permission(s: &str) -> Option<Permission> {
112        match s {
113            "fileSystem.read" => Some(Permission::FileSystemRead),
114            "fileSystem.write" => Some(Permission::FileSystemWrite),
115            "fileSystem.delete" => Some(Permission::FileSystemDelete),
116            "workspace.read" => Some(Permission::WorkspaceRead),
117            "workspace.write" => Some(Permission::WorkspaceWrite),
118            "workspace.execute" => Some(Permission::WorkspaceExecute),
119            "network.http" => Some(Permission::NetworkHttp),
120            "network.https" => Some(Permission::NetworkHttps),
121            "network.webSocket" => Some(Permission::NetworkWebSocket),
122            "clipboard.read" => Some(Permission::ClipboardRead),
123            "clipboard.write" => Some(Permission::ClipboardWrite),
124            "secrets.read" => Some(Permission::SecretsRead),
125            "secrets.write" => Some(Permission::SecretsWrite),
126            "ui.notifications" => Some(Permission::ShowNotifications),
127            "ui.dialogs" => Some(Permission::ShowDialogs),
128            "ui.quickPick" => Some(Permission::ShowQuickPick),
129            "textEditor.read" => Some(Permission::TextEditorRead),
130            "textEditor.write" => Some(Permission::TextEditorWrite),
131            "textEditor.selection" => Some(Permission::TextEditorSelection),
132            "debug.start" => Some(Permission::DebugStart),
133            "debug.stop" => Some(Permission::DebugStop),
134            "debug.breakpoints" => Some(Permission::DebugBreakpoints),
135            "terminal.create" => Some(Permission::TerminalCreate),
136            "terminal.send" => Some(Permission::TerminalSend),
137            "tasks.execute" => Some(Permission::TasksExecute),
138            "commands.execute" => Some(Permission::CommandsExecute),
139            "authentication.getSession" => Some(Permission::AuthenticationGetSession),
140            "authentication.createSession" => Some(Permission::AuthenticationCreateSession),
141            _ => None,
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_permission_parsing() {
152        let perms = ExtensionPermissions::from_manifest(
153            "test.extension".to_string(),
154            Some(vec!["fileSystem.read".to_string(), "fileSystem.write".to_string()]),
155        )
156        .unwrap();
157
158        assert!(perms.has_permission(&Permission::FileSystemRead));
159        assert!(perms.has_permission(&Permission::FileSystemWrite));
160        assert!(!perms.has_permission(&Permission::FileSystemDelete));
161    }
162
163    #[test]
164    fn test_permission_check() {
165        let perms = ExtensionPermissions::from_manifest(
166            "test.extension".to_string(),
167            Some(vec!["fileSystem.read".to_string()]),
168        )
169        .unwrap();
170
171        assert!(perms.check_permission(&Permission::FileSystemRead).is_ok());
172        assert!(perms.check_permission(&Permission::FileSystemWrite).is_err());
173    }
174
175    #[test]
176    fn test_extension_permissions_grants() {
177        // Test file system category
178        let fs_perms = ExtensionPermissions::from_manifest(
179            "fs.ext".to_string(),
180            Some(vec![
181                "fileSystem.read".to_string(),
182                "fileSystem.write".to_string(),
183                "fileSystem.delete".to_string(),
184            ]),
185        )
186        .unwrap();
187        assert!(fs_perms.has_permission(&Permission::FileSystemRead));
188        assert!(fs_perms.has_permission(&Permission::FileSystemWrite));
189        assert!(fs_perms.has_permission(&Permission::FileSystemDelete));
190        assert!(!fs_perms.has_permission(&Permission::NetworkHttp));
191
192        // Test network category
193        let net_perms = ExtensionPermissions::from_manifest(
194            "net.ext".to_string(),
195            Some(vec![
196                "network.http".to_string(),
197                "network.https".to_string(),
198                "network.webSocket".to_string(),
199            ]),
200        )
201        .unwrap();
202        assert!(net_perms.has_permission(&Permission::NetworkHttp));
203        assert!(net_perms.has_permission(&Permission::NetworkHttps));
204        assert!(net_perms.has_permission(&Permission::NetworkWebSocket));
205        assert!(!net_perms.has_permission(&Permission::FileSystemRead));
206
207        // Test ui category
208        let ui_perms = ExtensionPermissions::from_manifest(
209            "ui.ext".to_string(),
210            Some(vec![
211                "ui.notifications".to_string(),
212                "ui.dialogs".to_string(),
213                "ui.quickPick".to_string(),
214            ]),
215        )
216        .unwrap();
217        assert!(ui_perms.has_permission(&Permission::ShowNotifications));
218        assert!(ui_perms.has_permission(&Permission::ShowDialogs));
219        assert!(ui_perms.has_permission(&Permission::ShowQuickPick));
220        assert!(!ui_perms.has_permission(&Permission::FileSystemRead));
221
222        // Test debug category
223        let dbg_perms = ExtensionPermissions::from_manifest(
224            "dbg.ext".to_string(),
225            Some(vec![
226                "debug.start".to_string(),
227                "debug.stop".to_string(),
228                "debug.breakpoints".to_string(),
229            ]),
230        )
231        .unwrap();
232        assert!(dbg_perms.has_permission(&Permission::DebugStart));
233        assert!(dbg_perms.has_permission(&Permission::DebugStop));
234        assert!(dbg_perms.has_permission(&Permission::DebugBreakpoints));
235        assert!(!dbg_perms.has_permission(&Permission::FileSystemRead));
236    }
237
238    #[test]
239    fn test_extension_permissions_default() {
240        // No permissions granted when manifest has None
241        let no_perms = ExtensionPermissions::from_manifest(
242            "default.ext".to_string(),
243            None,
244        )
245        .unwrap();
246        assert!(!no_perms.has_permission(&Permission::FileSystemRead));
247        assert!(!no_perms.has_permission(&Permission::NetworkHttp));
248        assert!(!no_perms.has_permission(&Permission::ShowNotifications));
249        assert!(!no_perms.has_permission(&Permission::DebugStart));
250
251        // No permissions granted when manifest has empty list
252        let empty_perms = ExtensionPermissions::from_manifest(
253            "empty.ext".to_string(),
254            Some(vec![]),
255        )
256        .unwrap();
257        assert!(!empty_perms.has_permission(&Permission::FileSystemRead));
258
259        // Invalid permission string should fail
260        let bad_result = ExtensionPermissions::from_manifest(
261            "bad.ext".to_string(),
262            Some(vec!["invalid.permission".to_string()]),
263        );
264        assert!(bad_result.is_err());
265    }
266
267    #[test]
268    fn test_permission_serde_roundtrip() {
269        // Verify each Permission variant serializes and deserializes correctly
270        let all_perms = vec![
271            Permission::FileSystemRead,
272            Permission::FileSystemWrite,
273            Permission::FileSystemDelete,
274            Permission::WorkspaceRead,
275            Permission::WorkspaceWrite,
276            Permission::WorkspaceExecute,
277            Permission::NetworkHttp,
278            Permission::NetworkHttps,
279            Permission::NetworkWebSocket,
280            Permission::ClipboardRead,
281            Permission::ClipboardWrite,
282            Permission::SecretsRead,
283            Permission::SecretsWrite,
284            Permission::ShowNotifications,
285            Permission::ShowDialogs,
286            Permission::ShowQuickPick,
287            Permission::TextEditorRead,
288            Permission::TextEditorWrite,
289            Permission::TextEditorSelection,
290            Permission::DebugStart,
291            Permission::DebugStop,
292            Permission::DebugBreakpoints,
293            Permission::TerminalCreate,
294            Permission::TerminalSend,
295            Permission::TasksExecute,
296            Permission::CommandsExecute,
297            Permission::AuthenticationGetSession,
298            Permission::AuthenticationCreateSession,
299        ];
300
301        for perm in &all_perms {
302            let json = serde_json::to_string(perm).unwrap();
303            let deserialized: Permission = serde_json::from_str(&json).unwrap();
304            assert_eq!(&deserialized, perm, "Roundtrip failed for {:?}", perm);
305        }
306    }
307
308    #[test]
309    fn test_permission_serde_uses_camel_case() {
310        // Verify that serde rename_all = "camelCase" is applied
311        let json = serde_json::to_string(&Permission::FileSystemRead).unwrap();
312        assert!(
313            json.contains("fileSystemRead"),
314            "Expected camelCase 'fileSystemRead' in JSON: {}",
315            json
316        );
317
318        let json = serde_json::to_string(&Permission::NetworkWebSocket).unwrap();
319        assert!(
320            json.contains("networkWebSocket"),
321            "Expected camelCase 'networkWebSocket' in JSON: {}",
322            json
323        );
324
325        let json = serde_json::to_string(&Permission::AuthenticationGetSession).unwrap();
326        assert!(
327            json.contains("authenticationGetSession"),
328            "Expected camelCase 'authenticationGetSession' in JSON: {}",
329            json
330        );
331    }
332
333    #[test]
334    fn test_permission_workspace_category() {
335        let perms = ExtensionPermissions::from_manifest(
336            "workspace.ext".to_string(),
337            Some(vec![
338                "workspace.read".to_string(),
339                "workspace.write".to_string(),
340                "workspace.execute".to_string(),
341            ]),
342        )
343        .unwrap();
344        assert!(perms.has_permission(&Permission::WorkspaceRead));
345        assert!(perms.has_permission(&Permission::WorkspaceWrite));
346        assert!(perms.has_permission(&Permission::WorkspaceExecute));
347        assert!(!perms.has_permission(&Permission::FileSystemRead));
348    }
349
350    #[test]
351    fn test_permission_clipboard_category() {
352        let perms = ExtensionPermissions::from_manifest(
353            "clipboard.ext".to_string(),
354            Some(vec![
355                "clipboard.read".to_string(),
356                "clipboard.write".to_string(),
357            ]),
358        )
359        .unwrap();
360        assert!(perms.has_permission(&Permission::ClipboardRead));
361        assert!(perms.has_permission(&Permission::ClipboardWrite));
362        assert!(!perms.has_permission(&Permission::FileSystemRead));
363    }
364
365    #[test]
366    fn test_permission_secrets_category() {
367        let perms = ExtensionPermissions::from_manifest(
368            "secrets.ext".to_string(),
369            Some(vec![
370                "secrets.read".to_string(),
371                "secrets.write".to_string(),
372            ]),
373        )
374        .unwrap();
375        assert!(perms.has_permission(&Permission::SecretsRead));
376        assert!(perms.has_permission(&Permission::SecretsWrite));
377        assert!(!perms.has_permission(&Permission::FileSystemRead));
378    }
379
380    #[test]
381    fn test_permission_text_editor_category() {
382        let perms = ExtensionPermissions::from_manifest(
383            "editor.ext".to_string(),
384            Some(vec![
385                "textEditor.read".to_string(),
386                "textEditor.write".to_string(),
387                "textEditor.selection".to_string(),
388            ]),
389        )
390        .unwrap();
391        assert!(perms.has_permission(&Permission::TextEditorRead));
392        assert!(perms.has_permission(&Permission::TextEditorWrite));
393        assert!(perms.has_permission(&Permission::TextEditorSelection));
394        assert!(!perms.has_permission(&Permission::FileSystemRead));
395    }
396
397    #[test]
398    fn test_permission_terminal_category() {
399        let perms = ExtensionPermissions::from_manifest(
400            "terminal.ext".to_string(),
401            Some(vec![
402                "terminal.create".to_string(),
403                "terminal.send".to_string(),
404            ]),
405        )
406        .unwrap();
407        assert!(perms.has_permission(&Permission::TerminalCreate));
408        assert!(perms.has_permission(&Permission::TerminalSend));
409        assert!(!perms.has_permission(&Permission::FileSystemRead));
410    }
411
412    #[test]
413    fn test_permission_tasks_and_commands_category() {
414        let perms = ExtensionPermissions::from_manifest(
415            "cmd.ext".to_string(),
416            Some(vec![
417                "tasks.execute".to_string(),
418                "commands.execute".to_string(),
419            ]),
420        )
421        .unwrap();
422        assert!(perms.has_permission(&Permission::TasksExecute));
423        assert!(perms.has_permission(&Permission::CommandsExecute));
424        assert!(!perms.has_permission(&Permission::FileSystemRead));
425    }
426
427    #[test]
428    fn test_permission_authentication_category() {
429        let perms = ExtensionPermissions::from_manifest(
430            "auth.ext".to_string(),
431            Some(vec![
432                "authentication.getSession".to_string(),
433                "authentication.createSession".to_string(),
434            ]),
435        )
436        .unwrap();
437        assert!(perms.has_permission(&Permission::AuthenticationGetSession));
438        assert!(perms.has_permission(&Permission::AuthenticationCreateSession));
439        assert!(!perms.has_permission(&Permission::FileSystemRead));
440    }
441
442    #[test]
443    fn test_permission_hash_equality() {
444        // Permissions with the same variant should be equal and hash the same
445        use std::collections::HashSet;
446        let mut set = HashSet::new();
447        set.insert(Permission::FileSystemRead);
448        set.insert(Permission::FileSystemRead);
449        assert_eq!(set.len(), 1, "Duplicate permissions should deduplicate in HashSet");
450        set.insert(Permission::FileSystemWrite);
451        assert_eq!(set.len(), 2);
452    }
453
454    #[test]
455    fn test_extension_permissions_new_constructor() {
456        let perms = ExtensionPermissions::new(
457            "direct.ext".to_string(),
458            vec![Permission::NetworkHttp, Permission::NetworkHttps],
459        );
460        assert!(perms.has_permission(&Permission::NetworkHttp));
461        assert!(perms.has_permission(&Permission::NetworkHttps));
462        assert!(!perms.has_permission(&Permission::NetworkWebSocket));
463    }
464
465    #[test]
466    fn test_check_permission_error_message() {
467        let perms = ExtensionPermissions::from_manifest(
468            "my.ext".to_string(),
469            Some(vec!["fileSystem.read".to_string()]),
470        )
471        .unwrap();
472
473        let err = perms.check_permission(&Permission::FileSystemWrite).unwrap_err();
474        assert!(err.contains("my.ext"), "Error should mention extension id");
475        assert!(err.contains("FileSystemWrite"), "Error should mention the permission");
476    }
477}