mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-10 22:38:44 +02:00
* 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
346 lines
11 KiB
Rust
346 lines
11 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
use std::{
|
|
fs::create_dir_all,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use tauri_utils::acl::manifest::PermissionFile;
|
|
|
|
#[path = "src/scope.rs"]
|
|
#[allow(dead_code)]
|
|
mod scope;
|
|
|
|
/// FS scope entry.
|
|
#[derive(schemars::JsonSchema)]
|
|
#[serde(untagged)]
|
|
#[allow(unused)]
|
|
enum FsScopeEntry {
|
|
/// A path that can be accessed by the webview when using the fs APIs.
|
|
/// FS scope path pattern.
|
|
///
|
|
/// The pattern can start with a variable that resolves to a system base directory.
|
|
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
|
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
|
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
|
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
|
Value(PathBuf),
|
|
Object {
|
|
/// A path that can be accessed by the webview when using the fs APIs.
|
|
///
|
|
/// The pattern can start with a variable that resolves to a system base directory.
|
|
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
|
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
|
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
|
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
|
path: PathBuf,
|
|
},
|
|
}
|
|
|
|
// Ensure `FsScopeEntry` and `scope::EntryRaw` is kept in sync
|
|
fn _f() {
|
|
match scope::EntryRaw::Value(PathBuf::new()) {
|
|
scope::EntryRaw::Value(path) => FsScopeEntry::Value(path),
|
|
scope::EntryRaw::Object { path } => FsScopeEntry::Object { path },
|
|
};
|
|
match FsScopeEntry::Value(PathBuf::new()) {
|
|
FsScopeEntry::Value(path) => scope::EntryRaw::Value(path),
|
|
FsScopeEntry::Object { path } => scope::EntryRaw::Object { path },
|
|
};
|
|
}
|
|
|
|
const BASE_DIR_VARS: &[&str] = &[
|
|
"AUDIO",
|
|
"CACHE",
|
|
"CONFIG",
|
|
"DATA",
|
|
"LOCALDATA",
|
|
"DESKTOP",
|
|
"DOCUMENT",
|
|
"DOWNLOAD",
|
|
"EXE",
|
|
"FONT",
|
|
"HOME",
|
|
"PICTURE",
|
|
"PUBLIC",
|
|
"RUNTIME",
|
|
"TEMPLATE",
|
|
"VIDEO",
|
|
"RESOURCE",
|
|
"LOG",
|
|
"TEMP",
|
|
"APPCONFIG",
|
|
"APPDATA",
|
|
"APPLOCALDATA",
|
|
"APPCACHE",
|
|
"APPLOG",
|
|
];
|
|
/// 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
|
|
Command::new("unwatch"),
|
|
Command::new("size"),
|
|
Command::new("start_accessing_security_scoped_resource"),
|
|
Command::new("stop_accessing_security_scoped_resource"),
|
|
];
|
|
|
|
fn main() {
|
|
let autogenerated = Path::new("permissions/autogenerated/");
|
|
let base_dirs = &autogenerated.join("base-directories");
|
|
|
|
if !base_dirs.exists() {
|
|
create_dir_all(base_dirs).expect("unable to create autogenerated base directories dir");
|
|
}
|
|
|
|
for base_dir in BASE_DIR_VARS {
|
|
let upper = base_dir;
|
|
let lower = base_dir.to_lowercase();
|
|
let toml = format!(
|
|
r###"# Automatically generated - DO NOT EDIT!
|
|
|
|
"$schema" = "../../schemas/schema.json"
|
|
|
|
# Scopes Section
|
|
# This section contains scopes, which define file level access
|
|
|
|
[[permission]]
|
|
identifier = "scope-{lower}-recursive"
|
|
description = "This scope permits recursive access to the complete `${upper}` folder, including sub directories and files."
|
|
|
|
[[permission.scope.allow]]
|
|
path = "${upper}"
|
|
[[permission.scope.allow]]
|
|
path = "${upper}/**"
|
|
|
|
[[permission]]
|
|
identifier = "scope-{lower}"
|
|
description = "This scope permits access to all files and list content of top level directories in the `${upper}` folder."
|
|
|
|
[[permission.scope.allow]]
|
|
path = "${upper}"
|
|
[[permission.scope.allow]]
|
|
path = "${upper}/*"
|
|
|
|
[[permission]]
|
|
identifier = "scope-{lower}-index"
|
|
description = "This scope permits to list all files and folders in the `${upper}`folder."
|
|
|
|
[[permission.scope.allow]]
|
|
path = "${upper}"
|
|
|
|
# Sets Section
|
|
# This section combines the scope elements with enablement of commands
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-read-recursive"
|
|
description = "This allows full recursive read access to the complete `${upper}` folder, files and subdirectories."
|
|
permissions = [
|
|
"read-all",
|
|
"scope-{lower}-recursive"
|
|
]
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-write-recursive"
|
|
description = "This allows full recursive write access to the complete `${upper}` folder, files and subdirectories."
|
|
permissions = [
|
|
"write-all",
|
|
"scope-{lower}-recursive"
|
|
]
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-read"
|
|
description = "This allows non-recursive read access to the `${upper}` folder."
|
|
permissions = [
|
|
"read-all",
|
|
"scope-{lower}"
|
|
]
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-write"
|
|
description = "This allows non-recursive write access to the `${upper}` folder."
|
|
permissions = [
|
|
"write-all",
|
|
"scope-{lower}"
|
|
]
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-meta-recursive"
|
|
description = "This allows full recursive read access to metadata of the `${upper}` folder, including file listing and statistics."
|
|
permissions = [
|
|
"read-meta",
|
|
"scope-{lower}-recursive"
|
|
]
|
|
|
|
[[set]]
|
|
identifier = "allow-{lower}-meta"
|
|
description = "This allows non-recursive read access to metadata of the `${upper}` folder, including file listing and statistics."
|
|
permissions = [
|
|
"read-meta",
|
|
"scope-{lower}-index"
|
|
]"###
|
|
);
|
|
|
|
let permission_path = base_dirs.join(format!("{lower}.toml"));
|
|
if toml != std::fs::read_to_string(&permission_path).unwrap_or_default() {
|
|
std::fs::write(permission_path, toml)
|
|
.unwrap_or_else(|e| panic!("unable to autogenerate ${lower}: {e}"));
|
|
}
|
|
}
|
|
|
|
tauri_plugin::Builder::new(
|
|
&COMMANDS
|
|
.iter()
|
|
// FIXME: https://docs.rs/crate/tauri-plugin-fs/2.1.0/builds/1571296
|
|
.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();
|
|
|
|
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 {
|
|
name: command,
|
|
dependencies,
|
|
} in COMMANDS
|
|
{
|
|
if dependencies.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let permission_path = permissions_dir.join(format!("{command}.toml"));
|
|
|
|
let content = std::fs::read_to_string(&permission_path)
|
|
.unwrap_or_else(|_| panic!("failed to read {command}.toml"));
|
|
|
|
let mut permission_file = toml::from_str::<PermissionFile>(&content)
|
|
.unwrap_or_else(|_| panic!("failed to deserialize {command}.toml"));
|
|
|
|
for p in permission_file
|
|
.permission
|
|
.iter_mut()
|
|
.filter(|p| p.identifier.starts_with("allow"))
|
|
{
|
|
p.commands.allow = std::iter::once(command)
|
|
.chain(dependencies.iter())
|
|
.map(|c| c.to_string())
|
|
.collect();
|
|
}
|
|
|
|
let out = toml::to_string_pretty(&permission_file)
|
|
.unwrap_or_else(|_| panic!("failed to serialize {command}.toml"));
|
|
let out = format!(
|
|
r#"# Automatically generated - DO NOT EDIT!
|
|
|
|
"$schema" = "../../schemas/schema.json"
|
|
|
|
{out}"#
|
|
);
|
|
|
|
if content != out {
|
|
std::fs::write(permission_path, out)
|
|
.unwrap_or_else(|_| panic!("failed to write {command}.toml"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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")
|
|
);
|
|
}
|
|
}
|