Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changes/fix-fs-write-sets-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fs: patch
fs-js: patch
---

The `write-all` and `write-files` permissions (and so every `allow-*-write[-recursive]` set) are now sets of the `allow-*` command permissions, so they also allow the commands a command depends on. `writeFile` with a `ReadableStream` uses `open` and failed with "not allowed" under these sets.
152 changes: 114 additions & 38 deletions plugins/fs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,62 @@ const BASE_DIR_VARS: &[&str] = &[
"APPCACHE",
"APPLOG",
];
const COMMANDS: &[(&str, &[&str])] = &[
("mkdir", &[]),
("create", &[]),
("copy_file", &[]),
("remove", &[]),
("rename", &[]),
("truncate", &[]),
("ftruncate", &[]),
("write", &[]),
("write_file", &["open", "write"]),
("write_text_file", &[]),
("read_dir", &[]),
("read_file", &[]),
("read", &[]),
("open", &[]),
("read_text_file", &[]),
("read_text_file_lines", &["read_text_file_lines_next"]),
("read_text_file_lines_next", &[]),
("seek", &[]),
("stat", &[]),
("lstat", &[]),
("fstat", &[]),
("exists", &[]),
("watch", &[]),
/// A command exposed by the plugin.
struct Command {
name: &'static str,
/// Other commands the JS API calls when using this command,
/// e.g. `writeFile` with a stream uses `open` and `write`.
///
/// The `allow-*` permission of this command also allows its dependencies,
/// and hand-written permissions that allow this command must allow them too.
dependencies: &'static [&'static str],
}

impl Command {
const fn new(name: &'static str) -> Self {
Self {
name,
dependencies: &[],
}
}

const fn dependencies(self, dependencies: &'static [&'static str]) -> Self {
Self {
name: self.name,
dependencies,
}
}
}

const COMMANDS: &[Command] = &[
Command::new("mkdir"),
Command::new("create"),
Command::new("copy_file"),
Command::new("remove"),
Command::new("rename"),
Command::new("truncate"),
Command::new("ftruncate"),
Command::new("write"),
Command::new("write_file").dependencies(&["open", "write"]),
Command::new("write_text_file"),
Command::new("read_dir"),
Command::new("read_file"),
Command::new("read"),
Command::new("open"),
Command::new("read_text_file"),
Command::new("read_text_file_lines").dependencies(&["read_text_file_lines_next"]),
Command::new("read_text_file_lines_next"),
Command::new("seek"),
Command::new("stat"),
Command::new("lstat"),
Command::new("fstat"),
Command::new("exists"),
Command::new("watch"),
// TODO: Remove this in v3
("unwatch", &[]),
("size", &[]),
("start_accessing_security_scoped_resource", &[]),
("stop_accessing_security_scoped_resource", &[]),
Command::new("unwatch"),
Command::new("size"),
Command::new("start_accessing_security_scoped_resource"),
Command::new("stop_accessing_security_scoped_resource"),
];

