diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index c8bcc8964..75dd5d497 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -386,10 +386,14 @@ fn install(s: &mut Session, p: &Value) -> Result { } else { // This module takes the id over, so nothing the user approved for the id applies any more. registry::revoke(&m.id); - s.store_grant(&m.id, None); } // Installed plug-ins stay installed after a restart (#2541). let stored = s.store_plugin(plugin.id(), &bytes); + // When the write fails, the old stored module stays in the folder and needs its grant at the + // next launch. + if stored && !m.asks_permissions() { + s.store_grant(&m.id, None); + } // 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 5425b9d31..d11f13bee 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -735,6 +735,32 @@ fn a_module_without_permissions_ends_the_grant_kept_in_the_store() { run(&mut fourth, "plugin.remove", json!({"id": other})); } +/// A module listing no permissions that can't be written to the plug-in store leaves the stored +/// module in place, so `grants.json` keeps the grant that module needs at the next launch. +#[test] +fn a_failed_store_write_keeps_the_stored_grant() { + let folder = crate::tests_links::Folder::new("plugin-store-failed-write"); + let id = "test.engine.permissions.storefail"; + let empty = json!({"objects": []}); + let approved = asking(id, r#"["doc.read"]"#, &empty); + let plain = asking(id, "null", &empty); + let grants = folder.0.join("grants.json"); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&approved), "grant": ["doc.read"]})); + let before = std::fs::read(&grants).unwrap(); + let file = s.stored_plugin_file(id).unwrap(); + std::fs::remove_file(&file).unwrap(); + std::fs::create_dir(&file).unwrap(); + + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&plain)})); + assert_eq!(std::fs::read(&grants).unwrap(), before, "the grant stays with the module left in the store"); + assert!(vectorcraft_plugins::registry::granted(id).is_none(), "the process runs the module that lists nothing"); + + std::fs::remove_dir(&file).unwrap(); + run(&mut s, "plugin.remove", json!({"id": id})); +} + /// A plug-in folder that leaves a module listing no permissions under an id forgets the process's /// grant for it, unless a later file in the folder leaves a granted module there. A grant kept in /// the plug-in store stays, with the stored module it covers. diff --git a/docs/plugins.md b/docs/plugins.md index e35d6d648..04a18c9f4 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -139,7 +139,8 @@ capability model proposed for PhotoCraft plug-ins grant stay as they are; once it is, the grant records the new hash. A grant whose hash is not 64 lower-case hexadecimal digits covers nothing. `plugin.remove` forgets the grant, and so does a successful `plugin.install` of a module that lists no permissions under that id, so the earlier bytes need a new approval. A refused install leaves - the grant as it is. A stored module that lists no permissions has any grant for its id forgotten when the `Plug-ins` + the grant as it is. When such an install can't write the module to the `Plug-ins` folder, the module already stored + there stays and `grants.json` keeps its grant. A stored module that lists no permissions has any grant for its id forgotten when the `Plug-ins` folder is attached. The Additional Plug-ins Folder and `plugin.reload` do not change the `Plug-ins` folder: when they leave a module that lists no permissions under an id, the process forgets its own grant for that id, and a grant kept in `grants.json` stays with the stored module it covers. An entry of `grants.json` that this version can't use (a