1use std::{borrow::Cow, collections::BTreeMap, fmt};
8
9use crate::platform::Target;
10
11use super::{
12 APP_ACL_KEY, Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes,
13 Value,
14 capability::{Capability, PermissionEntry},
15 has_app_manifest,
16 manifest::Manifest,
17};
18
19pub type ScopeKey = u64;
21
22#[derive(Default, Clone, PartialEq, Eq)]
27pub struct ResolvedCommandReference {
28 #[cfg(debug_assertions)]
30 pub capability: String,
31 #[cfg(debug_assertions)]
33 pub permission: String,
34}
35
36impl ResolvedCommandReference {
37 #[doc(hidden)]
40 pub fn new(
41 #[cfg_attr(not(debug_assertions), allow(unused))] capability: String,
42 #[cfg_attr(not(debug_assertions), allow(unused))] permission: String,
43 ) -> Self {
44 Self {
45 #[cfg(debug_assertions)]
46 capability,
47 #[cfg(debug_assertions)]
48 permission,
49 }
50 }
51}
52
53#[derive(Default, Clone, PartialEq, Eq)]
55pub struct ResolvedCommand {
56 pub context: ExecutionContext,
58 #[cfg(debug_assertions)]
60 pub referenced_by: ResolvedCommandReference,
61 pub windows: Vec<glob::Pattern>,
63 pub webviews: Vec<glob::Pattern>,
65 pub scope_id: Option<ScopeKey>,
67}
68
69impl ResolvedCommand {
70 #[doc(hidden)]
73 pub fn new(
74 context: ExecutionContext,
75 #[cfg_attr(not(debug_assertions), allow(unused))] referenced_by: ResolvedCommandReference,
76 windows: Vec<glob::Pattern>,
77 webviews: Vec<glob::Pattern>,
78 scope_id: Option<ScopeKey>,
79 ) -> Self {
80 Self {
81 context,
82 #[cfg(debug_assertions)]
83 referenced_by,
84 windows,
85 webviews,
86 scope_id,
87 }
88 }
89}
90
91impl fmt::Debug for ResolvedCommand {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("ResolvedCommand")
94 .field("context", &self.context)
95 .field("windows", &self.windows)
96 .field("webviews", &self.webviews)
97 .field("scope_id", &self.scope_id)
98 .finish()
99 }
100}
101
102#[derive(Debug, Default, Clone)]
104pub struct ResolvedScope {
105 pub allow: Vec<Value>,
107 pub deny: Vec<Value>,
109}
110
111#[derive(Debug, Default)]
113pub struct Resolved {
114 pub has_app_acl: bool,
116 pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
118 pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
120 pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
122 pub global_scope: BTreeMap<String, ResolvedScope>,
124}
125
126impl Resolved {
127 pub fn resolve(
134 acl: &BTreeMap<String, Manifest>,
135 capabilities: BTreeMap<String, Capability>,
136 target: Target,
137 ) -> Result<Self, Error> {
138 Self::resolve_with_base_scope_id(acl, capabilities, target, 0)
139 }
140
141 pub fn resolve_with_base_scope_id(
147 acl: &BTreeMap<String, Manifest>,
148 mut capabilities: BTreeMap<String, Capability>,
149 target: Target,
150 base_scope_id: ScopeKey,
151 ) -> Result<Self, Error> {
152 let mut allowed_commands = BTreeMap::new();
153 let mut denied_commands = BTreeMap::new();
154
155 let mut current_scope_id = base_scope_id;
156 let mut command_scope = BTreeMap::new();
157 let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
158
159 for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
161 with_resolved_permissions(
162 capability,
163 acl,
164 target,
165 |ResolvedPermission {
166 key,
167 commands,
168 scope,
169 #[cfg_attr(not(debug_assertions), allow(unused))]
170 permission_name,
171 }| {
172 if commands.allow.is_empty() && commands.deny.is_empty() {
173 global_scope.entry(key).or_default().push(scope);
175 } else {
176 let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
177 current_scope_id += 1;
178 command_scope.insert(
179 current_scope_id,
180 ResolvedScope {
181 allow: scope.allow.unwrap_or_default(),
182 deny: scope.deny.unwrap_or_default(),
183 },
184 );
185 Some(current_scope_id)
186 } else {
187 None
188 };
189
190 for allowed_command in &commands.allow {
191 resolve_command(
192 &mut allowed_commands,
193 if key == APP_ACL_KEY {
194 allowed_command.to_string()
195 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
196 format!("plugin:{core_plugin_name}|{allowed_command}")
197 } else {
198 format!("plugin:{key}|{allowed_command}")
199 },
200 capability,
201 scope_id,
202 #[cfg(debug_assertions)]
203 permission_name,
204 )?;
205 }
206
207 for denied_command in &commands.deny {
208 resolve_command(
209 &mut denied_commands,
210 if key == APP_ACL_KEY {
211 denied_command.to_string()
212 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
213 format!("plugin:{core_plugin_name}|{denied_command}")
214 } else {
215 format!("plugin:{key}|{denied_command}")
216 },
217 capability,
218 scope_id,
219 #[cfg(debug_assertions)]
220 permission_name,
221 )?;
222 }
223 }
224
225 Ok(())
226 },
227 )?;
228 }
229
230 let global_scope = global_scope
231 .into_iter()
232 .map(|(key, scopes)| {
233 let mut resolved_scope = ResolvedScope {
234 allow: Vec::new(),
235 deny: Vec::new(),
236 };
237 for scope in scopes {
238 if let Some(allow) = scope.allow {
239 resolved_scope.allow.extend(allow);
240 }
241 if let Some(deny) = scope.deny {
242 resolved_scope.deny.extend(deny);
243 }
244 }
245 (key, resolved_scope)
246 })
247 .collect();
248
249 let resolved = Self {
250 has_app_acl: has_app_manifest(acl),
251 allowed_commands,
252 denied_commands,
253 command_scope,
254 global_scope,
255 };
256
257 Ok(resolved)
258 }
259}
260
261fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
262 raw.sort();
263
264 let mut patterns = Vec::new();
265 for pattern in raw {
266 patterns.push(glob::Pattern::new(&pattern)?);
267 }
268
269 Ok(patterns)
270}
271
272fn resolve_command(
273 commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
274 command: String,
275 capability: &Capability,
276 scope_id: Option<ScopeKey>,
277 #[cfg(debug_assertions)] referenced_by_permission_identifier: &str,
278) -> Result<(), Error> {
279 let mut contexts = Vec::new();
280 if capability.local {
281 contexts.push(ExecutionContext::Local);
282 }
283 if let Some(remote) = &capability.remote {
284 contexts.extend(remote.urls.iter().map(|url| {
285 ExecutionContext::Remote {
286 url: url
287 .parse()
288 .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
289 }
290 }));
291 }
292
293 for context in contexts {
294 let resolved_list = commands.entry(command.clone()).or_default();
295
296 resolved_list.push(ResolvedCommand {
297 context,
298 #[cfg(debug_assertions)]
299 referenced_by: ResolvedCommandReference {
300 capability: capability.identifier.clone(),
301 permission: referenced_by_permission_identifier.to_owned(),
302 },
303 windows: parse_glob_patterns(capability.windows.clone())?,
304 webviews: parse_glob_patterns(capability.webviews.clone())?,
305 scope_id,
306 });
307 }
308
309 Ok(())
310}
311
312struct ResolvedPermission<'a> {
313 key: String,
314 permission_name: &'a str,
315 commands: Commands,
316 scope: Scopes,
317}
318
319fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
322 capability: &Capability,
323 acl: &BTreeMap<String, Manifest>,
324 target: Target,
325 mut f: F,
326) -> Result<(), Error> {
327 for permission_entry in &capability.permissions {
328 let permission_id = permission_entry.identifier();
329
330 let permissions = get_permissions(permission_id, acl)?
331 .into_iter()
332 .filter(|p| p.permission.is_active(&target));
333
334 for TraversedPermission {
335 key,
336 permission_name,
337 permission,
338 } in permissions
339 {
340 let mut resolved_scope = Scopes::default();
341 let mut commands = Commands::default();
342
343 if let PermissionEntry::ExtendedPermission {
344 identifier: _,
345 scope,
346 } = permission_entry
347 {
348 if let Some(allow) = scope.allow.clone() {
349 resolved_scope
350 .allow
351 .get_or_insert_with(Default::default)
352 .extend(allow);
353 }
354 if let Some(deny) = scope.deny.clone() {
355 resolved_scope
356 .deny
357 .get_or_insert_with(Default::default)
358 .extend(deny);
359 }
360 }
361
362 if let Some(allow) = permission.scope.allow.clone() {
363 resolved_scope
364 .allow
365 .get_or_insert_with(Default::default)
366 .extend(allow);
367 }
368 if let Some(deny) = permission.scope.deny.clone() {
369 resolved_scope
370 .deny
371 .get_or_insert_with(Default::default)
372 .extend(deny);
373 }
374
375 commands.allow.extend(permission.commands.allow.clone());
376 commands.deny.extend(permission.commands.deny.clone());
377
378 f(ResolvedPermission {
379 key,
380 permission_name: &permission_name,
381 commands,
382 scope: resolved_scope,
383 })?;
384 }
385 }
386
387 Ok(())
388}
389
390#[derive(Debug)]
392pub struct TraversedPermission<'a> {
393 pub key: String,
395 pub permission_name: String,
397 pub permission: Cow<'a, Permission>,
402}
403
404pub fn get_permissions<'a>(
406 permission_id: &Identifier,
407 acl: &'a BTreeMap<String, Manifest>,
408) -> Result<Vec<TraversedPermission<'a>>, Error> {
409 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
410 let permission_name = permission_id.get_base();
411
412 let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
413 key: display_perm_key(key).to_string(),
414 available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
415 })?;
416
417 if permission_name == "default" {
418 manifest
419 .default_permission
420 .as_ref()
421 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
422 .unwrap_or_else(|| Ok(Default::default()))
423 } else if let Some(set) = manifest.permission_sets.get(permission_name) {
424 get_permission_set_permissions(permission_id, acl, manifest, set)
425 } else if let Some(permission) = manifest.permissions.get(permission_name) {
426 Ok(vec![TraversedPermission {
427 key: key.to_string(),
428 permission_name: permission_name.to_string(),
429 permission: Cow::Borrowed(permission),
430 }])
431 } else if let Some(permission) = manifest.command_permission(permission_name, key == APP_ACL_KEY)
432 {
433 Ok(vec![TraversedPermission {
434 key: key.to_string(),
435 permission_name: permission_name.to_string(),
436 permission: Cow::Owned(permission),
437 }])
438 } else {
439 Err(Error::UnknownPermission {
440 key: display_perm_key(key).to_string(),
441 permission: permission_name.to_string(),
442 })
443 }
444}
445
446fn get_permission_set_permissions<'a>(
448 permission_id: &Identifier,
449 acl: &'a BTreeMap<String, Manifest>,
450 manifest: &'a Manifest,
451 set: &'a PermissionSet,
452) -> Result<Vec<TraversedPermission<'a>>, Error> {
453 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
454
455 let mut permissions = Vec::new();
456
457 for perm in &set.permissions {
458 let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
464 let (manifest, permission_id, key, permission_name) =
465 if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
466 (manifest, &id, new_key, id.get_base())
467 } else {
468 (manifest, permission_id, key, perm.as_str())
469 };
470
471 if permission_name == "default" {
472 permissions.extend(
473 manifest
474 .default_permission
475 .as_ref()
476 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
477 .transpose()?
478 .unwrap_or_default(),
479 );
480 } else if let Some(permission) = manifest.permissions.get(permission_name) {
481 permissions.push(TraversedPermission {
482 key: key.to_string(),
483 permission_name: permission_name.to_string(),
484 permission: Cow::Borrowed(permission),
485 });
486 } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
487 permissions.extend(get_permission_set_permissions(
488 permission_id,
489 acl,
490 manifest,
491 permission_set,
492 )?);
493 } else if let Some(permission) =
494 manifest.command_permission(permission_name, key == APP_ACL_KEY)
495 {
496 permissions.push(TraversedPermission {
497 key: key.to_string(),
498 permission_name: permission_name.to_string(),
499 permission: Cow::Owned(permission),
500 });
501 } else {
502 return Err(Error::SetPermissionNotFound {
503 permission: permission_name.to_string(),
504 set: set.identifier.clone(),
505 });
506 }
507 }
508
509 Ok(permissions)
510}
511
512#[inline]
513fn display_perm_key(prefix: &str) -> &str {
514 if prefix == APP_ACL_KEY {
515 "app manifest"
516 } else {
517 prefix
518 }
519}
520
521#[cfg(any(feature = "build", feature = "build-2"))]
522mod build {
523 use proc_macro2::TokenStream;
524 use quote::{ToTokens, TokenStreamExt, quote};
525 use std::convert::identity;
526
527 use super::*;
528 use crate::{literal_struct, tokens::*};
529
530 #[cfg(debug_assertions)]
531 impl ToTokens for ResolvedCommandReference {
532 fn to_tokens(&self, tokens: &mut TokenStream) {
533 let capability = str_lit(&self.capability);
534 let permission = str_lit(&self.permission);
535 tokens.append_all(quote! {
536 ::tauri::utils::acl::resolved::ResolvedCommandReference::new(#capability, #permission)
537 });
538 }
539 }
540
541 impl ToTokens for ResolvedCommand {
542 fn to_tokens(&self, tokens: &mut TokenStream) {
543 #[cfg(debug_assertions)]
544 let referenced_by = &self.referenced_by;
545 #[cfg(not(debug_assertions))]
546 let referenced_by =
547 quote!(::tauri::utils::acl::resolved::ResolvedCommandReference::default());
548
549 let context = &self.context;
550
551 let windows = vec_lit(&self.windows, |window| {
552 let w = window.as_str();
553 quote!(#w.parse().unwrap())
554 });
555 let webviews = vec_lit(&self.webviews, |window| {
556 let w = window.as_str();
557 quote!(#w.parse().unwrap())
558 });
559 let scope_id = opt_lit(self.scope_id.as_ref());
560
561 tokens.append_all(quote! {
562 ::tauri::utils::acl::resolved::ResolvedCommand::new(
563 #context,
564 #referenced_by,
565 #windows,
566 #webviews,
567 #scope_id
568 )
569 })
570 }
571 }
572
573 impl ToTokens for ResolvedScope {
574 fn to_tokens(&self, tokens: &mut TokenStream) {
575 let allow = vec_lit(&self.allow, identity);
576 let deny = vec_lit(&self.deny, identity);
577 literal_struct!(
578 tokens,
579 ::tauri::utils::acl::resolved::ResolvedScope,
580 allow,
581 deny
582 )
583 }
584 }
585
586 impl ToTokens for Resolved {
587 fn to_tokens(&self, tokens: &mut TokenStream) {
588 let has_app_acl = self.has_app_acl;
589
590 let allowed_commands = map_lit(
591 quote! { ::std::collections::BTreeMap },
592 &self.allowed_commands,
593 str_lit,
594 |v| vec_lit(v, identity),
595 );
596
597 let denied_commands = map_lit(
598 quote! { ::std::collections::BTreeMap },
599 &self.denied_commands,
600 str_lit,
601 |v| vec_lit(v, identity),
602 );
603
604 let command_scope = map_lit(
605 quote! { ::std::collections::BTreeMap },
606 &self.command_scope,
607 identity,
608 identity,
609 );
610
611 let global_scope = map_lit(
612 quote! { ::std::collections::BTreeMap },
613 &self.global_scope,
614 str_lit,
615 identity,
616 );
617
618 literal_struct!(
619 tokens,
620 ::tauri::utils::acl::resolved::Resolved,
621 has_app_acl,
622 allowed_commands,
623 denied_commands,
624 command_scope,
625 global_scope
626 )
627 }
628 }
629}
630
631#[cfg(test)]
632mod tests {
633
634 use super::{
635 APP_ACL_KEY, Identifier, Manifest, Permission, PermissionSet, Resolved, get_permissions,
636 };
637 use crate::platform::Target;
638
639 fn manifest<const P: usize, const S: usize>(
640 name: &str,
641 permissions: [&str; P],
642 default_set: Option<&[&str]>,
643 sets: [(&str, &[&str]); S],
644 ) -> (String, Manifest) {
645 (
646 name.to_string(),
647 Manifest {
648 default_permission: default_set.map(|perms| PermissionSet {
649 identifier: "default".to_string(),
650 description: "default set".to_string(),
651 permissions: perms.iter().map(|s| s.to_string()).collect(),
652 }),
653 permissions: permissions
654 .iter()
655 .map(|p| {
656 (
657 p.to_string(),
658 Permission {
659 identifier: p.to_string(),
660 ..Default::default()
661 },
662 )
663 })
664 .collect(),
665 permission_sets: sets
666 .iter()
667 .map(|(s, perms)| {
668 (
669 s.to_string(),
670 PermissionSet {
671 identifier: s.to_string(),
672 description: format!("{s} set"),
673 permissions: perms.iter().map(|s| s.to_string()).collect(),
674 },
675 )
676 })
677 .collect(),
678 ..Default::default()
679 },
680 )
681 }
682
683 fn id(id: &str) -> Identifier {
684 Identifier::try_from(id.to_string()).unwrap()
685 }
686
687 #[test]
688 fn resolves_permissions_from_other_plugins() {
689 let acl = [
690 manifest(
691 "fs",
692 ["read", "write", "rm", "exist"],
693 Some(&["read", "exist"]),
694 [],
695 ),
696 manifest(
697 "http",
698 ["fetch", "fetch-cancel"],
699 None,
700 [("fetch-with-cancel", &["fetch", "fetch-cancel"])],
701 ),
702 manifest(
703 "dialog",
704 ["open", "save"],
705 None,
706 [(
707 "extra",
708 &[
709 "save",
710 "fs:default",
711 "fs:write",
712 "http:default",
713 "http:fetch-with-cancel",
714 ],
715 )],
716 ),
717 ]
718 .into();
719
720 let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
721 assert_eq!(permissions.len(), 2);
722 assert_eq!(permissions[0].key, "fs");
723 assert_eq!(permissions[0].permission_name, "read");
724 assert_eq!(permissions[1].key, "fs");
725 assert_eq!(permissions[1].permission_name, "exist");
726
727 let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
728 assert_eq!(permissions.len(), 1);
729 assert_eq!(permissions[0].key, "fs");
730 assert_eq!(permissions[0].permission_name, "rm");
731
732 let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
733 assert_eq!(permissions.len(), 2);
734 assert_eq!(permissions[0].key, "http");
735 assert_eq!(permissions[0].permission_name, "fetch");
736 assert_eq!(permissions[1].key, "http");
737 assert_eq!(permissions[1].permission_name, "fetch-cancel");
738
739 let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
740 assert_eq!(permissions.len(), 6);
741 assert_eq!(permissions[0].key, "dialog");
742 assert_eq!(permissions[0].permission_name, "save");
743 assert_eq!(permissions[1].key, "fs");
744 assert_eq!(permissions[1].permission_name, "read");
745 assert_eq!(permissions[2].key, "fs");
746 assert_eq!(permissions[2].permission_name, "exist");
747 assert_eq!(permissions[3].key, "fs");
748 assert_eq!(permissions[3].permission_name, "write");
749 assert_eq!(permissions[4].key, "http");
750 assert_eq!(permissions[4].permission_name, "fetch");
751 assert_eq!(permissions[5].key, "http");
752 assert_eq!(permissions[5].permission_name, "fetch-cancel");
753 }
754
755 fn manifest_with_commands(name: &str, commands: &[&str]) -> (String, Manifest) {
756 (
757 name.to_string(),
758 Manifest {
759 commands: commands.iter().map(|c| c.to_string()).collect(),
760 ..Default::default()
761 },
762 )
763 }
764
765 #[test]
766 fn resolves_implicit_command_permissions() {
767 let acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into();
768
769 let permissions = get_permissions(&id("fs:allow-read-file"), &acl).unwrap();
771 assert_eq!(permissions.len(), 1);
772 assert_eq!(permissions[0].key, "fs");
773 assert_eq!(permissions[0].permission.commands.allow, ["read_file"]);
774 assert!(permissions[0].permission.commands.deny.is_empty());
775
776 let permissions = get_permissions(&id("fs:deny-write-file"), &acl).unwrap();
778 assert_eq!(permissions.len(), 1);
779 assert_eq!(permissions[0].permission.commands.deny, ["write_file"]);
780
781 assert!(get_permissions(&id("fs:allow-unknown"), &acl).is_err());
783 }
784
785 #[test]
786 fn resolves_wildcard_command_permission() {
787 let acl = [(
788 APP_ACL_KEY.to_string(),
789 manifest_with_commands("__app__", &["read_file", "write_file"]).1,
790 )]
791 .into();
792
793 let permissions = get_permissions(&id("allow-*"), &acl).unwrap();
795 assert_eq!(permissions.len(), 1);
796 assert_eq!(permissions[0].permission.commands.allow, ["*"]);
797
798 let permissions = get_permissions(&id("deny-*"), &acl).unwrap();
799 assert_eq!(permissions.len(), 1);
800 assert_eq!(permissions[0].permission.commands.deny, ["*"]);
801
802 let plugin_acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into();
804 assert!(get_permissions(&id("fs:allow-*"), &plugin_acl).is_err());
805 assert!(get_permissions(&id("fs:allow-read-file"), &plugin_acl).is_ok());
807
808 let empty = [(APP_ACL_KEY.to_string(), manifest("__app__", [], None, []).1)].into();
810 assert!(get_permissions(&id("allow-*"), &empty).is_err());
811 }
812
813 #[test]
814 fn resolve_wildcard_uses_single_entry() {
815 use crate::acl::{Capability, capability::PermissionEntry};
816
817 let acl = [(
818 APP_ACL_KEY.to_string(),
819 Manifest {
820 commands: ["echo", "ping", "spam"]
821 .iter()
822 .map(|c| c.to_string())
823 .collect(),
824 ..Default::default()
825 },
826 )]
827 .into();
828
829 let capability = Capability {
830 identifier: "main".to_string(),
831 description: String::new(),
832 remote: None,
833 local: true,
834 windows: vec!["main".to_string()],
835 webviews: Vec::new(),
836 permissions: vec![PermissionEntry::PermissionRef(id("allow-*"))],
837 platforms: None,
838 };
839
840 let resolved = Resolved::resolve(
841 &acl,
842 [("main".to_string(), capability)].into(),
843 Target::Linux,
844 )
845 .unwrap();
846
847 assert_eq!(resolved.allowed_commands.len(), 1);
849 assert!(resolved.allowed_commands.contains_key("*"));
850 assert!(resolved.denied_commands.is_empty());
851 }
852
853 #[test]
854 fn resolve_assigns_scope_ids_from_base() {
855 use super::{Capability, Resolved, Target};
856 use std::collections::BTreeMap;
857
858 let acl: BTreeMap<String, Manifest> = [(
859 "http".to_string(),
860 Manifest {
861 permissions: [(
862 "allow-fetch".to_string(),
863 serde_json::from_value::<Permission>(serde_json::json!({
864 "identifier": "allow-fetch",
865 "commands": { "allow": ["fetch"] },
866 "scope": { "allow": [{ "url": "https://example.com" }] }
867 }))
868 .unwrap(),
869 )]
870 .into(),
871 ..Default::default()
872 },
873 )]
874 .into();
875 let capabilities: BTreeMap<String, Capability> = [(
876 "main".to_string(),
877 serde_json::from_value(serde_json::json!({
878 "identifier": "main",
879 "windows": ["main"],
880 "permissions": ["http:allow-fetch"]
881 }))
882 .unwrap(),
883 )]
884 .into();
885
886 let resolved = Resolved::resolve(&acl, capabilities.clone(), Target::current()).unwrap();
887 assert_eq!(
888 resolved.command_scope.keys().copied().collect::<Vec<_>>(),
889 vec![1]
890 );
891 assert_eq!(
892 resolved.allowed_commands["plugin:http|fetch"][0].scope_id,
893 Some(1)
894 );
895
896 let resolved =
897 Resolved::resolve_with_base_scope_id(&acl, capabilities, Target::current(), 10).unwrap();
898 assert_eq!(
899 resolved.command_scope.keys().copied().collect::<Vec<_>>(),
900 vec![11]
901 );
902 assert_eq!(
903 resolved.allowed_commands["plugin:http|fetch"][0].scope_id,
904 Some(11)
905 );
906 }
907}