1use std::collections::BTreeMap;
6use std::fmt::{Debug, Display};
7use std::sync::Arc;
8
9use serde::de::DeserializeOwned;
10
11#[cfg(feature = "dynamic-acl")]
12use tauri_utils::acl::capability::CapabilityFile;
13#[cfg(any(feature = "dynamic-acl", debug_assertions))]
14use tauri_utils::acl::manifest::Manifest;
15use tauri_utils::acl::{
16 resolved::{Resolved, ResolvedCommand, ResolvedScope, ScopeKey},
17 ExecutionContext, Value, APP_ACL_KEY,
18};
19
20use url::Url;
21
22use crate::{ipc::InvokeError, sealed::ManagerBase, Runtime};
23use crate::{AppHandle, Manager, StateManager, Webview};
24
25use super::{CommandArg, CommandItem};
26
27pub struct RuntimeAuthority {
29 #[cfg(any(feature = "dynamic-acl", debug_assertions))]
30 acl: BTreeMap<String, Manifest>,
31 has_app_acl: bool,
32 allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
33 denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
34 pub(crate) scope_manager: ScopeManager,
35}
36
37pub enum Origin {
39 Local,
41 Remote {
43 url: Url,
45 },
46}
47
48impl Display for Origin {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::Local => write!(f, "local"),
52 Self::Remote { url } => write!(f, "remote: {url}"),
53 }
54 }
55}
56
57impl Origin {
58 fn matches(&self, context: &ExecutionContext) -> bool {
59 match (self, context) {
60 (Self::Local, ExecutionContext::Local) => true,
61 (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => {
62 url_pattern.test(url)
63 }
64 _ => false,
65 }
66 }
67}
68
69#[cfg(any(feature = "dynamic-acl", debug_assertions))]
78#[doc(hidden)]
79#[macro_export]
80macro_rules! runtime_authority {
81 ($acl:expr, $resolved_acl:expr) => {
82 $crate::ipc::RuntimeAuthority::new($acl, $resolved_acl)
83 };
84}
85
86#[cfg(not(any(feature = "dynamic-acl", debug_assertions)))]
95#[doc(hidden)]
96#[macro_export]
97macro_rules! runtime_authority {
98 ($_acl:expr, $resolved_acl:expr) => {
99 $crate::ipc::RuntimeAuthority::new($resolved_acl)
100 };
101}
102
103impl RuntimeAuthority {
104 #[doc(hidden)]
108 pub fn new(
109 #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap<String, Manifest>,
110 resolved_acl: Resolved,
111 ) -> Self {
112 let command_cache = resolved_acl
113 .command_scope
114 .keys()
115 .map(|key| (*key, StateManager::new()))
116 .collect();
117 Self {
118 #[cfg(any(feature = "dynamic-acl", debug_assertions))]
119 acl,
120 has_app_acl: resolved_acl.has_app_acl,
121 allowed_commands: resolved_acl.allowed_commands,
122 denied_commands: resolved_acl.denied_commands,
123 scope_manager: ScopeManager {
124 command_scope: resolved_acl.command_scope,
125 global_scope: resolved_acl.global_scope,
126 command_cache,
127 global_scope_cache: StateManager::new(),
128 },
129 }
130 }
131
132 pub(crate) fn has_app_manifest(&self) -> bool {
133 self.has_app_acl
134 }
135
136 #[doc(hidden)]
137 pub fn __allow_command(&mut self, command: String, context: ExecutionContext) {
138 self.allowed_commands.insert(
139 command,
140 vec![ResolvedCommand {
141 context,
142 windows: vec!["*".parse().unwrap()],
143 ..Default::default()
144 }],
145 );
146 }
147
148 #[cfg(feature = "dynamic-acl")]
150 pub fn add_capability(&mut self, capability: impl super::RuntimeCapability) -> crate::Result<()> {
151 self.add_capability_inner(capability.build())
152 }
153
154 #[cfg(feature = "dynamic-acl")]
155 fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> {
156 let mut capabilities = BTreeMap::new();
157 match capability {
158 CapabilityFile::Capability(c) => {
159 capabilities.insert(c.identifier.clone(), c);
160 }
161
162 CapabilityFile::List(capabilities_list)
163 | CapabilityFile::NamedList {
164 capabilities: capabilities_list,
165 } => {
166 capabilities.extend(
167 capabilities_list
168 .into_iter()
169 .map(|c| (c.identifier.clone(), c)),
170 );
171 }
172 }
173
174 let resolved = Resolved::resolve(
175 &self.acl,
176 capabilities,
177 tauri_utils::platform::Target::current(),
178 )
179 .unwrap();
180
181 for (plugin, global_scope) in resolved.global_scope {
183 let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default();
184
185 global_scope_entry.allow.extend(global_scope.allow);
186 global_scope_entry.deny.extend(global_scope.deny);
187
188 self.scope_manager.global_scope_cache = StateManager::new();
189 }
190
191 for (cmd_key, resolved_cmds) in resolved.denied_commands {
193 let entry = self.denied_commands.entry(cmd_key).or_default();
194 entry.extend(resolved_cmds);
195 }
196
197 for (cmd_key, resolved_cmds) in resolved.allowed_commands {
199 for resolved_cmd in &resolved_cmds {
201 if let Some(scope_id) = resolved_cmd.scope_id {
202 let command_scope = resolved.command_scope.get(&scope_id).unwrap();
203
204 let command_scope_entry = self
205 .scope_manager
206 .command_scope
207 .entry(scope_id)
208 .or_default();
209 command_scope_entry
210 .allow
211 .extend(command_scope.allow.clone());
212 command_scope_entry.deny.extend(command_scope.deny.clone());
213
214 self
215 .scope_manager
216 .command_cache
217 .insert(scope_id, StateManager::new());
218 }
219 }
220
221 let entry = self.allowed_commands.entry(cmd_key).or_default();
222 entry.extend(resolved_cmds);
223 }
224
225 Ok(())
226 }
227
228 #[cfg(debug_assertions)]
229 pub(crate) fn resolve_access_message(
230 &self,
231 key: &str,
232 command_name: &str,
233 window: &str,
234 webview: &str,
235 origin: &Origin,
236 ) -> String {
237 fn print_references(resolved: &[ResolvedCommand]) -> String {
238 resolved
239 .iter()
240 .map(|r| {
241 format!(
242 "capability: {}, permission: {}",
243 r.referenced_by.capability, r.referenced_by.permission
244 )
245 })
246 .collect::<Vec<_>>()
247 .join(" || ")
248 }
249
250 fn print_allowed_on(resolved: &[ResolvedCommand]) -> String {
251 if resolved.is_empty() {
252 "command not allowed on any window/webview/URL context".to_string()
253 } else {
254 let mut s = "allowed on: ".to_string();
255
256 let last_index = resolved.len() - 1;
257 for (index, cmd) in resolved.iter().enumerate() {
258 let windows = cmd
259 .windows
260 .iter()
261 .map(|w| format!("\"{}\"", w.as_str()))
262 .collect::<Vec<_>>()
263 .join(", ");
264 let webviews = cmd
265 .webviews
266 .iter()
267 .map(|w| format!("\"{}\"", w.as_str()))
268 .collect::<Vec<_>>()
269 .join(", ");
270
271 s.push('[');
272
273 if !windows.is_empty() {
274 s.push_str(&format!("windows: {windows}, "));
275 }
276
277 if !webviews.is_empty() {
278 s.push_str(&format!("webviews: {webviews}, "));
279 }
280
281 match &cmd.context {
282 ExecutionContext::Local => s.push_str("URL: local"),
283 ExecutionContext::Remote { url } => s.push_str(&format!("URL: {}", url.as_str())),
284 }
285
286 s.push(']');
287
288 if index != last_index {
289 s.push_str(", ");
290 }
291 }
292
293 s
294 }
295 }
296
297 fn has_permissions_allowing_command(
298 manifest: &Manifest,
299 set: &crate::utils::acl::PermissionSet,
300 command: &str,
301 ) -> bool {
302 for permission_id in &set.permissions {
303 if permission_id == "default" {
304 if let Some(default) = &manifest.default_permission {
305 if has_permissions_allowing_command(manifest, default, command) {
306 return true;
307 }
308 }
309 } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) {
310 if has_permissions_allowing_command(manifest, ref_set, command) {
311 return true;
312 }
313 } else if let Some(permission) = manifest.permissions.get(permission_id) {
314 if permission.commands.allow.contains(&command.into()) {
315 return true;
316 }
317 }
318 }
319 false
320 }
321
322 let command = if key == APP_ACL_KEY {
323 command_name.to_string()
324 } else {
325 format!("plugin:{key}|{command_name}")
326 };
327
328 let command_pretty_name = if key == APP_ACL_KEY {
329 command_name.to_string()
330 } else {
331 format!("{key}.{command_name}")
332 };
333
334 if let Some(resolved) = self.denied_commands.get(&command) {
335 format!(
336 "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}",
337 print_references(resolved)
338 )
339 } else {
340 let command_matches = self.allowed_commands.get(&command);
341
342 if let Some(resolved) = self.allowed_commands.get(&command) {
343 let resolved_matching_origin = resolved
344 .iter()
345 .filter(|cmd| origin.matches(&cmd.context))
346 .collect::<Vec<&ResolvedCommand>>();
347 if resolved_matching_origin
348 .iter()
349 .any(|cmd| cmd.webviews.iter().any(|w| w.matches(webview)))
350 || resolved_matching_origin
351 .iter()
352 .any(|cmd| cmd.windows.iter().any(|w| w.matches(window)))
353 {
354 "allowed".to_string()
355 } else {
356 format!("{command_pretty_name} not allowed on window \"{window}\", webview \"{webview}\", URL: {}\n\n{}\n\nreferenced by: {}",
357 match origin {
358 Origin::Local => "local",
359 Origin::Remote { url } => url.as_str()
360 },
361 print_allowed_on(resolved),
362 print_references(resolved)
363 )
364 }
365 } else {
366 let permission_error_detail = if let Some((key, manifest)) = self
367 .acl
368 .get_key_value(key)
369 .or_else(|| self.acl.get_key_value(&format!("core:{key}")))
370 {
371 let mut permissions_referencing_command = Vec::new();
372
373 if let Some(default) = &manifest.default_permission {
374 if has_permissions_allowing_command(manifest, default, command_name) {
375 permissions_referencing_command.push("default".into());
376 }
377 }
378 for set in manifest.permission_sets.values() {
379 if has_permissions_allowing_command(manifest, set, command_name) {
380 permissions_referencing_command.push(set.identifier.clone());
381 }
382 }
383 for permission in manifest.permissions.values() {
384 if permission.commands.allow.contains(&command_name.into()) {
385 permissions_referencing_command.push(permission.identifier.clone());
386 }
387 }
388
389 permissions_referencing_command.sort();
390
391 let associated_permissions = permissions_referencing_command
392 .into_iter()
393 .map(|permission| {
394 if key == APP_ACL_KEY {
395 permission
396 } else {
397 format!("{key}:{permission}")
398 }
399 })
400 .collect::<Vec<_>>()
401 .join(", ");
402
403 if associated_permissions.is_empty() {
404 "Command not found".to_string()
405 } else {
406 format!("Permissions associated with this command: {associated_permissions}")
407 }
408 } else {
409 "Plugin not found".to_string()
410 };
411
412 if let Some(resolved_cmds) = command_matches {
413 format!(
414 "{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}",
415 resolved_cmds
416 .iter()
417 .map(|resolved| {
418 let context = match &resolved.context {
419 ExecutionContext::Local => "[local]".to_string(),
420 ExecutionContext::Remote { url } => format!("[remote: {}]", url.as_str()),
421 };
422 format!(
423 "- context: {context}, referenced by: capability: {}, permission: {}",
424 resolved.referenced_by.capability,
425 resolved.referenced_by.permission
426 )
427 })
428 .collect::<Vec<_>>()
429 .join("\n")
430 )
431 } else {
432 format!("{command_pretty_name} not allowed. {permission_error_detail}")
433 }
434 }
435 }
436 }
437
438 pub fn resolve_access(
440 &self,
441 command: &str,
442 window: &str,
443 webview: &str,
444 origin: &Origin,
445 ) -> Option<Vec<ResolvedCommand>> {
446 if self
447 .denied_commands
448 .get(command)
449 .map(|resolved| resolved.iter().any(|cmd| origin.matches(&cmd.context)))
450 .is_some()
451 {
452 None
453 } else {
454 self.allowed_commands.get(command).and_then(|resolved| {
455 let resolved_cmds = resolved
456 .iter()
457 .filter(|cmd| {
458 origin.matches(&cmd.context)
459 && (cmd.webviews.iter().any(|w| w.matches(webview))
460 || cmd.windows.iter().any(|w| w.matches(window)))
461 })
462 .cloned()
463 .collect::<Vec<_>>();
464 if resolved_cmds.is_empty() {
465 None
466 } else {
467 Some(resolved_cmds)
468 }
469 })
470 }
471 }
472}
473
474#[derive(Debug)]
476pub struct ScopeValue<T: ScopeObject> {
477 allow: Arc<Vec<Arc<T>>>,
478 deny: Arc<Vec<Arc<T>>>,
479}
480
481impl<T: ScopeObject> ScopeValue<T> {
482 fn clone(&self) -> Self {
483 Self {
484 allow: self.allow.clone(),
485 deny: self.deny.clone(),
486 }
487 }
488
489 pub fn allows(&self) -> &Vec<Arc<T>> {
491 &self.allow
492 }
493
494 pub fn denies(&self) -> &Vec<Arc<T>> {
496 &self.deny
497 }
498}
499
500#[derive(Debug)]
502pub struct CommandScope<T: ScopeObject> {
503 allow: Vec<Arc<T>>,
504 deny: Vec<Arc<T>>,
505}
506
507impl<T: ScopeObject> CommandScope<T> {
508 pub(crate) fn resolve<R: Runtime>(
509 webview: &Webview<R>,
510 scope_ids: Vec<u64>,
511 ) -> crate::Result<Self> {
512 let mut allow = Vec::new();
513 let mut deny = Vec::new();
514
515 for scope_id in scope_ids {
516 let scope = webview
517 .manager()
518 .runtime_authority
519 .lock()
520 .unwrap()
521 .scope_manager
522 .get_command_scope_typed::<R, T>(webview.app_handle(), &scope_id)?;
523
524 for s in scope.allows() {
525 allow.push(s.clone());
526 }
527 for s in scope.denies() {
528 deny.push(s.clone());
529 }
530 }
531
532 Ok(CommandScope { allow, deny })
533 }
534
535 pub fn allows(&self) -> &Vec<Arc<T>> {
537 &self.allow
538 }
539
540 pub fn denies(&self) -> &Vec<Arc<T>> {
542 &self.deny
543 }
544}
545
546impl<T: ScopeObjectMatch> CommandScope<T> {
547 pub fn matches(&self, input: &T::Input) -> bool {
595 if self.deny.iter().any(|s| s.matches(input)) {
597 return false;
598 }
599
600 if self.allow.is_empty() {
602 true
603 } else {
604 self.allow.iter().any(|s| s.matches(input))
605 }
606 }
607}
608
609impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope<T> {
610 fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
612 let scope_ids = command.acl.as_ref().map(|resolved| {
613 resolved
614 .iter()
615 .filter_map(|cmd| cmd.scope_id)
616 .collect::<Vec<_>>()
617 });
618 if let Some(scope_ids) = scope_ids {
619 CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into)
620 } else {
621 Ok(CommandScope {
622 allow: Default::default(),
623 deny: Default::default(),
624 })
625 }
626 }
627}
628
629#[derive(Debug)]
631pub struct GlobalScope<T: ScopeObject>(ScopeValue<T>);
632
633impl<T: ScopeObject> GlobalScope<T> {
634 pub(crate) fn resolve<R: Runtime>(webview: &Webview<R>, plugin: &str) -> crate::Result<Self> {
635 webview
636 .manager()
637 .runtime_authority
638 .lock()
639 .unwrap()
640 .scope_manager
641 .get_global_scope_typed(webview.app_handle(), plugin)
642 .map(Self)
643 }
644
645 pub fn allows(&self) -> &Vec<Arc<T>> {
647 &self.0.allow
648 }
649
650 pub fn denies(&self) -> &Vec<Arc<T>> {
652 &self.0.deny
653 }
654}
655
656impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope<T> {
657 fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
659 GlobalScope::resolve(
660 &command.message.webview,
661 command.plugin.unwrap_or(APP_ACL_KEY),
662 )
663 .map_err(InvokeError::from_error)
664 }
665}
666
667#[derive(Debug)]
668pub struct ScopeManager {
669 command_scope: BTreeMap<ScopeKey, ResolvedScope>,
670 global_scope: BTreeMap<String, ResolvedScope>,
671 command_cache: BTreeMap<ScopeKey, StateManager>,
672 global_scope_cache: StateManager,
673}
674
675pub trait ScopeObject: Sized + Send + Sync + Debug + 'static {
680 type Error: std::error::Error + Send + Sync;
682 fn deserialize<R: Runtime>(app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error>;
684}
685
686impl<T: Send + Sync + Debug + DeserializeOwned + 'static> ScopeObject for T {
687 type Error = serde_json::Error;
688 fn deserialize<R: Runtime>(_app: &AppHandle<R>, raw: Value) -> Result<Self, Self::Error> {
689 serde_json::from_value(raw.into())
690 }
691}
692
693pub trait ScopeObjectMatch: ScopeObject {
729 type Input: ?Sized;
733
734 fn matches(&self, input: &Self::Input) -> bool;
736}
737
738impl ScopeManager {
739 pub(crate) fn get_global_scope_typed<R: Runtime, T: ScopeObject>(
740 &self,
741 app: &AppHandle<R>,
742 key: &str,
743 ) -> crate::Result<ScopeValue<T>> {
744 match self.global_scope_cache.try_get::<ScopeValue<T>>() {
745 Some(cached) => Ok(cached.inner().clone()),
746 None => {
747 let mut allow = Vec::new();
748 let mut deny = Vec::new();
749
750 if let Some(global_scope) = self.global_scope.get(key) {
751 for allowed in &global_scope.allow {
752 allow
753 .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
754 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
755 )?));
756 }
757 for denied in &global_scope.deny {
758 deny
759 .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
760 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
761 )?));
762 }
763 }
764
765 let scope = ScopeValue {
766 allow: Arc::new(allow),
767 deny: Arc::new(deny),
768 };
769 self.global_scope_cache.set(scope.clone());
770 Ok(scope)
771 }
772 }
773 }
774
775 fn get_command_scope_typed<R: Runtime, T: ScopeObject>(
776 &self,
777 app: &AppHandle<R>,
778 key: &ScopeKey,
779 ) -> crate::Result<ScopeValue<T>> {
780 let cache = self.command_cache.get(key).unwrap();
781 match cache.try_get::<ScopeValue<T>>() {
782 Some(cached) => Ok(cached.inner().clone()),
783 None => {
784 let resolved_scope = self
785 .command_scope
786 .get(key)
787 .unwrap_or_else(|| panic!("missing command scope for key {key}"));
788
789 let mut allow = Vec::new();
790 let mut deny = Vec::new();
791
792 for allowed in &resolved_scope.allow {
793 allow
794 .push(Arc::new(T::deserialize(app, allowed.clone()).map_err(
795 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
796 )?));
797 }
798 for denied in &resolved_scope.deny {
799 deny
800 .push(Arc::new(T::deserialize(app, denied.clone()).map_err(
801 |e| crate::Error::CannotDeserializeScope(Box::new(e)),
802 )?));
803 }
804
805 let value = ScopeValue {
806 allow: Arc::new(allow),
807 deny: Arc::new(deny),
808 };
809
810 let _ = cache.set(value.clone());
811 Ok(value)
812 }
813 }
814 }
815}
816
817#[cfg(test)]
818mod tests {
819 use glob::Pattern;
820 use tauri_utils::acl::{
821 resolved::{Resolved, ResolvedCommand},
822 ExecutionContext,
823 };
824
825 use crate::ipc::Origin;
826
827 use super::RuntimeAuthority;
828
829 #[test]
830 fn window_glob_pattern_matches() {
831 let command = "my-command";
832 let window = "main-*";
833 let webview = "other-*";
834
835 let resolved_cmd = vec![ResolvedCommand {
836 windows: vec![Pattern::new(window).unwrap()],
837 ..Default::default()
838 }];
839 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
840 .into_iter()
841 .collect();
842
843 let authority = RuntimeAuthority::new(
844 Default::default(),
845 Resolved {
846 allowed_commands,
847 ..Default::default()
848 },
849 );
850
851 assert_eq!(
852 authority.resolve_access(
853 command,
854 &window.replace('*', "something"),
855 webview,
856 &Origin::Local
857 ),
858 Some(resolved_cmd)
859 );
860 }
861
862 #[test]
863 fn webview_glob_pattern_matches() {
864 let command = "my-command";
865 let window = "other-*";
866 let webview = "main-*";
867
868 let resolved_cmd = vec![ResolvedCommand {
869 windows: vec![Pattern::new(window).unwrap()],
870 webviews: vec![Pattern::new(webview).unwrap()],
871 ..Default::default()
872 }];
873 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
874 .into_iter()
875 .collect();
876
877 let authority = RuntimeAuthority::new(
878 Default::default(),
879 Resolved {
880 allowed_commands,
881 ..Default::default()
882 },
883 );
884
885 assert_eq!(
886 authority.resolve_access(
887 command,
888 window,
889 &webview.replace('*', "something"),
890 &Origin::Local
891 ),
892 Some(resolved_cmd)
893 );
894 }
895
896 #[test]
897 fn remote_domain_matches() {
898 let url = "https://tauri.app";
899 let command = "my-command";
900 let window = "main";
901 let webview = "main";
902
903 let resolved_cmd = vec![ResolvedCommand {
904 windows: vec![Pattern::new(window).unwrap()],
905 context: ExecutionContext::Remote {
906 url: url.parse().unwrap(),
907 },
908 ..Default::default()
909 }];
910 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
911 .into_iter()
912 .collect();
913
914 let authority = RuntimeAuthority::new(
915 Default::default(),
916 Resolved {
917 allowed_commands,
918 ..Default::default()
919 },
920 );
921
922 assert_eq!(
923 authority.resolve_access(
924 command,
925 window,
926 webview,
927 &Origin::Remote {
928 url: url.parse().unwrap()
929 }
930 ),
931 Some(resolved_cmd)
932 );
933 }
934
935 #[test]
936 fn remote_domain_glob_pattern_matches() {
937 let url = "http://tauri.*";
938 let command = "my-command";
939 let window = "main";
940 let webview = "main";
941
942 let resolved_cmd = vec![ResolvedCommand {
943 windows: vec![Pattern::new(window).unwrap()],
944 context: ExecutionContext::Remote {
945 url: url.parse().unwrap(),
946 },
947 ..Default::default()
948 }];
949 let allowed_commands = [(command.to_string(), resolved_cmd.clone())]
950 .into_iter()
951 .collect();
952
953 let authority = RuntimeAuthority::new(
954 Default::default(),
955 Resolved {
956 allowed_commands,
957 ..Default::default()
958 },
959 );
960
961 assert_eq!(
962 authority.resolve_access(
963 command,
964 window,
965 webview,
966 &Origin::Remote {
967 url: url.replace('*', "studio").parse().unwrap()
968 }
969 ),
970 Some(resolved_cmd)
971 );
972 }
973
974 #[test]
975 fn remote_context_denied() {
976 let command = "my-command";
977 let window = "main";
978 let webview = "main";
979
980 let resolved_cmd = vec![ResolvedCommand {
981 windows: vec![Pattern::new(window).unwrap()],
982 ..Default::default()
983 }];
984 let allowed_commands = [(command.to_string(), resolved_cmd)].into_iter().collect();
985
986 let authority = RuntimeAuthority::new(
987 Default::default(),
988 Resolved {
989 allowed_commands,
990 ..Default::default()
991 },
992 );
993
994 assert!(authority
995 .resolve_access(
996 command,
997 window,
998 webview,
999 &Origin::Remote {
1000 url: "https://tauri.app".parse().unwrap()
1001 }
1002 )
1003 .is_none());
1004 }
1005
1006 #[test]
1007 fn denied_command_takes_precendence() {
1008 let command = "my-command";
1009 let window = "main";
1010 let webview = "main";
1011 let windows = vec![Pattern::new(window).unwrap()];
1012 let allowed_commands = [(
1013 command.to_string(),
1014 vec![ResolvedCommand {
1015 windows: windows.clone(),
1016 ..Default::default()
1017 }],
1018 )]
1019 .into_iter()
1020 .collect();
1021 let denied_commands = [(
1022 command.to_string(),
1023 vec![ResolvedCommand {
1024 windows,
1025 ..Default::default()
1026 }],
1027 )]
1028 .into_iter()
1029 .collect();
1030
1031 let authority = RuntimeAuthority::new(
1032 Default::default(),
1033 Resolved {
1034 allowed_commands,
1035 denied_commands,
1036 ..Default::default()
1037 },
1038 );
1039
1040 assert!(authority
1041 .resolve_access(command, window, webview, &Origin::Local)
1042 .is_none());
1043 }
1044
1045 #[cfg(debug_assertions)]
1046 #[test]
1047 fn resolve_access_message() {
1048 use tauri_utils::acl::manifest::Manifest;
1049
1050 let plugin_name = "myplugin";
1051 let command_allowed_on_window = "my-command-window";
1052 let command_allowed_on_webview_window = "my-command-webview-window";
1053 let window = "main-*";
1054 let webview = "webview-*";
1055 let remote_url = "http://localhost:8080";
1056
1057 let referenced_by = tauri_utils::acl::resolved::ResolvedCommandReference {
1058 capability: "maincap".to_string(),
1059 permission: "allow-command".to_string(),
1060 };
1061
1062 let resolved_window_cmd = ResolvedCommand {
1063 windows: vec![Pattern::new(window).unwrap()],
1064 referenced_by: referenced_by.clone(),
1065 ..Default::default()
1066 };
1067 let resolved_webview_window_cmd = ResolvedCommand {
1068 windows: vec![Pattern::new(window).unwrap()],
1069 webviews: vec![Pattern::new(webview).unwrap()],
1070 referenced_by: referenced_by.clone(),
1071 ..Default::default()
1072 };
1073 let resolved_webview_window_remote_cmd = ResolvedCommand {
1074 windows: vec![Pattern::new(window).unwrap()],
1075 webviews: vec![Pattern::new(webview).unwrap()],
1076 referenced_by,
1077 context: ExecutionContext::Remote {
1078 url: remote_url.parse().unwrap(),
1079 },
1080 ..Default::default()
1081 };
1082
1083 let allowed_commands = [
1084 (
1085 format!("plugin:{plugin_name}|{command_allowed_on_window}"),
1086 vec![resolved_window_cmd],
1087 ),
1088 (
1089 format!("plugin:{plugin_name}|{command_allowed_on_webview_window}"),
1090 vec![
1091 resolved_webview_window_cmd,
1092 resolved_webview_window_remote_cmd,
1093 ],
1094 ),
1095 ]
1096 .into_iter()
1097 .collect();
1098
1099 let authority = RuntimeAuthority::new(
1100 [(
1101 plugin_name.to_string(),
1102 Manifest {
1103 default_permission: None,
1104 permissions: Default::default(),
1105 permission_sets: Default::default(),
1106 global_scope_schema: None,
1107 },
1108 )]
1109 .into_iter()
1110 .collect(),
1111 Resolved {
1112 allowed_commands,
1113 ..Default::default()
1114 },
1115 );
1116
1117 assert_eq!(
1119 authority.resolve_access_message(
1120 "unknown-plugin",
1121 command_allowed_on_window,
1122 window,
1123 webview,
1124 &Origin::Local
1125 ),
1126 "unknown-plugin.my-command-window not allowed. Plugin not found"
1127 );
1128
1129 assert_eq!(
1131 authority.resolve_access_message(
1132 plugin_name,
1133 "unknown-command",
1134 window,
1135 webview,
1136 &Origin::Local
1137 ),
1138 "myplugin.unknown-command not allowed. Command not found"
1139 );
1140
1141 assert_eq!(
1143 authority.resolve_access_message(
1144 plugin_name,
1145 command_allowed_on_window,
1146 "other-window",
1147 "any-webview",
1148 &Origin::Local
1149 ),
1150 "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"
1151 );
1152
1153 assert_eq!(
1155 authority.resolve_access_message(
1156 plugin_name,
1157 command_allowed_on_window,
1158 window,
1159 "any-webview",
1160 &Origin::Remote {
1161 url: "http://localhst".parse().unwrap()
1162 }
1163 ),
1164 "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"
1165 );
1166
1167 assert_eq!(
1169 authority.resolve_access_message(
1170 plugin_name,
1171 command_allowed_on_webview_window,
1172 "other-window",
1173 "other-webview",
1174 &Origin::Local
1175 ),
1176 "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"
1177 );
1178
1179 assert_eq!(
1181 authority.resolve_access_message(
1182 plugin_name,
1183 command_allowed_on_webview_window,
1184 window,
1185 webview,
1186 &Origin::Remote {
1187 url: "http://localhost:123".parse().unwrap()
1188 }
1189 ),
1190 "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"
1191 );
1192 }
1193}