fn main() {
Expand Down Expand Up @@ -215,19 +242,25 @@ permissions = [
&COMMANDS
.iter()
// FIXME: https://docs.rs/crate/tauri-plugin-fs/2.1.0/builds/1571296
.filter(|c| c.1.is_empty())
.map(|c| c.0)
.filter(|c| c.dependencies.is_empty())
.map(|c| c.name)
.collect::<Vec<_>>(),
)
.global_api_script_path("./api-iife.js")
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
.android_path("android")
.build();

// workaround to include nested permissions as `tauri_plugin` doesn't support it
validate_command_dependencies(Path::new("permissions"));

// workaround to include command dependencies as `tauri_plugin` doesn't support it
let permissions_dir = autogenerated.join("commands");
for (command, nested_commands) in COMMANDS {
if nested_commands.is_empty() {
for Command {
name: command,
dependencies,
} in COMMANDS
{
if dependencies.is_empty() {
continue;
}

Expand All @@ -244,11 +277,10 @@ permissions = [
.iter_mut()
.filter(|p| p.identifier.starts_with("allow"))
{
for c in nested_commands.iter().map(|s| s.to_string()) {
if !p.commands.allow.contains(&c) {
p.commands.allow.push(c);
}
}
p.commands.allow = std::iter::once(command)
.chain(dependencies.iter())
.map(|c| c.to_string())
.collect();
}

let out = toml::to_string_pretty(&permission_file)
Expand All @@ -267,3 +299,47 @@ permissions = [
}
}
}

/// Ensures every hand-written permission that allows a command also allows its dependencies.
fn validate_command_dependencies(permissions_dir: &Path) {
let mut errors = Vec::new();

for entry in std::fs::read_dir(permissions_dir).expect("failed to read permissions dir") {
let path = entry.expect("failed to read permissions dir entry").path();
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
continue;
}
println!("cargo:rerun-if-changed={}", path.display());

let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
let permission_file = toml::from_str::<PermissionFile>(&content)
.unwrap_or_else(|e| panic!("failed to deserialize {}: {e}", path.display()));

for permission in &permission_file.permission {
let allowed = &permission.commands.allow;
for command in COMMANDS
.iter()
.filter(|c| allowed.iter().any(|a| a == c.name))
{
for dependency in command.dependencies {
if !allowed.iter().any(|a| a == dependency) {
errors.push(format!(
"{}: `{}` allows `{}` but not its dependency `{dependency}`",
path.display(),
permission.identifier,
command.name,
));
}
}
}
}
}

if !errors.is_empty() {
panic!(
"permissions are missing command dependencies:\n{}",
errors.join("\n")
);
}
}
8 changes: 4 additions & 4 deletions plugins/fs/permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2035,16 +2035,16 @@
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
},
{
"description": "This enables all write related commands without any pre-configured accessible paths.",
"description": "This enables all write related commands without any pre-configured accessible paths.\n#### This permission set includes:\n\n- `allow-mkdir`\n- `allow-create`\n- `allow-copy-file`\n- `allow-remove`\n- `allow-rename`\n- `allow-truncate`\n- `allow-ftruncate`\n- `allow-write`\n- `allow-write-file`\n- `allow-write-text-file`",
"type": "string",
"const": "write-all",
"markdownDescription": "This enables all write related commands without any pre-configured accessible paths."
"markdownDescription": "This enables all write related commands without any pre-configured accessible paths.\n#### This permission set includes:\n\n- `allow-mkdir`\n- `allow-create`\n- `allow-copy-file`\n- `allow-remove`\n- `allow-rename`\n- `allow-truncate`\n- `allow-ftruncate`\n- `allow-write`\n- `allow-write-file`\n- `allow-write-text-file`"
},
{
"description": "This enables all file write related commands without any pre-configured accessible paths.",
"description": "This enables all file write related commands without any pre-configured accessible paths.\n#### This permission set includes:\n\n- `allow-create`\n- `allow-copy-file`\n- `allow-remove`\n- `allow-rename`\n- `allow-truncate`\n- `allow-ftruncate`\n- `allow-write`\n- `allow-write-file`\n- `allow-write-text-file`",
"type": "string",
"const": "write-files",
"markdownDescription": "This enables all file write related commands without any pre-configured accessible paths."
"markdownDescription": "This enables all file write related commands without any pre-configured accessible paths.\n#### This permission set includes:\n\n- `allow-create`\n- `allow-copy-file`\n- `allow-remove`\n- `allow-rename`\n- `allow-truncate`\n- `allow-ftruncate`\n- `allow-write`\n- `allow-write-file`\n- `allow-write-text-file`"
}
]
}
Expand Down
24 changes: 12 additions & 12 deletions plugins/fs/permissions/write-all.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"$schema" = "schemas/schema.json"

[[permission]]
[[set]]
identifier = "write-all"
description = "This enables all write related commands without any pre-configured accessible paths."
commands.allow = [
"mkdir",
"create",
"copy_file",
"remove",
"rename",
"truncate",
"ftruncate",
"write",
"write_file",
"write_text_file",
permissions = [
"allow-mkdir",
"allow-create",
"allow-copy-file",
"allow-remove",
"allow-rename",
"allow-truncate",
"allow-ftruncate",
"allow-write",
"allow-write-file",
"allow-write-text-file",
]
22 changes: 11 additions & 11 deletions plugins/fs/permissions/write-files.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"$schema" = "schemas/schema.json"

[[permission]]
[[set]]
identifier = "write-files"
description = "This enables all file write related commands without any pre-configured accessible paths."
commands.allow = [
"create",
"copy_file",
"remove",
"rename",
"truncate",
"ftruncate",
"write",
"write_file",
"write_text_file",
permissions = [
"allow-create",
"allow-copy-file",
"allow-remove",
"allow-rename",
"allow-truncate",
"allow-ftruncate",
"allow-write",
"allow-write-file",
"allow-write-text-file",
]
Loading