// 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::>(), ) .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::(&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::(&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") ); } }