1use 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 FileSystemRead,
15 FileSystemWrite,
16 FileSystemDelete,
17
18 WorkspaceRead,
20 WorkspaceWrite,
21 WorkspaceExecute,
22
23 NetworkHttp,
25 NetworkHttps,
26 NetworkWebSocket,
27
28 ClipboardRead,
30 ClipboardWrite,
31
32 SecretsRead,
34 SecretsWrite,
35
36 ShowNotifications,
38 ShowDialogs,
39 ShowQuickPick,
40
41 TextEditorRead,
43 TextEditorWrite,
44 TextEditorSelection,
45
46 DebugStart,
48 DebugStop,
49 DebugBreakpoints,
50
51 TerminalCreate,
53 TerminalSend,
54
55 TasksExecute,
57
58 CommandsExecute,
60
61 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(), };
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 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 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 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 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 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 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 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 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 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 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}