Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/fix-dynamic-acl-scope-id-collision.md
Original file line number Diff line number Diff line change
@@ -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.
114 changes: 113 additions & 1 deletion crates/tauri/src/ipc/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<T: ScopeObject> {
Expand Down Expand Up @@ -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<_>>(),
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<_>>(),
vec![1, 5]
);
assert_eq!(
resolved.allowed_commands.get("fetch").unwrap()[0].scope_id,
Some(1)
);
}
}