1use std::collections::BTreeMap;
6use std::fmt::{Debug, Display};
7use std::sync::Arc;
8
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11
12use tauri_utils::acl::has_app_manifest;
13use tauri_utils::acl::{
14 capability::{Capability, CapabilityFile, PermissionEntry},
15 manifest::Manifest,
16 Value, APP_ACL_KEY,
17};
18use tauri_utils::acl::{
19 resolved::{Resolved, ResolvedCommand, ResolvedScope, ScopeKey},
20 ExecutionContext, Scopes,
21};
22use tauri_utils::platform::Target;
23
24use url::Url;
25
26use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime};
27use crate::{AppHandle, Manager, StateManager, Webview};
28
29use super::{CommandArg, CommandItem};
30
31pub struct RuntimeAuthority {
33 acl: BTreeMap<String, crate::utils::acl::manifest::Manifest>,
34 allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
35 denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
36 pub(crate) scope_manager: ScopeManager,
37}
38
39pub enum Origin {
41 Local,
43 Remote {
45 url: Url,
47 },
48}
49
50impl Display for Origin {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::Local => write!(f, "local"),
54 Self::Remote { url } => write!(f, "remote: {url}"),
55 }
56 }
57}
58
59impl Origin {
60 fn matches(&self, context: &ExecutionContext) -> bool {
61 match (self, context) {
62 (Self::Local, ExecutionContext::Local) => true,
63 (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => {
64 url_pattern.test(url)
65 }
66 _ => false,
67 }
68 }
69}
70
71pub trait RuntimeCapability {
73 fn build(self) -> CapabilityFile;
75}
76
77impl<T: AsRef<str>> RuntimeCapability for T {
78 fn build(self) -> CapabilityFile {
79 self.as_ref().parse().expect("invalid capability")
80 }
81}
82
83pub struct CapabilityBuilder(Capability);
85
86impl CapabilityBuilder {
87 pub fn new(identifier: impl Into<String>) -> Self {
89 Self(Capability {
90 identifier: identifier.into(),
91 description: "".into(),
92 remote: None,
93 local: true,
94 windows: Vec::new(),
95 webviews: Vec::new(),
96 permissions: Vec::new(),
97 platforms: None,
98 })
99 }
100
101 pub fn remote(mut self, url: String) -> Self {
103 self
104 .0
105 .remote
106 .get_or_insert_with(Default::default)
107 .urls
108 .push(url);
109 self
110 }
111
112 pub fn local(mut self, local: bool) -> Self {
114 self.0.local = local;
115 self
116 }
117
118 pub fn window(mut self, window: impl Into<String>) -> Self {
120 self.0.windows.push(window.into());
121 self
122 }
123
124 pub fn windows(mut self, windows: impl IntoIterator<Item = impl Into<String>>) -> Self {
126 self.0.windows.extend(windows.into_iter().map(|w| w.into()));
127 self
128 }
129
130 pub fn webview(mut self, webview: impl Into<String>) -> Self {
132 self.0.webviews.push(webview.into());
133 self
134 }
135
136 pub fn webviews(mut self, webviews: impl IntoIterator<Item = impl Into<String>>) -> Self {
138 self
139 .0
140 .webviews
141 .extend(webviews.into_iter().map(|w| w.into()));
142 self
143 }
144
145 pub fn permission(mut self, permission: impl Into<String>) -> Self {
147 let permission = permission.into();
148 self.0.permissions.push(PermissionEntry::PermissionRef(
149 permission
150 .clone()
151 .try_into()
152 .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")),
153 ));
154 self
155 }
156
157 pub fn permission_scoped<T: Serialize>(
159 mut self,
160 permission: impl Into<String>,
161 allowed: Vec<T>,
162 denied: Vec<T>,
163 ) -> Self {
164 let permission = permission.into();
165 let identifier = permission
166 .clone()
167 .try_into()
168 .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'"));
169
170 let allowed_scope = allowed
171 .into_iter()
172 .map(|a| {
173 serde_json::to_value(a)
174 .expect("failed to serialize scope")
175 .into()
176 })
177 .collect();
178 let denied_scope = denied
179 .into_iter()
180 .map(|a| {
181 serde_json::to_value(a)
182 .expect("failed to serialize scope")
183 .into()
184 })
185 .collect();
186 let scope = Scopes {
187 allow: Some(allowed_scope),
188 deny: Some(denied_scope),
189 };
190
191 self
192 .0
193 .permissions
194 .push(PermissionEntry::ExtendedPermission { identifier, scope });
195 self
196 }
197
198 pub fn platform(mut self, platform: Target) -> Self {
202 self
203 .0
204 .platforms
205 .get_or_insert_with(Default::default)
206 .push(platform);
207 self
208 }
209
210 pub fn platforms(mut self, platforms: impl IntoIterator<Item = Target>) -> Self {
214 self
215 .0
216 .platforms
217 .get_or_insert_with(Default::default)
218 .extend(platforms);
219 self
220 }
221}
222
223impl RuntimeCapability for CapabilityBuilder {
224 fn build(self) -> CapabilityFile {
225 CapabilityFile::Capability(self.0)
226 }
227}
228
229impl RuntimeAuthority {
230 #[doc(hidden)]
231 pub fn new(acl: BTreeMap<String, Manifest>, resolved_acl: Resolved) -> Self {
232 let command_cache = resolved_acl
233 .command_scope
234 .keys()
235 .map(|key| (*key, StateManager::new()))
236 .collect();
237 Self {
238 acl,
239 allowed_commands: resolved_acl.allowed_commands,
240 denied_commands: resolved_acl.denied_commands,
241 scope_manager: ScopeManager {
242 command_scope: resolved_acl.command_scope,
243 global_scope: resolved_acl.global_scope,
244 command_cache,
245 global_scope_cache: StateManager::new(),
246 },
247 }
248 }
249
250 pub(crate) fn has_app_manifest(&self) -> bool {
251 has_app_manifest(&self.acl)
252 }
253
254 #[doc(hidden)]
255 pub fn __allow_command(&mut self, command: String, context: ExecutionContext) {
256 self.allowed_commands.insert(
257 command,
258 vec![ResolvedCommand {
259 context,
260 windows: vec!["*".parse().unwrap()],
261 ..Default::default()
262 }],
263 );
264 }
265
266 pub fn add_capability(&mut self, capability: impl RuntimeCapability) -> crate::Result<()> {
268 let mut capabilities = BTreeMap::new();
269 match capability.build() {
270 CapabilityFile::Capability(c) => {
271 capabilities.insert(c.identifier.clone(), c);
272 }
273
274 CapabilityFile::List(capabilities_list)
275 | CapabilityFile::NamedList {
276 capabilities: capabilities_list,
277 } => {
278 capabilities.extend(
279 capabilities_list
280 .into_iter()
281 .map(|c| (c.identifier.clone(), c)),
282 );
283 }
284 }
285
286 let resolved = Resolved::resolve(
287 &self.acl,
288 capabilities,
289 tauri_utils::platform::Target::current(),
290 )
291 .unwrap();
292
293 for (plugin, global_scope) in resolved.global_scope {
295 let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default();
296
297 global_scope_entry.allow.extend(global_scope.allow);
298 global_scope_entry.deny.extend(global_scope.deny);
299
300 self.scope_manager.global_scope_cache = StateManager::new();
301 }
302
303 for (cmd_key, resolved_cmds) in resolved.denied_commands {
305 let entry = self.denied_commands.entry(cmd_key).or_default();
306 entry.extend(resolved_cmds);
307 }
308
309 for (cmd_key, resolved_cmds) in resolved.allowed_commands {
311 for resolved_cmd in &resolved_cmds {
313 if let Some(scope_id) = resolved_cmd.scope_id {
314 let command_scope = resolved.command_scope.get(&scope_id).unwrap();
315
316 let command_scope_entry = self
317 .scope_manager
318 .command_scope
319 .entry(scope_id)
320 .or_default();
321 command_scope_entry
322 .allow
323 .extend(command_scope.allow.clone());
324 command_scope_entry.deny.extend(command_scope.deny.clone());
325
326 self
327 .scope_manager
328 .command_cache
329 .insert(scope_id, StateManager::new());
330 }
331 }
332
333 let entry = self.allowed_commands.entry(cmd_key).or_default();
334 entry.extend(resolved_cmds);
335 }
336
337 Ok(())
338 }
339
340 #[cfg(debug_assertions)]
341 pub(crate) fn resolve_access_message(
342 &self,
343 key: &str,
344 command_name: &str,
345 window: &str,
346 webview: &str,
347 origin: &Origin,
348 ) -> String {
349 fn print_references(resolved: &[ResolvedCommand]) -> String {
350 resolved
351 .iter()
352 .map(|r| {
353 format!(
354 "capability: {}, permission: {}",
355 r.referenced_by.capability, r.referenced_by.permission
356 )
357 })
358 .collect::<Vec<_>>()
359 .join(" || ")
360 }
361
362 fn print_allowed_on(resolved: &[ResolvedCommand]) -> String {
363 if resolved.is_empty() {
364 "command not allowed on any window/webview/URL context".to_string()
365 } else {
366 let mut s = "allowed on: ".to_string();
367
368 let last_index = resolved.len() - 1;
369 for (index, cmd) in resolved.iter().enumerate() {
370 let windows = cmd
371 .windows
372 .iter()
373 .map(|w| format!("\"{}\"", w.as_str()))
374 .collect::<Vec<_>>()
375 .join(", ");
376 let webviews = cmd
377 .webviews
378 .iter()
379 .map(|w| format!("\"{}\"", w.as_str()))
380 .collect::<Vec<_>>()
381 .join(", ");
382
383 s.push('[');
384
385 if !windows.is_empty() {
386 s.push_str(&format!("windows: {windows}, "));
387 }
388
389 if !webviews.is_empty() {
390 s.push_str(&format!("webviews: {webviews}, "));
391 }
392
393 match &cmd.context {
394 ExecutionContext::Local => s.push_str("URL: local"),
395 ExecutionContext::Remote { url } => s.push_str(&format!("URL: {}", url.as_str())),
396 }
397
398 s.push(']');
399
400 if index != last_index {
401 s.push_str(", ");
402 }
403 }
404
405 s
406 }
407 }
408
409 fn has_permissions_allowing_command(
410 manifest: &crate::utils::acl::manifest::Manifest,
411 set: &crate::utils::acl::PermissionSet,
412 command: &str,
413 ) -> bool {
414 for permission_id in &set.permissions {
415 if permission_id == "default" {
416 if let Some(default) = &manifest.default_permission {
417 if has_permissions_allowing_command(manifest, default, command) {
418 return true;
419 }
420 }
421 } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) {
422 if has_permissions_allowing_command(manifest, ref_set, command) {
423 return true;
424 }
425 } else if let Some(permission) = manifest.permissions.get(permission_id) {
426 if permission.commands.allow.contains(&command.into()) {
427 return true;
428 }
429 }
430 }
431 false
432 }
433
434 let command = if key == APP_ACL_KEY {
435 command_name.to_string()
436 } else {
437 format!("plugin:{key}|{command_name}")
438 };
439
440 let command_pretty_name = if key == APP_ACL_KEY {
441 command_name.to_string()
442 } else {
443 format!("{key}.{command_name}")
444 };
445
446 if let Some(resolved) = self.denied_commands.get(&command) {
447 format!(
448 "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}",
449 print_references(resolved)
450 )
451 } else {
452 let command_matches = self.allowed_commands.get(&command);
453
454 if let Some(resolved) = self.allowed_commands.get(&command) {
455 let resolved_matching_origin = resolved
456 .iter()
457 .filter(|cmd| origin.matches(&cmd.context))
458 .collect::<Vec<&ResolvedCommand>>();
459 if resolved_matching_origin
460 .iter()
461 .any(|cmd| cmd.webviews.iter().any(|w| w.matches(webview)))
462 || resolved_matching_origin
463 .iter()
464 .any(|cmd| cmd.windows.iter().any(|w| w.matches(window)))
465 {
466 "allowed".to_string()
467 } else {
468 format!("{command_pretty_name} not allowed on window \"{window}\", webview \"{webview}\", URL: {}\n\n{}\n\nreferenced by: {}",
469 match origin {
470 Origin::Local => "local",
471 Origin::Remote { url } => url.as_str()
472 },
473 print_allowed_on(resolved),
474 print_references(resolved)
475 )
476 }
477 } else {
478 let permission_error_detail = if let Some((key, manifest)) = self
479 .acl
480 .get_key_value(key)
481 .or_else(|| self.acl.get_key_value(&format!("core:{key}")))
482 {
483 let mut permissions_referencing_command = Vec::new();
484
485 if let Some(default) = &manifest.default_permission {
486 if has_permissions_allowing_command(manifest, default, command_name) {
487 permissions_referencing_command.push("default".into());
488 }
489 }
490 for set in manifest.permission_sets.values() {
491 if has_permissions_allowing_command(manifest, set, command_name) {
492 permissions_referencing_command.push(set.identifier.clone());
493 }
494 }
495 for permission in manifest.permissions.values() {
496 if permission.commands.allow.contains(&command_name.into()) {
497 permissions_referencing_command.push(permission.identifier.clone());
498 }
499 }
500
501 permissions_referencing_command.sort();
502
503 let associated_permissions = permissions_referencing_command
504 .into_iter()
505 .map(|permission| {
506 if key == APP_ACL_KEY {
507 permission
508 } else {
509 format!("{key}:{permission}")
510 }
511 })
512 .collect::<Vec<_>>()
513 .join(", ");
514
515 if associated_permissions.is_empty() {
516 "Command not found".to_string()
517 } else {
518 format!("Permissions associated with this command: {associated_permissions}")
519 }
520 } else {
521 "Plugin not found".to_string()
522 };
523
524 if let Some(resolved_cmds) = command_matches {
525 format!(
526 "{command_pretty_name} not allowed on origin [{origin}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}",
527 resolved_cmds
528 .iter()
529 .map(|resolved| {
530 let context = match &resolved.context {
531 ExecutionContext::Local => "[local]".to_string(),
532 ExecutionContext::Remote { url } => format!("[remote: {}]", url.as_str()),
533 };
534 format!(
535 "- context: {context}, referenced by: capability: {}, permission: {}",
536 resolved.referenced_by.capability,
537 resolved.referenced_by.permission
538 )
539 })
540 .collect::<Vec<_>>()
541 .join("\n")
542 )
543 } else {
544 format!("{command_pretty_name} not allowed. {permission_error_detail}")
545 }
546 }
547 }
548 }
549
550 pub fn resolve_access(
552 &self,
553 command: &str,
554 window: &str,
555 webview: &str,
556 origin: &Origin,
557 ) -> Option<Vec<ResolvedCommand>> {
558 if self
559 .denied_commands
560 .get(command)
561 .map(|resolved| resolved.iter().any(|cmd| origin.matches(&cmd.context)))
562 .is_some()
563 {
564 None
565 } else {
566 self.allowed_commands.get(command).and_then(|resolved| {
567 let resolved_cmds = resolved
568 .iter()
569 .filter(|cmd| {
570 origin.matches(&cmd.context)
571 && (cmd.webviews.iter().any(|w| w.matches(webview))
572 || cmd.windows.iter().any(|w| w.matches(window)))
573 })
574 .cloned()
575 .collect::<Vec<_>>();
576 if resolved_cmds.is_empty() {
577 None
578 } else {
579 Some(resolved_cmds)
580 }
581 })
582 }
583 }
584}
585
586#[derive(Debug)]
588pub struct ScopeValue<T: ScopeObject> {
589 allow: Arc<Vec<Arc<T>>>,
590 deny: Arc<Vec<Arc<T>>>,
591}
592
593impl<T: ScopeObject> ScopeValue<T> {
594 fn clone(&self) -> Self {
595 Self {
596 allow: self.allow.clone(),
597 deny: self.deny.clone(),
598 }
599 }
600
601 pub fn allows(&self) -> &Vec<Arc<T>> {
603 &self.allow
604 }
605
606 pub fn denies(&self) -> &Vec<Arc<T>> {
608 &self.deny
609 }
610}
611
612#[derive(Debug)]
614pub struct CommandScope<T: ScopeObject> {
615 allow: Vec<Arc<T>>,
616 deny: Vec<Arc<T>>,
617}
618
619impl<T: ScopeObject> CommandScope<T> {
620 pub(crate) fn resolve<R: Runtime>(
621 webview: &Webview<R>,
622 scope_ids: Vec<u64>,
623 ) -> crate::Result<Self> {
624 let mut allow = Vec::new();
625 let mut deny = Vec::new();
626
627 for scope_id in scope_ids {
628 let scope = webview
629 .manager()
630 .runtime_authority
631 .lock()
632 .unwrap()
633 .scope_manager
634 .get_command_scope_typed::<R, T>(webview.app_handle(), &scope_id)?;
635
636 for s in scope.allows() {
637 allow.push(s.clone());
638 }
639 for s in scope.denies() {
640 deny.push(s.clone());
641 }
642 }
643
644 Ok(CommandScope { allow, deny })
645 }
646
647 pub fn allows(&self) -> &Vec<Arc<T>> {
649 &self.allow
650 }
651
652 pub fn denies(&self) -> &Vec<Arc<T>> {
654 &self.deny
655 }
656}
657
658impl<T: ScopeObjectMatch> CommandScope<T> {
659 pub fn matches(&self, input: &T::Input) -> bool {
707 if self.deny.iter().any(|s| s.matches(input)) {
709 return false;
710 }
711
712 if self.allow.is_empty() {
714 true
715 } else {
716 self.allow.iter().any(|s| s.matches(input))
717 }
718 }
719}
720
721impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T> {
722 fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
724 let scope_ids = command.acl.as_ref().map(|resolved| {
725 resolved
726 .iter()
727 .filter_map(|cmd| cmd.scope_id)
728 .collect::<Vec<_>>()
729 });
730 if let Some(scope_ids) = scope_ids {
731 CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into)
732 } else {
733 Ok(CommandScope {
734 allow: Default::default(),
735 deny: Default::default(),
736 })
737 }
738 }
739}
740
741#[derive(Debug)]
743pub struct GlobalScope<T: ScopeObject>(ScopeValue<T>);
744
745impl<T: ScopeObject> GlobalScope<T> {
746 pub(crate) fn resolve<R: Runtime>(webview: &Webview<R>, plugin: &str) -> crate::Result<Self> {
747 webview
748 .manager()
749 .runtime_authority
750 .lock()
751 .unwrap()
752 .scope_manager
753 .get_global_scope_typed(webview.app_handle(), plugin)
754 .map(Self)
755 }
756
757 pub fn allows(&self) -> &Vec<Arc<T>> {
759 &self.0.allow
760 }
761
762 pub fn denies(&self) -> &Vec<Arc<T>> {
764 &self.0.deny
765 }
766}
767
768impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope<T> {
769 fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
771 GlobalScope::resolve(
772 &command.message.webview,
773 command.plugin.unwrap_or(APP_ACL_KEY),
774 )
775 .map_err(InvokeError::from_error)
776 }
777}
778
779#[derive(Debug)]
780pub struct ScopeManager {
781 command_scope: BTreeMap<ScopeKey, ResolvedScope>,
782 global_scope: BTreeMap<String, ResolvedScope>,
783 command_cache: BTreeMap<ScopeKey, StateManager>,
784 global_scope_cache: StateManager,
785}
786
787pub trait ScopeObject: Sized + Send + Sync + Debug + 'static {
792 type Error: std::error::Error + Send + Sync;
794 fn deserialize<R: Runtime>(app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error>;
796}
797
798impl<T: Send + Sync + Debug + DeserializeOwned + 'static> ScopeObject for T {
799 type Error = serde_json::Error;
800 fn deserialize<R: Runtime>(_app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error> {
801 serde_json::from_value(raw.into())
802 }
803}
804
805pub trait ScopeObjectMatch: ScopeObject {
841 type Input: ?Sized;
845
846 fn matches(&self, input: &Self::Input) -> bool;
848}
849
850impl ScopeManager {
851 pub(crate) fn get_global_scope_typed<R: Runtime, T: ScopeObject>(
852 &self,
853 app: &AppHandle<R>,
854 key: &str,
855 ) -> crate::Result<ScopeValue<T>> {
856 match self.global_scope_cache.try_get::<ScopeValue<T>>() {
857 Some(cached) => Ok(cached.inner().clone()),
858 None => {
859 let mut allow = Vec::new();
860 let mut deny = Vec::new();
861
862 if let Some(global_scope) = self.global_scope.get(key) {
863 for allowed in &global_scope.allow {
864 allow
865 .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
866 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
867 )?));
868 }
869 for denied in &global_scope.deny {
870 deny
871 .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
872 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
873 )?));
874 }
875 }
876
877 let scope = ScopeValue {
878 allow: Arc::new(allow),
879 deny: Arc::new(deny),
880 };
881 self.global_scope_cache.set(scope.clone());
882 Ok(scope)
883 }
884 }
885 }
886
887 fn get_command_scope_typed<R: Runtime, T: ScopeObject>(
888 &self,
889 app: &AppHandle<R>,
890 key: &ScopeKey,
891 ) -> crate::Result<ScopeValue<T>> {
892 let cache = self.command_cache.get(key).unwrap();
893 match cache.try_get::<ScopeValue<T>>() {
894 Some(cached) => Ok(cached.inner().clone()),
895 None => {
896 let resolved_scope = self
897 .command_scope
898 .get(key)
899 .unwrap_or_else(|| panic!("missing command scope for key {key}"));
900
901 let mut allow = Vec::new();
902 let mut deny = Vec::new();
903
904 for allowed in &resolved_scope.allow {
905 allow
906 .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
907 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
908 )?));
909 }
910 for denied in &resolved_scope.deny {
911 deny
912 .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
913 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
914 )?));
915 }
916
917 let value = ScopeValue {
918 allow: Arc::new(allow),
919 deny: Arc::new(deny),
920 };
921
922 let _ = cache.set(value.clone());
923 Ok(value)
924 }
925 }
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use glob::Pattern;
932 use tauri_utils::acl::{
933 resolved::{Resolved, ResolvedCommand},
934 ExecutionContext,
935 };
936
937 use crate::ipc::Origin;
938
939 use super::RuntimeAuthority;
940
941 #[test]
942 fn window_glob_pattern_matches() {
943 let command = "my-command";
944 let window = "main-*";
945 let webview = "other-*";
946
947 let resolved_cmd = vec![ResolvedCommand {
948 windows: vec![Pattern::new(window).unwrap()],
949 ..Default::default()
950 }];
951 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
952 .into_iter()
953 .collect();
954
955 let authority = RuntimeAuthority::new(
956 Default::default(),
957 Resolved {
958 allowed_commands,
959 ..Default::default()
960 },
961 );
962
963 assert_eq!(
964 authority.resolve_access(
965 command,
966 &window.replace('*', "something"),
967 webview,
968 &Origin::Local
969 ),
970 Some(resolved_cmd)
971 );
972 }
973
974 #[test]
975 fn webview_glob_pattern_matches() {
976 let command = "my-command";
977 let window = "other-*";
978 let webview = "main-*";
979
980 let resolved_cmd = vec![ResolvedCommand {
981 windows: vec![Pattern::new(window).unwrap()],
982 webviews: vec![Pattern::new(webview).unwrap()],
983 ..Default::default()
984 }];
985 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
986 .into_iter()
987 .collect();
988
989 let authority = RuntimeAuthority::new(
990 Default::default(),
991 Resolved {
992 allowed_commands,
993 ..Default::default()
994 },
995 );
996
997 assert_eq!(
998 authority.resolve_access(
999 command,
1000 window,
1001 &webview.replace('*', "something"),
1002 &Origin::Local
1003 ),
1004 Some(resolved_cmd)
1005 );
1006 }
1007
1008 #[test]
1009 fn remote_domain_matches() {
1010 let url = "https://tauri.app";
1011 let command = "my-command";
1012 let window = "main";
1013 let webview = "main";
1014
1015 let resolved_cmd = vec![ResolvedCommand {
1016 windows: vec![Pattern::new(window).unwrap()],
1017 context: ExecutionContext::Remote {
1018 url: url.parse().unwrap(),
1019 },
1020 ..Default::default()
1021 }];
1022 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
1023 .into_iter()
1024 .collect();
1025
1026 let authority = RuntimeAuthority::new(
1027 Default::default(),
1028 Resolved {
1029 allowed_commands,
1030 ..Default::default()
1031 },
1032 );
1033
1034 assert_eq!(
1035 authority.resolve_access(
1036 command,
1037 window,
1038 webview,
1039 &Origin::Remote {
1040 url: url.parse().unwrap()
1041 }
1042 ),
1043 Some(resolved_cmd)
1044 );
1045 }
1046
1047 #[test]
1048 fn remote_domain_glob_pattern_matches() {
1049 let url = "http://tauri.*";
1050 let command = "my-command";
1051 let window = "main";
1052 let webview = "main";
1053
1054 let resolved_cmd = vec![ResolvedCommand {
1055 windows: vec![Pattern::new(window).unwrap()],
1056 context: ExecutionContext::Remote {
1057 url: url.parse().unwrap(),
1058 },
1059 ..Default::default()
1060 }];
1061 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
1062 .into_iter()
1063 .collect();
1064
1065 let authority = RuntimeAuthority::new(
1066 Default::default(),
1067 Resolved {
1068 allowed_commands,
1069 ..Default::default()
1070 },
1071 );
1072
1073 assert_eq!(
1074 authority.resolve_access(
1075 command,
1076 window,
1077 webview,
1078 &Origin::Remote {
1079 url: url.replace('*', "studio").parse().unwrap()
1080 }
1081 ),
1082 Some(resolved_cmd)
1083 );
1084 }
1085
1086 #[test]
1087 fn remote_context_denied() {
1088 let command = "my-command";
1089 let window = "main";
1090 let webview = "main";
1091
1092 let resolved_cmd = vec![ResolvedCommand {
1093 windows: vec![Pattern::new(window).unwrap()],
1094 ..Default::default()
1095 }];
1096 let allowed_commands = [(command.to_string(), resolved_cmd)].into_iter().collect();
1097
1098 let authority = RuntimeAuthority::new(
1099 Default::default(),
1100 Resolved {
1101 allowed_commands,
1102 ..Default::default()
1103 },
1104 );
1105
1106 assert!(authority
1107 .resolve_access(
1108 command,
1109 window,
1110 webview,
1111 &Origin::Remote {
1112 url: "https://tauri.app".parse().unwrap()
1113 }
1114 )
1115 .is_none());
1116 }
1117
1118 #[test]
1119 fn denied_command_takes_precendence() {
1120 let command = "my-command";
1121 let window = "main";
1122 let webview = "main";
1123 let windows = vec![Pattern::new(window).unwrap()];
1124 let allowed_commands = [(
1125 command.to_string(),
1126 vec![ResolvedCommand {
1127 windows: windows.clone(),
1128 ..Default::default()
1129 }],
1130 )]
1131 .into_iter()
1132 .collect();
1133 let denied_commands = [(
1134 command.to_string(),
1135 vec![ResolvedCommand {
1136 windows,
1137 ..Default::default()
1138 }],
1139 )]
1140 .into_iter()
1141 .collect();
1142
1143 let authority = RuntimeAuthority::new(
1144 Default::default(),
1145 Resolved {
1146 allowed_commands,
1147 denied_commands,
1148 ..Default::default()
1149 },
1150 );
1151
1152 assert!(authority
1153 .resolve_access(command, window, webview, &Origin::Local)
1154 .is_none());
1155 }
1156
1157 #[cfg(debug_assertions)]
1158 #[test]
1159 fn resolve_access_message() {
1160 use tauri_utils::acl::manifest::Manifest;
1161
1162 let plugin_name = "myplugin";
1163 let command_allowed_on_window = "my-command-window";
1164 let command_allowed_on_webview_window = "my-command-webview-window";
1165 let window = "main-*";
1166 let webview = "webview-*";
1167 let remote_url = "http://localhost:8080";
1168
1169 let referenced_by = tauri_utils::acl::resolved::ResolvedCommandReference {
1170 capability: "maincap".to_string(),
1171 permission: "allow-command".to_string(),
1172 };
1173
1174 let resolved_window_cmd = ResolvedCommand {
1175 windows: vec![Pattern::new(window).unwrap()],
1176 referenced_by: referenced_by.clone(),
1177 ..Default::default()
1178 };
1179 let resolved_webview_window_cmd = ResolvedCommand {
1180 windows: vec![Pattern::new(window).unwrap()],
1181 webviews: vec![Pattern::new(webview).unwrap()],
1182 referenced_by: referenced_by.clone(),
1183 ..Default::default()
1184 };
1185 let resolved_webview_window_remote_cmd = ResolvedCommand {
1186 windows: vec![Pattern::new(window).unwrap()],
1187 webviews: vec![Pattern::new(webview).unwrap()],
1188 referenced_by,
1189 context: ExecutionContext::Remote {
1190 url: remote_url.parse().unwrap(),
1191 },
1192 ..Default::default()
1193 };
1194
1195 let allowed_commands = [
1196 (
1197 format!("plugin:{plugin_name}|{command_allowed_on_window}"),
1198 vec![resolved_window_cmd],
1199 ),
1200 (
1201 format!("plugin:{plugin_name}|{command_allowed_on_webview_window}"),
1202 vec![
1203 resolved_webview_window_cmd,
1204 resolved_webview_window_remote_cmd,
1205 ],
1206 ),
1207 ]
1208 .into_iter()
1209 .collect();
1210
1211 let authority = RuntimeAuthority::new(
1212 [(
1213 plugin_name.to_string(),
1214 Manifest {
1215 default_permission: None,
1216 permissions: Default::default(),
1217 permission_sets: Default::default(),
1218 global_scope_schema: None,
1219 },
1220 )]
1221 .into_iter()
1222 .collect(),
1223 Resolved {
1224 allowed_commands,
1225 ..Default::default()
1226 },
1227 );
1228
1229 assert_eq!(
1231 authority.resolve_access_message(
1232 "unknown-plugin",
1233 command_allowed_on_window,
1234 window,
1235 webview,
1236 &Origin::Local
1237 ),
1238 "unknown-plugin.my-command-window not allowed. Plugin not found"
1239 );
1240
1241 assert_eq!(
1243 authority.resolve_access_message(
1244 plugin_name,
1245 "unknown-command",
1246 window,
1247 webview,
1248 &Origin::Local
1249 ),
1250 "myplugin.unknown-command not allowed. Command not found"
1251 );
1252
1253 assert_eq!(
1255 authority.resolve_access_message(
1256 plugin_name,
1257 command_allowed_on_window,
1258 "other-window",
1259 "any-webview",
1260 &Origin::Local
1261 ),
1262 "myplugin.my-command-window not allowed on window \"other-window\", webview \"any-webview\", URL: local\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command"
1263 );
1264
1265 assert_eq!(
1267 authority.resolve_access_message(
1268 plugin_name,
1269 command_allowed_on_window,
1270 window,
1271 "any-webview",
1272 &Origin::Remote {
1273 url: "http://localhst".parse().unwrap()
1274 }
1275 ),
1276 "myplugin.my-command-window not allowed on window \"main-*\", webview \"any-webview\", URL: http://localhst/\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command"
1277 );
1278
1279 assert_eq!(
1281 authority.resolve_access_message(
1282 plugin_name,
1283 command_allowed_on_webview_window,
1284 "other-window",
1285 "other-webview",
1286 &Origin::Local
1287 ),
1288 "myplugin.my-command-webview-window not allowed on window \"other-window\", webview \"other-webview\", URL: local\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command"
1289 );
1290
1291 assert_eq!(
1293 authority.resolve_access_message(
1294 plugin_name,
1295 command_allowed_on_webview_window,
1296 window,
1297 webview,
1298 &Origin::Remote {
1299 url: "http://localhost:123".parse().unwrap()
1300 }
1301 ),
1302 "myplugin.my-command-webview-window not allowed on window \"main-*\", webview \"webview-*\", URL: http://localhost:123/\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command"
1303 );
1304 }
1305}