Skip to content

Commit 6a263be

Browse files
authored
fix(fs): allow open, seek and fstat in the write permission sets (#3630)
* fix(fs): allow open, seek and fstat in the write permission sets The stream path of writeFile calls `open`, which only the nested command list of `allow-write-file` granted (plugins/fs/permissions/write-all.toml, write-files.toml), so `fs:allow-appdata-write-recursive` and the other write sets rejected streamed writes; seek/fstat were missing for FileHandles created under these sets. Adds open, seek and fstat to write-all and write-files. This widens the write sets slightly (open is already implied by allow-write-file); the generated reference does not list set members, so nothing to regenerate. * use dependencies instead
1 parent db25585 commit 6a263be

5 files changed

Lines changed: 147 additions & 65 deletions

File tree

‎.changes/fix-fs-write-sets-open.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fs: patch
3+
fs-js: patch
4+
---
5+
6+
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.

‎plugins/fs/build.rs‎

Lines changed: 114 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -77,35 +77,62 @@ const BASE_DIR_VARS: &[&str] = &[
7777
"APPCACHE",
7878
"APPLOG",
7979
];
80-
const COMMANDS: &[(&str, &[&str])] = &[
81-
("mkdir", &[]),
82-
("create", &[]),
83-
("copy_file", &[]),
84-
("remove", &[]),
85-
("rename", &[]),
86-
("truncate", &[]),
87-
("ftruncate", &[]),
88-
("write", &[]),
89-
("write_file", &["open", "write"]),
90-
("write_text_file", &[]),
91-
("read_dir", &[]),
92-
("read_file", &[]),
93-
("read", &[]),
94-
("open", &[]),
95-
("read_text_file", &[]),
96-
("read_text_file_lines", &["read_text_file_lines_next"]),
97-
("read_text_file_lines_next", &[]),
98-
("seek", &[]),
99-
("stat", &[]),
100-
("lstat", &[]),
101-
("fstat", &[]),
102-
("exists", &[]),
103-
("watch", &[]),
80+
/// A command exposed by the plugin.
81+
struct Command {
82+
name: &'static str,
83+
/// Other commands the JS API calls when using this command,
84+
/// e.g. `writeFile` with a stream uses `open` and `write`.
85+
///
86+
/// The `allow-*` permission of this command also allows its dependencies,
87+
/// and hand-written permissions that allow this command must allow them too.
88+
dependencies: &'static [&'static str],
89+
}
90+
91+
impl Command {
92+
const fn new(name: &'static str) -> Self {
93+
Self {
94+
name,
95+
dependencies: &[],
96+
}
97+
}
98+
99+
const fn dependencies(self, dependencies: &'static [&'static str]) -> Self {
100+
Self {
101+
name: self.name,
102+
dependencies,
103+
}
104+
}
105+
}
106+
107+
const COMMANDS: &[Command] = &[
108+
Command::new("mkdir"),
109+
Command::new("create"),
110+
Command::new("copy_file"),
111+
Command::new("remove"),
112+
Command::new("rename"),
113+
Command::new("truncate"),
114+
Command::new("ftruncate"),
115+
Command::new("write"),
116+
Command::new("write_file").dependencies(&["open", "write"]),
117+
Command::new("write_text_file"),
118+
Command::new("read_dir"),
119+
Command::new("read_file"),
120+
Command::new("read"),
121+
Command::new("open"),
122+
Command::new("read_text_file"),
123+
Command::new("read_text_file_lines").dependencies(&["read_text_file_lines_next"]),
124+
Command::new("read_text_file_lines_next"),
125+
Command::new("seek"),
126+
Command::new("stat"),
127+
Command::new("lstat"),
128+
Command::new("fstat"),
129+
Command::new("exists"),
130+
Command::new("watch"),
104131
// TODO: Remove this in v3
105-
("unwatch", &[]),
106-
("size", &[]),
107-
("start_accessing_security_scoped_resource", &[]),
108-
("stop_accessing_security_scoped_resource", &[]),
132+
Command::new("unwatch"),
133+
Command::new("size"),
134+
Command::new("start_accessing_security_scoped_resource"),
135+
Command::new("stop_accessing_security_scoped_resource"),
109136
];
110137

