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