mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 06:06:38 +00:00
Plug-ins: a permission grant covers only the module that was approved
A grant records the module's SHA-256, but only its permissions were compared, so a module with other bytes under a granted id inherited the grant when it listed no new permission. A grant now covers a module only when the id, the SHA-256 and every listed permission match. plugin.inspect reports needsApproval for a changed module, plugin.install refuses it without a new grant and leaves the installed module and its grant as they are, Install Plug-in and File > Open ask again, and the plug-in store and plug-in folders skip it. The same bytes install again without asking, and a module with no permissions list or an empty one needs no grant. A grant whose hash is not 64 lower-case hexadecimal digits covers nothing. plugin.install still trusts the client that sends grant; the docs say so.
This commit is contained in:
@@ -199,6 +199,11 @@ pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||
Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// Is `s` a SHA-256 as [`sha256_hex`] writes it: 64 lower-case hexadecimal digits?
|
||||
fn is_sha256_hex(s: &str) -> bool {
|
||||
s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
|
||||
}
|
||||
|
||||
/// What the user approved for a plug-in: its id, the SHA-256 of the module they approved
|
||||
/// ([`sha256_hex`]) and the permissions it was granted.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -209,9 +214,10 @@ pub struct Grant {
|
||||
}
|
||||
|
||||
impl Grant {
|
||||
/// Does it grant every one of `wanted`?
|
||||
pub fn covers(&self, wanted: &[Permission]) -> bool {
|
||||
wanted.iter().all(|w| self.permissions.contains(w))
|
||||
/// Does it grant every one of `wanted` to the module with this `id` and SHA-256 `hash`? A grant
|
||||
/// whose own hash is not a valid SHA-256 ([`sha256_hex`]) covers nothing.
|
||||
pub fn covers(&self, id: &str, hash: &str, wanted: &[Permission]) -> bool {
|
||||
self.id == id && is_sha256_hex(&self.hash) && self.hash == hash && wanted.iter().all(|w| self.permissions.contains(w))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +287,37 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_grant_covers_its_permissions_and_no_more() {
|
||||
let g = Grant { id: "a".into(), hash: String::new(), permissions: vec![Permission::DocRead, Permission::DocWrite] };
|
||||
assert!(g.covers(&[Permission::DocRead]));
|
||||
assert!(g.covers(&[]));
|
||||
assert!(!g.covers(&[Permission::State]));
|
||||
assert!(!g.covers(&[Permission::DocRead, Permission::State]));
|
||||
let hash = sha256_hex(b"module");
|
||||
let g = Grant { id: "a".into(), hash: hash.clone(), permissions: vec![Permission::DocRead, Permission::DocWrite] };
|
||||
assert!(g.covers("a", &hash, &[Permission::DocRead]));
|
||||
assert!(g.covers("a", &hash, &[]));
|
||||
assert!(!g.covers("a", &hash, &[Permission::State]));
|
||||
assert!(!g.covers("a", &hash, &[Permission::DocRead, Permission::State]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_covers_only_the_module_that_was_approved() {
|
||||
let hash = sha256_hex(b"module");
|
||||
let wanted = [Permission::DocRead];
|
||||
let g = Grant { id: "a".into(), hash: hash.clone(), permissions: vec![Permission::DocRead] };
|
||||
assert!(g.covers("a", &hash, &wanted));
|
||||
assert!(!g.covers("b", &hash, &wanted), "another id");
|
||||
assert!(!g.covers("a", &sha256_hex(b"other"), &wanted), "other bytes");
|
||||
assert!(!g.covers("a", &sha256_hex(b"other"), &[]), "other bytes, even when nothing is wanted");
|
||||
}
|
||||
|
||||
/// Damaged data can't stand for every module, even when the hash asked about is just as damaged.
|
||||
#[test]
|
||||
fn a_grant_with_an_invalid_hash_covers_nothing() {
|
||||
let upper = sha256_hex(b"module").to_uppercase();
|
||||
let short = "ab".repeat(31);
|
||||
let long = "ab".repeat(33);
|
||||
let not_hex = "g".repeat(64);
|
||||
let wide = "é".repeat(32);
|
||||
for hash in ["", "00", upper.as_str(), short.as_str(), long.as_str(), not_hex.as_str(), wide.as_str()] {
|
||||
let g = Grant { id: "a".into(), hash: hash.into(), permissions: vec![Permission::DocRead] };
|
||||
assert!(!g.covers("a", hash, &[Permission::DocRead]), "{hash:?}");
|
||||
assert!(!g.covers("a", hash, &[]), "{hash:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ pub fn revoke(id: &str) -> bool {
|
||||
}
|
||||
|
||||
/// May `plugin` be installed without asking the user: does it list no permissions, or did the user
|
||||
/// already grant its id every permission it lists? A module with other bytes but no new permission
|
||||
/// needs no new approval.
|
||||
/// already grant this very module (its id and SHA-256) every permission it lists? A module with
|
||||
/// other bytes needs a new approval, even when it lists the same permissions.
|
||||
pub fn approved(plugin: &Plugin) -> bool {
|
||||
let m = plugin.manifest();
|
||||
!m.asks_permissions() || granted(&m.id).is_some_and(|g| g.covers(&m.permissions()))
|
||||
!m.asks_permissions() || granted(&m.id).is_some_and(|g| g.covers(&m.id, plugin.hash(), &m.permissions()))
|
||||
}
|
||||
|
||||
/// Installs `plugin`, replacing one with the same id.
|
||||
@@ -125,7 +125,7 @@ pub struct FolderReport {
|
||||
/// Installs every `*.wasm` directly in `dir` (not recursive), in file-name order. A bad module is
|
||||
/// reported and skipped, the others still load; so is a heavy one ([`crate::Budget::Heavy`]),
|
||||
/// which installs only once the user agrees, and one that lists permissions the user has not
|
||||
/// granted its id ([`approved`]).
|
||||
/// granted this very module ([`approved`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn load_folder(dir: &std::path::Path) -> Result<FolderReport> {
|
||||
load_folder_where(dir, |_| Ok(()), approved)
|
||||
|
||||
Reference in New Issue
Block a user