111138
fn main() {
@@ -215,19 +242,25 @@ permissions = [
215242
&COMMANDS
216243
.iter()
217244
// FIXME: https://docs.rs/crate/tauri-plugin-fs/2.1.0/builds/1571296
218-
.filter(|c| c.1.is_empty())
219-
.map(|c| c.0)
245+
.filter(|c| c.dependencies.is_empty())
246+
.map(|c| c.name)
220247
.collect::<Vec<_>>(),
221248
)
222249
.global_api_script_path("./api-iife.js")
223250
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
224251
.android_path("android")
225252
.build();
226253

227-
// workaround to include nested permissions as `tauri_plugin` doesn't support it
254+
validate_command_dependencies(Path::new("permissions"));
255+
256+
// workaround to include command dependencies as `tauri_plugin` doesn't support it
228257
let permissions_dir = autogenerated.join("commands");
229-
for (command, nested_commands) in COMMANDS {
230-
if nested_commands.is_empty() {
258+
for Command {
259+
name: command,
260+
dependencies,
261+
} in COMMANDS
262+
{
263+
if dependencies.is_empty() {
231264
continue;
232265
}
233266

@@ -244,11 +277,10 @@ permissions = [
244277
.iter_mut()
245278
.filter(|p| p.identifier.starts_with("allow"))
246279
{
247-
for c in nested_commands.iter().map(|s| s.to_string()) {
248-
if !p.commands.allow.contains(&c) {
249-
p.commands.allow.push(c);
250-
}
251-
}
280+
p.commands.allow = std::iter::once(command)
281+
.chain(dependencies.iter())
282+
.map(|c| c.to_string())
283+
.collect();
252284
}
253285

254286
let out = toml::to_string_pretty(&permission_file)
@@ -267,3 +299,47 @@ permissions = [
267299
}
268300
}
269301
}
302+
303+
/// Ensures every hand-written permission that allows a command also allows its dependencies.
304+
fn validate_command_dependencies(permissions_dir: &Path) {
305+
let mut errors = Vec::new();
306+
307+
for entry in std::fs::read_dir(permissions_dir).expect("failed to read permissions dir") {
308+
let path = entry.expect("failed to read permissions dir entry").path();
309+
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
310+
continue;
311+
}
312+
println!("cargo:rerun-if-changed={}", path.display());
313+
314+
let content = std::fs::read_to_string(&path)
315+
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
316+
let permission_file = toml::from_str::<PermissionFile>(&content)
317+
.unwrap_or_else(|e| panic!("failed to deserialize {}: {e}", path.display()));
318+
319+
for permission in &permission_file.permission {
320+
let allowed = &permission.commands.allow;
321+
for command in COMMANDS
322+
.iter()
323+
.filter(|c| allowed.iter().any(|a| a == c.name))
324+
{
325+
for dependency in command.dependencies {
326+
if !allowed.iter().any(|a| a == dependency) {
327+
errors.push(format!(
328+
"{}: `{}` allows `{}` but not its dependency `{dependency}`",
329+
path.display(),
330+
permission.identifier,
331+
command.name,
332+
));
333+
}
334+
}
335+
}
336+
}
337+
}
338+
339+
if !errors.is_empty() {
340+
panic!(
341+
"permissions are missing command dependencies:\n{}",
342+
errors.join("\n")
343+
);
344+
}
345+
}

‎plugins/fs/permissions/schemas/schema.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,16 +2035,16 @@
20352035
"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"
20362036
},
20372037
{
2038-
"description": "This enables all write related commands without any pre-configured accessible paths.",
2038+
"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`",
20392039
"type": "string",
20402040
"const": "write-all",
2041-
"markdownDescription": "This enables all write related commands without any pre-configured accessible paths."
2041+
"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`"
20422042
},
20432043
{
2044-
"description": "This enables all file write related commands without any pre-configured accessible paths.",
2044+
"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`",
20452045
"type": "string",
20462046
"const": "write-files",
2047-
"markdownDescription": "This enables all file write related commands without any pre-configured accessible paths."
2047+
"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`"
20482048
}
20492049
]
20502050
}
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"$schema" = "schemas/schema.json"
22

3-
[[permission]]
3+
[[set]]
44
identifier = "write-all"
55
description = "This enables all write related commands without any pre-configured accessible paths."
6-
commands.allow = [
7-
"mkdir",
8-
"create",
9-
"copy_file",
10-
"remove",
11-
"rename",
12-
"truncate",
13-
"ftruncate",
14-
"write",
15-
"write_file",
16-
"write_text_file",
6+
permissions = [
7+
"allow-mkdir",
8+
"allow-create",
9+
"allow-copy-file",
10+
"allow-remove",
11+
"allow-rename",
12+
"allow-truncate",
13+
"allow-ftruncate",
14+
"allow-write",
15+
"allow-write-file",
16+
"allow-write-text-file",
1717
]
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"$schema" = "schemas/schema.json"
22

3-
[[permission]]
3+
[[set]]
44
identifier = "write-files"
55
description = "This enables all file write related commands without any pre-configured accessible paths."
6-
commands.allow = [
7-
"create",
8-
"copy_file",
9-
"remove",
10-
"rename",
11-
"truncate",
12-
"ftruncate",
13-
"write",
14-
"write_file",
15-
"write_text_file",
6+
permissions = [
7+
"allow-create",
8+
"allow-copy-file",
9+
"allow-remove",
10+
"allow-rename",
11+
"allow-truncate",
12+
"allow-ftruncate",
13+
"allow-write",
14+
"allow-write-file",
15+
"allow-write-text-file",
1616
]

0 commit comments

Comments
 (0)