1use std::{collections::BTreeMap, fmt};
8
9use crate::platform::Target;
10
11use super::{
12 capability::{Capability, PermissionEntry},
13 has_app_manifest,
14 manifest::Manifest,
15 Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes, Value,
16 APP_ACL_KEY,
17};
18
19pub type ScopeKey = u64;
21
22#[cfg(debug_assertions)]
24#[derive(Default, Clone, PartialEq, Eq)]
25pub struct ResolvedCommandReference {
26 pub capability: String,
28 pub permission: String,
30}
31
32#[derive(Default, Clone, PartialEq, Eq)]
34pub struct ResolvedCommand {
35 pub context: ExecutionContext,
37 #[cfg(debug_assertions)]
39 pub referenced_by: ResolvedCommandReference,
40 pub windows: Vec<glob::Pattern>,
42 pub webviews: Vec<glob::Pattern>,
44 pub scope_id: Option<ScopeKey>,
46}
47
48impl fmt::Debug for ResolvedCommand {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.debug_struct("ResolvedCommand")
51 .field("context", &self.context)
52 .field("windows", &self.windows)
53 .field("webviews", &self.webviews)
54 .field("scope_id", &self.scope_id)
55 .finish()
56 }
57}
58
59#[derive(Debug, Default, Clone)]
61pub struct ResolvedScope {
62 pub allow: Vec<Value>,
64 pub deny: Vec<Value>,
66}
67
68#[derive(Debug, Default)]
70pub struct Resolved {
71 pub has_app_acl: bool,
73 pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
75 pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
77 pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
79 pub global_scope: BTreeMap<String, ResolvedScope>,
81}
82
83impl Resolved {
84 pub fn resolve(
86 acl: &BTreeMap<String, Manifest>,
87 mut capabilities: BTreeMap<String, Capability>,
88 target: Target,
89 ) -> Result<Self, Error> {
90 let mut allowed_commands = BTreeMap::new();
91 let mut denied_commands = BTreeMap::new();
92
93 let mut current_scope_id = 0;
94 let mut command_scope = BTreeMap::new();
95 let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
96
97 for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
99 with_resolved_permissions(
100 capability,
101 acl,
102 target,
103 |ResolvedPermission {
104 key,
105 commands,
106 scope,
107 #[cfg_attr(not(debug_assertions), allow(unused))]
108 permission_name,
109 }| {
110 if commands.allow.is_empty() && commands.deny.is_empty() {
111 global_scope.entry(key.to_string()).or_default().push(scope);
113 } else {
114 let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
115 current_scope_id += 1;
116 command_scope.insert(
117 current_scope_id,
118 ResolvedScope {
119 allow: scope.allow.unwrap_or_default(),
120 deny: scope.deny.unwrap_or_default(),
121 },
122 );
123 Some(current_scope_id)
124 } else {
125 None
126 };
127
128 for allowed_command in &commands.allow {
129 resolve_command(
130 &mut allowed_commands,
131 if key == APP_ACL_KEY {
132 allowed_command.to_string()
133 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
134 format!("plugin:{core_plugin_name}|{allowed_command}")
135 } else {
136 format!("plugin:{key}|{allowed_command}")
137 },
138 capability,
139 scope_id,
140 #[cfg(debug_assertions)]
141 permission_name.to_string(),
142 )?;
143 }
144
145 for denied_command in &commands.deny {
146 resolve_command(
147 &mut denied_commands,
148 if key == APP_ACL_KEY {
149 denied_command.to_string()
150 } else if let Some(core_plugin_name) = key.strip_prefix("core:") {
151 format!("plugin:{core_plugin_name}|{denied_command}")
152 } else {
153 format!("plugin:{key}|{denied_command}")
154 },
155 capability,
156 scope_id,
157 #[cfg(debug_assertions)]
158 permission_name.to_string(),
159 )?;
160 }
161 }
162
163 Ok(())
164 },
165 )?;
166 }
167
168 let global_scope = global_scope
169 .into_iter()
170 .map(|(key, scopes)| {
171 let mut resolved_scope = ResolvedScope {
172 allow: Vec::new(),
173 deny: Vec::new(),
174 };
175 for scope in scopes {
176 if let Some(allow) = scope.allow {
177 resolved_scope.allow.extend(allow);
178 }
179 if let Some(deny) = scope.deny {
180 resolved_scope.deny.extend(deny);
181 }
182 }
183 (key, resolved_scope)
184 })
185 .collect();
186
187 let resolved = Self {
188 has_app_acl: has_app_manifest(acl),
189 allowed_commands,
190 denied_commands,
191 command_scope,
192 global_scope,
193 };
194
195 Ok(resolved)
196 }
197}
198
199fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
200 raw.sort();
201
202 let mut patterns = Vec::new();
203 for pattern in raw {
204 patterns.push(glob::Pattern::new(&pattern)?);
205 }
206
207 Ok(patterns)
208}
209
210fn resolve_command(
211 commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
212 command: String,
213 capability: &Capability,
214 scope_id: Option<ScopeKey>,
215 #[cfg(debug_assertions)] referenced_by_permission_identifier: String,
216) -> Result<(), Error> {
217 let mut contexts = Vec::new();
218 if capability.local {
219 contexts.push(ExecutionContext::Local);
220 }
221 if let Some(remote) = &capability.remote {
222 contexts.extend(remote.urls.iter().map(|url| {
223 ExecutionContext::Remote {
224 url: url
225 .parse()
226 .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
227 }
228 }));
229 }
230
231 for context in contexts {
232 let resolved_list = commands.entry(command.clone()).or_default();
233
234 resolved_list.push(ResolvedCommand {
235 context,
236 #[cfg(debug_assertions)]
237 referenced_by: ResolvedCommandReference {
238 capability: capability.identifier.clone(),
239 permission: referenced_by_permission_identifier.clone(),
240 },
241 windows: parse_glob_patterns(capability.windows.clone())?,
242 webviews: parse_glob_patterns(capability.webviews.clone())?,
243 scope_id,
244 });
245 }
246
247 Ok(())
248}
249
250struct ResolvedPermission<'a> {
251 key: &'a str,
252 permission_name: &'a str,
253 commands: Commands,
254 scope: Scopes,
255}
256
257fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
260 capability: &Capability,
261 acl: &BTreeMap<String, Manifest>,
262 target: Target,
263 mut f: F,
264) -> Result<(), Error> {
265 for permission_entry in &capability.permissions {
266 let permission_id = permission_entry.identifier();
267
268 let permissions = get_permissions(permission_id, acl)?
269 .into_iter()
270 .filter(|p| p.permission.is_active(&target));
271
272 for TraversedPermission {
273 key,
274 permission_name,
275 permission,
276 } in permissions
277 {
278 let mut resolved_scope = Scopes::default();
279 let mut commands = Commands::default();
280
281 if let PermissionEntry::ExtendedPermission {
282 identifier: _,
283 scope,
284 } = permission_entry
285 {
286 if let Some(allow) = scope.allow.clone() {
287 resolved_scope
288 .allow
289 .get_or_insert_with(Default::default)
290 .extend(allow);
291 }
292 if let Some(deny) = scope.deny.clone() {
293 resolved_scope
294 .deny
295 .get_or_insert_with(Default::default)
296 .extend(deny);
297 }
298 }
299
300 if let Some(allow) = permission.scope.allow.clone() {
301 resolved_scope
302 .allow
303 .get_or_insert_with(Default::default)
304 .extend(allow);
305 }
306 if let Some(deny) = permission.scope.deny.clone() {
307 resolved_scope
308 .deny
309 .get_or_insert_with(Default::default)
310 .extend(deny);
311 }
312
313 commands.allow.extend(permission.commands.allow.clone());
314 commands.deny.extend(permission.commands.deny.clone());
315
316 f(ResolvedPermission {
317 key: &key,
318 permission_name: &permission_name,
319 commands,
320 scope: resolved_scope,
321 })?;
322 }
323 }
324
325 Ok(())
326}
327
328#[derive(Debug)]
330pub struct TraversedPermission<'a> {
331 pub key: String,
333 pub permission_name: String,
335 pub permission: &'a Permission,
337}
338
339pub fn get_permissions<'a>(
341 permission_id: &Identifier,
342 acl: &'a BTreeMap<String, Manifest>,
343) -> Result<Vec<TraversedPermission<'a>>, Error> {
344 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
345 let permission_name = permission_id.get_base();
346
347 let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
348 key: display_perm_key(key).to_string(),
349 available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
350 })?;
351
352 if permission_name == "default" {
353 manifest
354 .default_permission
355 .as_ref()
356 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
357 .unwrap_or_else(|| Ok(Default::default()))
358 } else if let Some(set) = manifest.permission_sets.get(permission_name) {
359 get_permission_set_permissions(permission_id, acl, manifest, set)
360 } else if let Some(permission) = manifest.permissions.get(permission_name) {
361 Ok(vec![TraversedPermission {
362 key: key.to_string(),
363 permission_name: permission_name.to_string(),
364 permission,
365 }])
366 } else {
367 Err(Error::UnknownPermission {
368 key: display_perm_key(key).to_string(),
369 permission: permission_name.to_string(),
370 })
371 }
372}
373
374fn get_permission_set_permissions<'a>(
376 permission_id: &Identifier,
377 acl: &'a BTreeMap<String, Manifest>,
378 manifest: &'a Manifest,
379 set: &'a PermissionSet,
380) -> Result<Vec<TraversedPermission<'a>>, Error> {
381 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
382
383 let mut permissions = Vec::new();
384
385 for perm in &set.permissions {
386 let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
392 let (manifest, permission_id, key, permission_name) =
393 if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
394 (manifest, &id, new_key, id.get_base())
395 } else {
396 (manifest, permission_id, key, perm.as_str())
397 };
398
399 if permission_name == "default" {
400 permissions.extend(
401 manifest
402 .default_permission
403 .as_ref()
404 .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
405 .transpose()?
406 .unwrap_or_default(),
407 );
408 } else if let Some(permission) = manifest.permissions.get(permission_name) {
409 permissions.push(TraversedPermission {
410 key: key.to_string(),
411 permission_name: permission_name.to_string(),
412 permission,
413 });
414 } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
415 permissions.extend(get_permission_set_permissions(
416 permission_id,
417 acl,
418 manifest,
419 permission_set,
420 )?);
421 } else {
422 return Err(Error::SetPermissionNotFound {
423 permission: permission_name.to_string(),
424 set: set.identifier.clone(),
425 });
426 }
427 }
428
429 Ok(permissions)
430}
431
432#[inline]
433fn display_perm_key(prefix: &str) -> &str {
434 if prefix == APP_ACL_KEY {
435 "app manifest"
436 } else {
437 prefix
438 }
439}
440
441#[cfg(feature = "build")]
442mod build {
443 use proc_macro2::TokenStream;
444 use quote::{quote, ToTokens, TokenStreamExt};
445 use std::convert::identity;
446
447 use super::*;
448 use crate::{literal_struct, tokens::*};
449
450 #[cfg(debug_assertions)]
451 impl ToTokens for ResolvedCommandReference {
452 fn to_tokens(&self, tokens: &mut TokenStream) {
453 let capability = str_lit(&self.capability);
454 let permission = str_lit(&self.permission);
455 literal_struct!(
456 tokens,
457 ::tauri::utils::acl::resolved::ResolvedCommandReference,
458 capability,
459 permission
460 )
461 }
462 }
463
464 impl ToTokens for ResolvedCommand {
465 fn to_tokens(&self, tokens: &mut TokenStream) {
466 #[cfg(debug_assertions)]
467 let referenced_by = &self.referenced_by;
468
469 let context = &self.context;
470
471 let windows = vec_lit(&self.windows, |window| {
472 let w = window.as_str();
473 quote!(#w.parse().unwrap())
474 });
475 let webviews = vec_lit(&self.webviews, |window| {
476 let w = window.as_str();
477 quote!(#w.parse().unwrap())
478 });
479 let scope_id = opt_lit(self.scope_id.as_ref());
480
481 #[cfg(debug_assertions)]
482 {
483 literal_struct!(
484 tokens,
485 ::tauri::utils::acl::resolved::ResolvedCommand,
486 context,
487 referenced_by,
488 windows,
489 webviews,
490 scope_id
491 )
492 }
493 #[cfg(not(debug_assertions))]
494 literal_struct!(
495 tokens,
496 ::tauri::utils::acl::resolved::ResolvedCommand,
497 context,
498 windows,
499 webviews,
500 scope_id
501 )
502 }
503 }
504
505 impl ToTokens for ResolvedScope {
506 fn to_tokens(&self, tokens: &mut TokenStream) {
507 let allow = vec_lit(&self.allow, identity);
508 let deny = vec_lit(&self.deny, identity);
509 literal_struct!(
510 tokens,
511 ::tauri::utils::acl::resolved::ResolvedScope,
512 allow,
513 deny
514 )
515 }
516 }
517
518 impl ToTokens for Resolved {
519 fn to_tokens(&self, tokens: &mut TokenStream) {
520 let has_app_acl = self.has_app_acl;
521
522 let allowed_commands = map_lit(
523 quote! { ::std::collections::BTreeMap },
524 &self.allowed_commands,
525 str_lit,
526 |v| vec_lit(v, identity),
527 );
528
529 let denied_commands = map_lit(
530 quote! { ::std::collections::BTreeMap },
531 &self.denied_commands,
532 str_lit,
533 |v| vec_lit(v, identity),
534 );
535
536 let command_scope = map_lit(
537 quote! { ::std::collections::BTreeMap },
538 &self.command_scope,
539 identity,
540 identity,
541 );
542
543 let global_scope = map_lit(
544 quote! { ::std::collections::BTreeMap },
545 &self.global_scope,
546 str_lit,
547 identity,
548 );
549
550 literal_struct!(
551 tokens,
552 ::tauri::utils::acl::resolved::Resolved,
553 has_app_acl,
554 allowed_commands,
555 denied_commands,
556 command_scope,
557 global_scope
558 )
559 }
560 }
561}
562
563#[cfg(test)]
564mod tests {
565
566 use super::{get_permissions, Identifier, Manifest, Permission, PermissionSet};
567
568 fn manifest<const P: usize, const S: usize>(
569 name: &str,
570 permissions: [&str; P],
571 default_set: Option<&[&str]>,
572 sets: [(&str, &[&str]); S],
573 ) -> (String, Manifest) {
574 (
575 name.to_string(),
576 Manifest {
577 default_permission: default_set.map(|perms| PermissionSet {
578 identifier: "default".to_string(),
579 description: "default set".to_string(),
580 permissions: perms.iter().map(|s| s.to_string()).collect(),
581 }),
582 permissions: permissions
583 .iter()
584 .map(|p| {
585 (
586 p.to_string(),
587 Permission {
588 identifier: p.to_string(),
589 ..Default::default()
590 },
591 )
592 })
593 .collect(),
594 permission_sets: sets
595 .iter()
596 .map(|(s, perms)| {
597 (
598 s.to_string(),
599 PermissionSet {
600 identifier: s.to_string(),
601 description: format!("{s} set"),
602 permissions: perms.iter().map(|s| s.to_string()).collect(),
603 },
604 )
605 })
606 .collect(),
607 ..Default::default()
608 },
609 )
610 }
611
612 fn id(id: &str) -> Identifier {
613 Identifier::try_from(id.to_string()).unwrap()
614 }
615
616 #[test]
617 fn resolves_permissions_from_other_plugins() {
618 let acl = [
619 manifest(
620 "fs",
621 ["read", "write", "rm", "exist"],
622 Some(&["read", "exist"]),
623 [],
624 ),
625 manifest(
626 "http",
627 ["fetch", "fetch-cancel"],
628 None,
629 [("fetch-with-cancel", &["fetch", "fetch-cancel"])],
630 ),
631 manifest(
632 "dialog",
633 ["open", "save"],
634 None,
635 [(
636 "extra",
637 &[
638 "save",
639 "fs:default",
640 "fs:write",
641 "http:default",
642 "http:fetch-with-cancel",
643 ],
644 )],
645 ),
646 ]
647 .into();
648
649 let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
650 assert_eq!(permissions.len(), 2);
651 assert_eq!(permissions[0].key, "fs");
652 assert_eq!(permissions[0].permission_name, "read");
653 assert_eq!(permissions[1].key, "fs");
654 assert_eq!(permissions[1].permission_name, "exist");
655
656 let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
657 assert_eq!(permissions.len(), 1);
658 assert_eq!(permissions[0].key, "fs");
659 assert_eq!(permissions[0].permission_name, "rm");
660
661 let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
662 assert_eq!(permissions.len(), 2);
663 assert_eq!(permissions[0].key, "http");
664 assert_eq!(permissions[0].permission_name, "fetch");
665 assert_eq!(permissions[1].key, "http");
666 assert_eq!(permissions[1].permission_name, "fetch-cancel");
667
668 let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
669 assert_eq!(permissions.len(), 6);
670 assert_eq!(permissions[0].key, "dialog");
671 assert_eq!(permissions[0].permission_name, "save");
672 assert_eq!(permissions[1].key, "fs");
673 assert_eq!(permissions[1].permission_name, "read");
674 assert_eq!(permissions[2].key, "fs");
675 assert_eq!(permissions[2].permission_name, "exist");
676 assert_eq!(permissions[3].key, "fs");
677 assert_eq!(permissions[3].permission_name, "write");
678 assert_eq!(permissions[4].key, "http");
679 assert_eq!(permissions[4].permission_name, "fetch");
680 assert_eq!(permissions[5].key, "http");
681 assert_eq!(permissions[5].permission_name, "fetch-cancel");
682 }
683}