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(
129 acl: &BTreeMap<String, Manifest>,
130 mut capabilities: BTreeMap<String, Capability>,
131 target: Target,
132 ) -> Result<Self, Error> {
133 let mut allowed_commands = BTreeMap::new();
134 let mut denied_commands = BTreeMap::new();
135
136 let mut current_scope_id = 0;
137 let mut command_scope = BTreeMap::new();
138 let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
139
140 for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
142 with_resolved_permissions(
143 capability,
144 acl,
145 target,
146 |ResolvedPermission {
147 key,
148 commands,
149 scope,
150 #[cfg_attr(not(debug_assertions), allow(unused))]
151 permission_name,
152 }| {
153 if commands.allow.is_empty() && commands.deny.is_empty() {
154 global_scope.entry(key).or_default().push(scope);
156 } else {
157 let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
158 current_scope_id += 1;
159 command_scope.insert(
160 current_scope_id,
161 ResolvedScope {
162 allow: scope.allow.unwrap_or_default(),
163 deny: scope.deny.unwrap_or_default(),
164 },
165 );
166 Some(current_scope_id)
167 } else {
168 None
169 };
170
171 for allowed_command in &commands.allow {
172 resolve_command(
173 &mut allowed_commands,
174 if key == APP_ACL_KEY {
175 allowed_command.to_string()
176 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
177 format!("plugin:{core_plugin_name}|{allowed_command}")
178 } else {
179 format!("plugin:{key}|{allowed_command}")
180 },
181 capability,
182 scope_id,
183 #[cfg(debug_assertions)]
184 permission_name,
185 )?;
186 }
187
188 for denied_command in &commands.deny {
189 resolve_command(
190 &mut denied_commands,
191 if key == APP_ACL_KEY {
192 denied_command.to_string()
193 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
194 format!("plugin:{core_plugin_name}|{denied_command}")
195 } else {
196 format!("plugin:{key}|{denied_command}")
197 },
198 capability,
199 scope_id,
200 #[cfg(debug_assertions)]
201 permission_name,
202 )?;
203 }
204 }
205
206 Ok(())
207 },
208 )?;
209 }
210
211 let global_scope = global_scope
212 .into_iter()
213 .map(|(key, scopes)| {
214 let mut resolved_scope = ResolvedScope {
215 allow: Vec::new(),
216 deny: Vec::new(),
217 };
218 for scope in scopes {
219 if let Some(allow) = scope.allow {
220 resolved_scope.allow.extend(allow);
221 }
222 if let Some(deny) = scope.deny {
223 resolved_scope.deny.extend(deny);
224 }
225 }
226 (key, resolved_scope)
227 })
228 .collect();
229
230 let resolved = Self {
231 has_app_acl: has_app_manifest(acl),
232 allowed_commands,
233 denied_commands,
234 command_scope,
235 global_scope,
236 };
237
238 Ok(resolved)
239 }
240}
241
242fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
243 raw.sort();
244
245 let mut patterns = Vec::new();
246 for pattern in raw {
247 patterns.push(glob::Pattern::new(&pattern)?);
248 }
249
250 Ok(patterns)
251}
252
253fn resolve_command(
254 commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
255 command: String,
256 capability: &Capability,
257 scope_id: Option<ScopeKey>,
258 #[cfg(debug_assertions)] referenced_by_permission_identifier: &str,
259) -> Result<(), Error> {
260 let mut contexts = Vec::new();
261 if capability.local {
262 contexts.push(ExecutionContext::Local);
263 }
264 if let Some(remote) = &capability.remote {
265 contexts.extend(remote.urls.iter().map(|url| {
266 ExecutionContext::Remote {
267 url: url
268 .parse()
269 .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
270 }
271 }));
272 }
273
274 for context in contexts {
275 let resolved_list = commands.entry(command.clone()).or_default();
276
277 resolved_list.push(ResolvedCommand {
278 context,
279 #[cfg(debug_assertions)]
280 referenced_by: ResolvedCommandReference {
281 capability: capability.identifier.clone(),
282 permission: referenced_by_permission_identifier.to_owned(),
283 },
284 windows: parse_glob_patterns(capability.windows.clone())?,
285 webviews: parse_glob_patterns(capability.webviews.clone())?,
286 scope_id,
287 });
288 }
289
290 Ok(())
291}
292
293struct ResolvedPermission<'a> {
294 key: String,
295 permission_name: &'a str,
296 commands: Commands,
297 scope: Scopes,
298}
299
300fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
303 capability: &Capability,
304 acl: &BTreeMap<String, Manifest>,
305 target: Target,
306 mut f: F,
307) -> Result<(), Error> {
308 for permission_entry in &capability.permissions {
309 let permission_id = permission_entry.identifier();
310
311 let permissions = get_permissions(permission_id, acl)?
312 .into_iter()
313 .filter(|p| p.permission.is_active(&target));
314
315 for TraversedPermission {
316 key,
317 permission_name,
318 permission,
319 } in permissions
320 {
321 let mut resolved_scope = Scopes::default();
322 let mut commands = Commands::default();
323
324 if let PermissionEntry::ExtendedPermission {
325 identifier: _,
326 scope,
327 } = permission_entry
328 {
329 if let Some(allow) = scope.allow.clone() {
330 resolved_scope
331 .allow
332 .get_or_insert_with(Default::default)
333 .extend(allow);
334 }
335 if let Some(deny) = scope.deny.clone() {
336 resolved_scope
337 .deny
338 .get_or_insert_with(Default::default)
339 .extend(deny);
340 }
341 }
342
343 if let Some(allow) = permission.scope.allow.clone() {
344 resolved_scope
345 .allow
346 .get_or_insert_with(Default::default)
347 .extend(allow);
348 }
349 if let Some(deny) = permission.scope.deny.clone() {
350 resolved_scope
351 .deny
352 .get_or_insert_with(Default::default)
353 .extend(deny);
354 }
355
356 commands.allow.extend(permission.commands.allow.clone());
357 commands.deny.extend(permission.commands.deny.clone());
358
359 f(ResolvedPermission {
360 key,
361 permission_name: &permission_name,
362 commands,
363 scope: resolved_scope,
364 })?;
365 }
366 }
367
368 Ok(())
369}
370
371#[derive(Debug)]
373pub struct TraversedPermission<'a> {
374 pub key: String,
376 pub permission_name: String,
378 pub permission: Cow<'a, Permission>,
383}
384
385pub fn get_permissions<'a>(
387 permission_id: &Identifier,
388 acl: &'a BTreeMap<String, Manifest>,
389) -> Result<Vec<TraversedPermission<'a>>, Error> {
390 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
391 let permission_name = permission_id.get_base();
392
393 let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
394 key: display_perm_key(key).to_string(),
395 available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
396 })?;
397
398 if permission_name == "default" {
399 manifest
400 .default_permission
401 .as_ref()
402 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
403 .unwrap_or_else(|| Ok(Default::default()))
404 } else if let Some(set) = manifest.permission_sets.get(permission_name) {
405 get_permission_set_permissions(permission_id, acl, manifest, set)
406 } else if let Some(permission) = manifest.permissions.get(permission_name) {
407 Ok(vec![TraversedPermission {
408 key: key.to_string(),
409 permission_name: permission_name.to_string(),
410 permission: Cow::Borrowed(permission),
411 }])
412 } else if let Some(permission) = manifest.command_permission(permission_name, key == APP_ACL_KEY)
413 {
414 Ok(vec![TraversedPermission {
415 key: key.to_string(),
416 permission_name: permission_name.to_string(),
417 permission: Cow::Owned(permission),
418 }])
419 } else {
420 Err(Error::UnknownPermission {
421 key: display_perm_key(key).to_string(),
422 permission: permission_name.to_string(),
423 })
424 }
425}
426
427fn get_permission_set_permissions<'a>(
429 permission_id: &Identifier,
430 acl: &'a BTreeMap<String, Manifest>,
431 manifest: &'a Manifest,
432 set: &'a PermissionSet,
433) -> Result<Vec<TraversedPermission<'a>>, Error> {
434 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
435
436 let mut permissions = Vec::new();
437
438 for perm in &set.permissions {
439 let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
445 let (manifest, permission_id, key, permission_name) =
446 if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
447 (manifest, &id, new_key, id.get_base())
448 } else {
449 (manifest, permission_id, key, perm.as_str())
450 };
451
452 if permission_name == "default" {
453 permissions.extend(
454 manifest
455 .default_permission
456 .as_ref()
457 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
458 .transpose()?
459 .unwrap_or_default(),
460 );
461 } else if let Some(permission) = manifest.permissions.get(permission_name) {
462 permissions.push(TraversedPermission {
463 key: key.to_string(),
464 permission_name: permission_name.to_string(),
465 permission: Cow::Borrowed(permission),
466 });
467 } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
468 permissions.extend(get_permission_set_permissions(
469 permission_id,
470 acl,
471 manifest,
472 permission_set,
473 )?);
474 } else if let Some(permission) =
475 manifest.command_permission(permission_name, key == APP_ACL_KEY)
476 {
477 permissions.push(TraversedPermission {
478 key: key.to_string(),
479 permission_name: permission_name.to_string(),
480 permission: Cow::Owned(permission),
481 });
482 } else {
483 return Err(Error::SetPermissionNotFound {
484 permission: permission_name.to_string(),
485 set: set.identifier.clone(),
486 });
487 }
488 }
489
490 Ok(permissions)
491}
492
493#[inline]
494fn display_perm_key(prefix: &str) -> &str {
495 if prefix == APP_ACL_KEY {
496 "app manifest"
497 } else {
498 prefix
499 }
500}
501
502#[cfg(any(feature = "build", feature = "build-2"))]
503mod build {
504 use proc_macro2::TokenStream;
505 use quote::{ToTokens, TokenStreamExt, quote};
506 use std::convert::identity;
507
508 use super::*;
509 use crate::{literal_struct, tokens::*};
510
511 #[cfg(debug_assertions)]
512 impl ToTokens for ResolvedCommandReference {
513 fn to_tokens(&self, tokens: &mut TokenStream) {
514 let capability = str_lit(&self.capability);
515 let permission = str_lit(&self.permission);
516 tokens.append_all(quote! {
517 ::tauri::utils::acl::resolved::ResolvedCommandReference::new(#capability, #permission)
518 });
519 }
520 }
521
522 impl ToTokens for ResolvedCommand {
523 fn to_tokens(&self, tokens: &mut TokenStream) {
524 #[cfg(debug_assertions)]
525 let referenced_by = &self.referenced_by;
526 #[cfg(not(debug_assertions))]
527 let referenced_by =
528 quote!(::tauri::utils::acl::resolved::ResolvedCommandReference::default());
529
530 let context = &self.context;
531
532 let windows = vec_lit(&self.windows, |window| {
533 let w = window.as_str();
534 quote!(#w.parse().unwrap())
535 });
536 let webviews = vec_lit(&self.webviews, |window| {
537 let w = window.as_str();
538 quote!(#w.parse().unwrap())
539 });
540 let scope_id = opt_lit(self.scope_id.as_ref());
541
542 tokens.append_all(quote! {
543 ::tauri::utils::acl::resolved::ResolvedCommand::new(
544 #context,
545 #referenced_by,
546 #windows,
547 #webviews,
548 #scope_id
549 )
550 })
551 }
552 }
553
554 impl ToTokens for ResolvedScope {
555 fn to_tokens(&self, tokens: &mut TokenStream) {
556 let allow = vec_lit(&self.allow, identity);
557 let deny = vec_lit(&self.deny, identity);
558 literal_struct!(
559 tokens,
560 ::tauri::utils::acl::resolved::ResolvedScope,
561 allow,
562 deny
563 )
564 }
565 }
566
567 impl ToTokens for Resolved {
568 fn to_tokens(&self, tokens: &mut TokenStream) {
569 let has_app_acl = self.has_app_acl;
570
571 let allowed_commands = map_lit(
572 quote! { ::std::collections::BTreeMap },
573 &self.allowed_commands,
574 str_lit,
575 |v| vec_lit(v, identity),
576 );
577
578 let denied_commands = map_lit(
579 quote! { ::std::collections::BTreeMap },
580 &self.denied_commands,
581 str_lit,
582 |v| vec_lit(v, identity),
583 );
584
585 let command_scope = map_lit(
586 quote! { ::std::collections::BTreeMap },
587 &self.command_scope,
588 identity,
589 identity,
590 );
591
592 let global_scope = map_lit(
593 quote! { ::std::collections::BTreeMap },
594 &self.global_scope,
595 str_lit,
596 identity,
597 );
598
599 literal_struct!(
600 tokens,
601 ::tauri::utils::acl::resolved::Resolved,
602 has_app_acl,
603 allowed_commands,
604 denied_commands,
605 command_scope,
606 global_scope
607 )
608 }
609 }
610}
611
612#[cfg(test)]
613mod tests {
614
615 use super::{
616 APP_ACL_KEY, Identifier, Manifest, Permission, PermissionSet, Resolved, get_permissions,
617 };
618 use crate::platform::Target;
619
620 fn manifest<const P: usize, const S: usize>(
621 name: &str,
622 permissions: [&str; P],
623 default_set: Option<&[&str]>,
624 sets: [(&str, &[&str]); S],
625 ) -> (String, Manifest) {
626 (
627 name.to_string(),
628 Manifest {
629 default_permission: default_set.map(|perms| PermissionSet {
630 identifier: "default".to_string(),
631 description: "default set".to_string(),
632 permissions: perms.iter().map(|s| s.to_string()).collect(),
633 }),
634 permissions: permissions
635 .iter()
636 .map(|p| {
637 (
638 p.to_string(),
639 Permission {
640 identifier: p.to_string(),
641 ..Default::default()
642 },
643 )
644 })
645 .collect(),
646 permission_sets: sets
647 .iter()
648 .map(|(s, perms)| {
649 (
650 s.to_string(),
651 PermissionSet {
652 identifier: s.to_string(),
653 description: format!("{s} set"),
654 permissions: perms.iter().map(|s| s.to_string()).collect(),
655 },
656 )
657 })
658 .collect(),
659 ..Default::default()
660 },
661 )
662 }
663
664 fn id(id: &str) -> Identifier {
665 Identifier::try_from(id.to_string()).unwrap()
666 }
667
668 #[test]
669 fn resolves_permissions_from_other_plugins() {
670 let acl = [
671 manifest(
672 "fs",
673 ["read", "write", "rm", "exist"],
674 Some(&["read", "exist"]),
675 [],
676 ),
677 manifest(
678 "http",
679 ["fetch", "fetch-cancel"],
680 None,
681 [("fetch-with-cancel", &["fetch", "fetch-cancel"])],
682 ),
683 manifest(
684 "dialog",
685 ["open", "save"],
686 None,
687 [(
688 "extra",
689 &[
690 "save",
691 "fs:default",
692 "fs:write",
693 "http:default",
694 "http:fetch-with-cancel",
695 ],
696 )],
697 ),
698 ]
699 .into();
700
701 let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
702 assert_eq!(permissions.len(), 2);
703 assert_eq!(permissions[0].key, "fs");
704 assert_eq!(permissions[0].permission_name, "read");
705 assert_eq!(permissions[1].key, "fs");
706 assert_eq!(permissions[1].permission_name, "exist");
707
708 let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
709 assert_eq!(permissions.len(), 1);
710 assert_eq!(permissions[0].key, "fs");
711 assert_eq!(permissions[0].permission_name, "rm");
712
713 let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
714 assert_eq!(permissions.len(), 2);
715 assert_eq!(permissions[0].key, "http");
716 assert_eq!(permissions[0].permission_name, "fetch");
717 assert_eq!(permissions[1].key, "http");
718 assert_eq!(permissions[1].permission_name, "fetch-cancel");
719
720 let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
721 assert_eq!(permissions.len(), 6);
722 assert_eq!(permissions[0].key, "dialog");
723 assert_eq!(permissions[0].permission_name, "save");
724 assert_eq!(permissions[1].key, "fs");
725 assert_eq!(permissions[1].permission_name, "read");
726 assert_eq!(permissions[2].key, "fs");
727 assert_eq!(permissions[2].permission_name, "exist");
728 assert_eq!(permissions[3].key, "fs");
729 assert_eq!(permissions[3].permission_name, "write");
730 assert_eq!(permissions[4].key, "http");
731 assert_eq!(permissions[4].permission_name, "fetch");
732 assert_eq!(permissions[5].key, "http");
733 assert_eq!(permissions[5].permission_name, "fetch-cancel");
734 }
735
736 fn manifest_with_commands(name: &str, commands: &[&str]) -> (String, Manifest) {
737 (
738 name.to_string(),
739 Manifest {
740 commands: commands.iter().map(|c| c.to_string()).collect(),
741 ..Default::default()
742 },
743 )
744 }
745
746 #[test]
747 fn resolves_implicit_command_permissions() {
748 let acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into();
749
750 let permissions = get_permissions(&id("fs:allow-read-file"), &acl).unwrap();
752 assert_eq!(permissions.len(), 1);
753 assert_eq!(permissions[0].key, "fs");
754 assert_eq!(permissions[0].permission.commands.allow, ["read_file"]);
755 assert!(permissions[0].permission.commands.deny.is_empty());
756
757 let permissions = get_permissions(&id("fs:deny-write-file"), &acl).unwrap();
759 assert_eq!(permissions.len(), 1);
760 assert_eq!(permissions[0].permission.commands.deny, ["write_file"]);
761
762 assert!(get_permissions(&id("fs:allow-unknown"), &acl).is_err());
764 }
765
766 #[test]
767 fn resolves_wildcard_command_permission() {
768 let acl = [(
769 APP_ACL_KEY.to_string(),
770 manifest_with_commands("__app__", &["read_file", "write_file"]).1,
771 )]
772 .into();
773
774 let permissions = get_permissions(&id("allow-*"), &acl).unwrap();
776 assert_eq!(permissions.len(), 1);
777 assert_eq!(permissions[0].permission.commands.allow, ["*"]);
778
779 let permissions = get_permissions(&id("deny-*"), &acl).unwrap();
780 assert_eq!(permissions.len(), 1);
781 assert_eq!(permissions[0].permission.commands.deny, ["*"]);
782
783 let plugin_acl = [manifest_with_commands("fs", &["read_file", "write_file"])].into();
785 assert!(get_permissions(&id("fs:allow-*"), &plugin_acl).is_err());
786 assert!(get_permissions(&id("fs:allow-read-file"), &plugin_acl).is_ok());
788
789 let empty = [(APP_ACL_KEY.to_string(), manifest("__app__", [], None, []).1)].into();
791 assert!(get_permissions(&id("allow-*"), &empty).is_err());
792 }
793
794 #[test]
795 fn resolve_wildcard_uses_single_entry() {
796 use crate::acl::{Capability, capability::PermissionEntry};
797
798 let acl = [(
799 APP_ACL_KEY.to_string(),
800 Manifest {
801 commands: ["echo", "ping", "spam"]
802 .iter()
803 .map(|c| c.to_string())
804 .collect(),
805 ..Default::default()
806 },
807 )]
808 .into();
809
810 let capability = Capability {
811 identifier: "main".to_string(),
812 description: String::new(),
813 remote: None,
814 local: true,
815 windows: vec!["main".to_string()],
816 webviews: Vec::new(),
817 permissions: vec![PermissionEntry::PermissionRef(id("allow-*"))],
818 platforms: None,
819 };
820
821 let resolved = Resolved::resolve(
822 &acl,
823 [("main".to_string(), capability)].into(),
824 Target::Linux,
825 )
826 .unwrap();
827
828 assert_eq!(resolved.allowed_commands.len(), 1);
830 assert!(resolved.allowed_commands.contains_key("*"));
831 assert!(resolved.denied_commands.is_empty());
832 }
833}