diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index 0b97ad65f..753c28708 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -437,13 +437,17 @@ fn install(s: &mut Session, p: &Value) -> Result { ), )); } - if m.asks_permissions() { - let g = Grant { id: m.id.clone(), hash: plugin.hash().to_string(), permissions: wanted }; + let new_grant = m.asks_permissions().then(|| Grant { id: m.id.clone(), hash: plugin.hash().to_string(), permissions: wanted }); + if let Some(g) = &new_grant { registry::grant(g.clone()); - s.store_grant(&m.id, Some(g)); } // Installed plug-ins stay installed after a restart (#2541). let stored = s.store_plugin(plugin.id(), &bytes); + // A stored grant names one module: when this one could not replace the stored file, the file + // keeps the grant it has. + if stored && let Some(g) = new_grant { + s.store_grant(&m.id, Some(g)); + } // Installing a disabled plug-in again (an updated version) turns it on for the next launch too, // also when it is disabled by its stored file alone (it stopped a launch while loading). When // the new version could not replace the stored file, the old one stays disabled. diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index b4e05bb94..ef9b35a98 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -801,6 +801,36 @@ fn a_filter_does_only_what_its_permissions_allow() { assert_eq!(s.doc().unwrap().doc.plugin_data(stateless), Some(&json!({"pen": 1}))); } +/// When an update can't replace the stored module, the store's grant stays the one for the module +/// it still holds, so that module loads at the next start. +#[test] +fn a_module_the_store_could_not_take_leaves_the_stored_grant() { + let folder = crate::tests_links::Folder::new("plugin-store-failed-update"); + let id = "test.engine.permissions.unwritable"; + let old = asking(id, r#"["doc.read"]"#, &json!({"objects": []})); + let new = asking(id, r#"["doc.read"]"#, &json!({"objects": [{"name": "x"}]})); + let (old_hash, new_hash) = (vectorcraft_plugins::permissions::sha256_hex(&old), vectorcraft_plugins::permissions::sha256_hex(&new)); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&old), "grant": ["doc.read"]})); + let stored = std::fs::read_dir(&folder.0).unwrap().map(|e| e.unwrap().path()).find(|p| p.extension().is_some_and(|e| e == "wasm")).unwrap(); + // A folder under the module's file name can't be replaced by a file. + std::fs::remove_file(&stored).unwrap(); + std::fs::create_dir(&stored).unwrap(); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&new), "grant": ["doc.read"]})); + let grants = std::fs::read_to_string(folder.0.join("grants.json")).unwrap(); + assert!(grants.contains(&old_hash) && !grants.contains(&new_hash), "{grants}"); + // With the old module back in that place, the next session loads it. + std::fs::remove_dir(&stored).unwrap(); + std::fs::write(&stored, &old).unwrap(); + vectorcraft_plugins::registry::remove(id); + vectorcraft_plugins::registry::revoke(id); + let mut next = session(); + next.attach_plugin_store(folder.0.clone()); + assert_eq!(vectorcraft_plugins::registry::get(id).map(|p| p.hash().to_string()), Some(old_hash)); + run(&mut next, "plugin.remove", json!({"id": id})); +} + /// What the user granted is kept in the plug-in store with the module: the next session installs /// the plug-in again without asking. A stored plug-in that lists permissions nobody granted it is /// skipped, even when another store granted it, and so is one in the Additional Plug-ins Folder