From e7a55b13f297d2c2f2551ab4006df547f419dabc Mon Sep 17 00:00:00 2001 From: Derek Knox Date: Fri, 24 Apr 2026 13:25:20 -0700 Subject: [PATCH] fix(acl): rebase runtime-resolved scope ids past baked ACL --- .../fix-dynamic-acl-scope-id-collision.md | 5 + crates/tauri/src/ipc/authority.rs | 114 +++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .changes/fix-dynamic-acl-scope-id-collision.md diff --git a/.changes/fix-dynamic-acl-scope-id-collision.md b/.changes/fix-dynamic-acl-scope-id-collision.md new file mode 100644 index 000000000000..be147b4799d2 --- /dev/null +++ b/.changes/fix-dynamic-acl-scope-id-collision.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:bug +--- + +Fix runtime-resolved capabilities (feature `dynamic-acl`) colliding with the baked ACL: `Resolved::resolve` restarts `current_scope_id` at 0, so newly-resolved `scope_id`s overlapped existing ones and command scope entries were merged into the wrong plugin's bucket. Rebase by the current max `scope_id` so each runtime-added capability stays isolated. diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs index 8bb81e398f26..12abad8418fb 100644 --- a/crates/tauri/src/ipc/authority.rs +++ b/crates/tauri/src/ipc/authority.rs @@ -171,13 +171,23 @@ impl RuntimeAuthority { } } - let resolved = Resolved::resolve( + let mut resolved = Resolved::resolve( &self.acl, capabilities, tauri_utils::platform::Target::current(), ) .unwrap(); + // Rebase fresh scope_ids past existing ones to avoid collisions on merge. + let scope_id_offset = self + .scope_manager + .command_scope + .keys() + .next_back() + .copied() + .unwrap_or(0); + rebase_scope_ids(&mut resolved, scope_id_offset); + // fill global scope for (plugin, global_scope) in resolved.global_scope { let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default(); @@ -471,6 +481,26 @@ impl RuntimeAuthority { } } +/// Offset every `scope_id` in `resolved` by `offset`; a zero offset is a no-op. +#[cfg(feature = "dynamic-acl")] +fn rebase_scope_ids(resolved: &mut Resolved, offset: ScopeKey) { + if offset == 0 { + return; + } + resolved.command_scope = std::mem::take(&mut resolved.command_scope) + .into_iter() + .map(|(k, v)| (k + offset, v)) + .collect(); + for cmd in resolved + .allowed_commands + .values_mut() + .chain(resolved.denied_commands.values_mut()) + .flatten() + { + cmd.scope_id = cmd.scope_id.map(|id| id + offset); + } +} + /// List of allowed and denied objects that match either the command-specific or plugin global scope criteria. #[derive(Debug)] pub struct ScopeValue { @@ -1190,4 +1220,86 @@ mod tests { "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" ); } + + #[cfg(feature = "dynamic-acl")] + #[test] + fn rebase_scope_ids_shifts_keys_and_scope_ids() { + use tauri_utils::acl::resolved::ResolvedScope; + + let mut resolved = Resolved { + command_scope: [(1, ResolvedScope::default()), (2, ResolvedScope::default())] + .into_iter() + .collect(), + allowed_commands: [( + "fetch".to_string(), + vec![ + ResolvedCommand { + scope_id: Some(1), + ..Default::default() + }, + ResolvedCommand { + scope_id: None, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(), + denied_commands: [( + "fetch".to_string(), + vec![ResolvedCommand { + scope_id: Some(2), + ..Default::default() + }], + )] + .into_iter() + .collect(), + ..Default::default() + }; + + super::rebase_scope_ids(&mut resolved, 10); + + assert_eq!( + resolved.command_scope.keys().copied().collect::>(), + vec![11, 12] + ); + let allowed = resolved.allowed_commands.get("fetch").unwrap(); + assert_eq!(allowed[0].scope_id, Some(11)); + assert_eq!(allowed[1].scope_id, None); + let denied = resolved.denied_commands.get("fetch").unwrap(); + assert_eq!(denied[0].scope_id, Some(12)); + } + + #[cfg(feature = "dynamic-acl")] + #[test] + fn rebase_scope_ids_zero_offset_is_noop() { + use tauri_utils::acl::resolved::ResolvedScope; + + let mut resolved = Resolved { + command_scope: [(1, ResolvedScope::default()), (5, ResolvedScope::default())] + .into_iter() + .collect(), + allowed_commands: [( + "fetch".to_string(), + vec![ResolvedCommand { + scope_id: Some(1), + ..Default::default() + }], + )] + .into_iter() + .collect(), + ..Default::default() + }; + + super::rebase_scope_ids(&mut resolved, 0); + + assert_eq!( + resolved.command_scope.keys().copied().collect::>(), + vec![1, 5] + ); + assert_eq!( + resolved.allowed_commands.get("fetch").unwrap()[0].scope_id, + Some(1) + ); + } }