1use std::{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: &'a Permission,
399}
400
401pub fn get_permissions<'a>(
403 permission_id: &Identifier,
404 acl: &'a BTreeMap<String, Manifest>,
405) -> Result<Vec<TraversedPermission<'a>>, Error> {
406 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
407 let permission_name = permission_id.get_base();
408
409 let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
410 key: display_perm_key(key).to_string(),
411 available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
412 })?;
413
414 if permission_name == "default" {
415 manifest
416 .default_permission
417 .as_ref()
418 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
419 .unwrap_or_else(|| Ok(Default::default()))
420 } else if let Some(set) = manifest.permission_sets.get(permission_name) {
421 get_permission_set_permissions(permission_id, acl, manifest, set)
422 } else if let Some(permission) = manifest.permissions.get(permission_name) {
423 Ok(vec![TraversedPermission {
424 key: key.to_string(),
425 permission_name: permission_name.to_string(),
426 permission,
427 }])
428 } else {
429 Err(Error::UnknownPermission {
430 key: display_perm_key(key).to_string(),
431 permission: permission_name.to_string(),
432 })
433 }
434}
435
436fn get_permission_set_permissions<'a>(
438 permission_id: &Identifier,
439 acl: &'a BTreeMap<String, Manifest>,
440 manifest: &'a Manifest,
441 set: &'a PermissionSet,
442) -> Result<Vec<TraversedPermission<'a>>, Error> {
443 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
444
445 let mut permissions = Vec::new();
446
447 for perm in &set.permissions {
448 let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
454 let (manifest, permission_id, key, permission_name) =
455 if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
456 (manifest, &id, new_key, id.get_base())
457 } else {
458 (manifest, permission_id, key, perm.as_str())
459 };
460
461 if permission_name == "default" {
462 permissions.extend(
463 manifest
464 .default_permission
465 .as_ref()
466 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
467 .transpose()?
468 .unwrap_or_default(),
469 );
470 } else if let Some(permission) = manifest.permissions.get(permission_name) {
471 permissions.push(TraversedPermission {
472 key: key.to_string(),
473 permission_name: permission_name.to_string(),
474 permission,
475 });
476 } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
477 permissions.extend(get_permission_set_permissions(
478 permission_id,
479 acl,
480 manifest,
481 permission_set,
482 )?);
483 } else {
484 return Err(Error::SetPermissionNotFound {
485 permission: permission_name.to_string(),
486 set: set.identifier.clone(),
487 });
488 }
489 }
490
491 Ok(permissions)
492}
493
494#[inline]
495fn display_perm_key(prefix: &str) -> &str {
496 if prefix == APP_ACL_KEY {
497 "app manifest"
498 } else {
499 prefix
500 }
501}
502
503#[cfg(any(feature = "build", feature = "build-2"))]
504mod build {
505 use proc_macro2::TokenStream;
506 use quote::{ToTokens, TokenStreamExt, quote};
507 use std::convert::identity;
508
509 use super::*;
510 use crate::{literal_struct, tokens::*};
511
512 #[cfg(debug_assertions)]
513 impl ToTokens for ResolvedCommandReference {
514 fn to_tokens(&self, tokens: &mut TokenStream) {
515 let capability = str_lit(&self.capability);
516 let permission = str_lit(&self.permission);
517 tokens.append_all(quote! {
518 ::tauri::utils::acl::resolved::ResolvedCommandReference::new(#capability, #permission)
519 });
520 }
521 }
522
523 impl ToTokens for ResolvedCommand {
524 fn to_tokens(&self, tokens: &mut TokenStream) {
525 #[cfg(debug_assertions)]
526 let referenced_by = &self.referenced_by;
527 #[cfg(not(debug_assertions))]
528 let referenced_by =
529 quote!(::tauri::utils::acl::resolved::ResolvedCommandReference::default());
530
531 let context = &self.context;
532
533 let windows = vec_lit(&self.windows, |window| {
534 let w = window.as_str();
535 quote!(#w.parse().unwrap())
536 });
537 let webviews = vec_lit(&self.webviews, |window| {
538 let w = window.as_str();
539 quote!(#w.parse().unwrap())
540 });
541 let scope_id = opt_lit(self.scope_id.as_ref());
542
543 tokens.append_all(quote! {
544 ::tauri::utils::acl::resolved::ResolvedCommand::new(
545 #context,
546 #referenced_by,
547 #windows,
548 #webviews,
549 #scope_id
550 )
551 })
552 }
553 }
554
555 impl ToTokens for ResolvedScope {
556 fn to_tokens(&self, tokens: &mut TokenStream) {
557 let allow = vec_lit(&self.allow, identity);
558 let deny = vec_lit(&self.deny, identity);
559 literal_struct!(
560 tokens,
561 ::tauri::utils::acl::resolved::ResolvedScope,
562 allow,
563 deny
564 )
565 }
566 }
567
568 impl ToTokens for Resolved {
569 fn to_tokens(&self, tokens: &mut TokenStream) {
570 let has_app_acl = self.has_app_acl;
571
572 let allowed_commands = map_lit(
573 quote! { ::std::collections::BTreeMap },
574 &self.allowed_commands,
575 str_lit,
576 |v| vec_lit(v, identity),
577 );
578
579 let denied_commands = map_lit(
580 quote! { ::std::collections::BTreeMap },
581 &self.denied_commands,
582 str_lit,
583 |v| vec_lit(v, identity),
584 );
585
586 let command_scope = map_lit(
587 quote! { ::std::collections::BTreeMap },
588 &self.command_scope,
589 identity,
590 identity,
591 );
592
593 let global_scope = map_lit(
594 quote! { ::std::collections::BTreeMap },
595 &self.global_scope,
596 str_lit,
597 identity,
598 );
599
600 literal_struct!(
601 tokens,
602 ::tauri::utils::acl::resolved::Resolved,
603 has_app_acl,
604 allowed_commands,
605 denied_commands,
606 command_scope,
607 global_scope
608 )
609 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615
616 use super::{Identifier, Manifest, Permission, PermissionSet, get_permissions};
617
618 fn manifest<const P: usize, const S: usize>(
619 name: &str,
620 permissions: [&str; P],
621 default_set: Option<&[&str]>,
622 sets: [(&str, &[&str]); S],
623 ) -> (String, Manifest) {
624 (
625 name.to_string(),
626 Manifest {
627 default_permission: default_set.map(|perms| PermissionSet {
628 identifier: "default".to_string(),
629 description: "default set".to_string(),
630 permissions: perms.iter().map(|s| s.to_string()).collect(),
631 }),
632 permissions: permissions
633 .iter()
634 .map(|p| {
635 (
636 p.to_string(),
637 Permission {
638 identifier: p.to_string(),
639 ..Default::default()
640 },
641 )
642 })
643 .collect(),
644 permission_sets: sets
645 .iter()
646 .map(|(s, perms)| {
647 (
648 s.to_string(),
649 PermissionSet {
650 identifier: s.to_string(),
651 description: format!("{s} set"),
652 permissions: perms.iter().map(|s| s.to_string()).collect(),
653 },
654 )
655 })
656 .collect(),
657 ..Default::default()
658 },
659 )
660 }
661
662 fn id(id: &str) -> Identifier {
663 Identifier::try_from(id.to_string()).unwrap()
664 }
665
666 #[test]
667 fn resolves_permissions_from_other_plugins() {
668 let acl = [
669 manifest(
670 "fs",
671 ["read", "write", "rm", "exist"],
672 Some(&["read", "exist"]),
673 [],
674 ),
675 manifest(
676 "http",
677 ["fetch", "fetch-cancel"],
678 None,
679 [("fetch-with-cancel", &["fetch", "fetch-cancel"])],
680 ),
681 manifest(
682 "dialog",
683 ["open", "save"],
684 None,
685 [(
686 "extra",
687 &[
688 "save",
689 "fs:default",
690 "fs:write",
691 "http:default",
692 "http:fetch-with-cancel",
693 ],
694 )],
695 ),
696 ]
697 .into();
698
699 let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
700 assert_eq!(permissions.len(), 2);
701 assert_eq!(permissions[0].key, "fs");
702 assert_eq!(permissions[0].permission_name, "read");
703 assert_eq!(permissions[1].key, "fs");
704 assert_eq!(permissions[1].permission_name, "exist");
705
706 let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
707 assert_eq!(permissions.len(), 1);
708 assert_eq!(permissions[0].key, "fs");
709 assert_eq!(permissions[0].permission_name, "rm");
710
711 let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
712 assert_eq!(permissions.len(), 2);
713 assert_eq!(permissions[0].key, "http");
714 assert_eq!(permissions[0].permission_name, "fetch");
715 assert_eq!(permissions[1].key, "http");
716 assert_eq!(permissions[1].permission_name, "fetch-cancel");
717
718 let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
719 assert_eq!(permissions.len(), 6);
720 assert_eq!(permissions[0].key, "dialog");
721 assert_eq!(permissions[0].permission_name, "save");
722 assert_eq!(permissions[1].key, "fs");
723 assert_eq!(permissions[1].permission_name, "read");
724 assert_eq!(permissions[2].key, "fs");
725 assert_eq!(permissions[2].permission_name, "exist");
726 assert_eq!(permissions[3].key, "fs");
727 assert_eq!(permissions[3].permission_name, "write");
728 assert_eq!(permissions[4].key, "http");
729 assert_eq!(permissions[4].permission_name, "fetch");
730 assert_eq!(permissions[5].key, "http");
731 assert_eq!(permissions[5].permission_name, "fetch-cancel");
732 }
733
734 #[test]
735 fn resolve_assigns_scope_ids_from_base() {
736 use super::{Capability, Resolved, Target};
737 use std::collections::BTreeMap;
738
739 let acl: BTreeMap<String, Manifest> = [(
740 "http".to_string(),
741 Manifest {
742 permissions: [(
743 "allow-fetch".to_string(),
744 serde_json::from_value::<Permission>(serde_json::json!({
745 "identifier": "allow-fetch",
746 "commands": { "allow": ["fetch"] },
747 "scope": { "allow": [{ "url": "https://example.com" }] }
748 }))
749 .unwrap(),
750 )]
751 .into(),
752 ..Default::default()
753 },
754 )]
755 .into();
756 let capabilities: BTreeMap<String, Capability> = [(
757 "main".to_string(),
758 serde_json::from_value(serde_json::json!({
759 "identifier": "main",
760 "windows": ["main"],
761 "permissions": ["http:allow-fetch"]
762 }))
763 .unwrap(),
764 )]
765 .into();
766
767 let resolved = Resolved::resolve(&acl, capabilities.clone(), Target::current()).unwrap();
768 assert_eq!(
769 resolved.command_scope.keys().copied().collect::<Vec<_>>(),
770 vec![1]
771 );
772 assert_eq!(
773 resolved.allowed_commands["plugin:http|fetch"][0].scope_id,
774 Some(1)
775 );
776
777 let resolved =
778 Resolved::resolve_with_base_scope_id(&acl, capabilities, Target::current(), 10).unwrap();
779 assert_eq!(
780 resolved.command_scope.keys().copied().collect::<Vec<_>>(),
781 vec![11]
782 );
783 assert_eq!(
784 resolved.allowed_commands["plugin:http|fetch"][0].scope_id,
785 Some(11)
786 );
787 }
788}