/
githubmirror
/
tauri
Обзор
Документация
Войти
/
githubmirror
/
tauri
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
crates/tauri-utils/src/acl/build.rs
500 строк
16 KB
Lucas Nogueira
chore: fix clippy warnings
16 апр 2026, 21:57
16 апр 2026, 21:57
df3fb97
Код
Авторство
О чём код?
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! ACL items that are only useful inside of build script/codegen context. use std::{ collections::{BTreeMap, HashMap}, env, fs, path::{Path, PathBuf}, }; use crate::{ acl::{has_app_manifest, AllowedCommands, Error}, config::Config, write_if_changed, }; use super::{ capability::{Capability, CapabilityFile}, manifest::PermissionFile, ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME, REMOVE_UNUSED_COMMANDS_ENV_VAR, }; /// Known name of the folder containing autogenerated permissions. pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated"; /// Cargo cfg key for permissions file paths pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH"; /// Cargo cfg key for global scope schemas pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH"; /// Allowed permission file extensions pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"]; /// Known filename of the permission documentation file pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md"; /// Allowed capability file extensions const CAPABILITY_FILE_EXTENSIONS: &[&str] = &[ "json", #[cfg(feature = "config-json5")] "json5", "toml", ]; /// Known folder name of the capability schemas const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas"; const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__"; fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> { let mut permissions = Vec::new(); for path in paths { let ext = path.extension().unwrap().to_string_lossy().to_string(); let permission_file = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?; let permission: PermissionFile = match ext.as_str() { "toml" => toml::from_str(&permission_file)?, "json" => serde_json::from_str(&permission_file)?, _ => return Err(Error::UnknownPermissionFormat(ext)), }; permissions.push(permission); } Ok(permissions) } /// Write the permissions to a temporary directory and pass it to the immediate consuming crate. pub fn define_permissions<F: Fn(&Path) -> bool>( pattern: &str, pkg_name: &str, out_dir: &Path, filter_fn: F, ) -> Result<Vec<PermissionFile>, Error> { let permission_files = glob::glob(pattern)? .flatten() .flat_map(|p| p.canonicalize()) // filter extension .filter(|p| { p.extension() .and_then(|e| e.to_str()) .map(|e| PERMISSION_FILE_EXTENSIONS.contains(&e)) .unwrap_or_default() }) .filter(|p| filter_fn(p)) // filter schemas .filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME) .collect::<Vec<PathBuf>>(); let pkg_name_valid_path = pkg_name.replace(':', "-"); let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files")); let permission_files_json = serde_json::to_string(&permission_files)?; write_if_changed(&permission_files_path, permission_files_json) .map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?; if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") { println!( "cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{PERMISSION_FILES_PATH_KEY}={}", permission_files_path.display() ); } else { println!( "cargo:{PERMISSION_FILES_PATH_KEY}={}", permission_files_path.display() ); } parse_permissions(permission_files) } /// Read all permissions listed from the defined cargo cfg key value. pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> { let mut permissions_map = HashMap::new(); for (key, value) in env::vars_os() { let key = key.to_string_lossy(); if let Some(plugin_crate_name_var) = key .strip_prefix("DEP_") .and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}"))) .map(|v| { v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN) .and_then(|v| v.strip_prefix("TAURI_")) .unwrap_or(v) }) { let permissions_path = PathBuf::from(value); let permissions_str = fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?; let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?; let permissions = parse_permissions(permissions)?; let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-"); let plugin_crate_name = plugin_crate_name .strip_prefix("tauri-plugin-") .map(ToString::to_string) .unwrap_or(plugin_crate_name); permissions_map.insert(plugin_crate_name, permissions); } } Ok(permissions_map) } /// Define the global scope schema JSON file path if it exists and pass it to the immediate consuming crate. pub fn define_global_scope_schema( schema: schemars::schema::RootSchema, pkg_name: &str, out_dir: &Path, ) -> Result<(), Error> { let path = out_dir.join("global-scope.json"); write_if_changed(&path, serde_json::to_vec(&schema)?) .map_err(|e| Error::WriteFile(e, path.clone()))?; if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") { println!( "cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display() ); } else { println!("cargo:{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display()); } Ok(()) } /// Read all global scope schemas listed from the defined cargo cfg key value. pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> { let mut schemas_map = HashMap::new(); for (key, value) in env::vars_os() { let key = key.to_string_lossy(); if let Some(plugin_crate_name_var) = key .strip_prefix("DEP_") .and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}"))) .map(|v| { v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN) .and_then(|v| v.strip_prefix("TAURI_")) .unwrap_or(v) }) { let path = PathBuf::from(value); let json = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?; let schema: serde_json::Value = serde_json::from_str(&json)?; let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-"); let plugin_crate_name = plugin_crate_name .strip_prefix("tauri-plugin-") .map(ToString::to_string) .unwrap_or(plugin_crate_name); schemas_map.insert(plugin_crate_name, schema); } } Ok(schemas_map) } /// Parses all capability files with the given glob pattern. pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> { let mut capabilities_map = BTreeMap::new(); for path in glob::glob(pattern)? .flatten() // filter extension .filter(|p| { p.extension() .and_then(|e| e.to_str()) .map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e)) .unwrap_or_default() }) // filter schema files // TODO: remove this before stable .filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME) { match CapabilityFile::load(&path)? { CapabilityFile::Capability(capability) => { if capabilities_map.contains_key(&capability.identifier) { return Err(Error::CapabilityAlreadyExists { identifier: capability.identifier, }); } capabilities_map.insert(capability.identifier.clone(), capability); } CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => { for capability in capabilities { if capabilities_map.contains_key(&capability.identifier) { return Err(Error::CapabilityAlreadyExists { identifier: capability.identifier, }); } capabilities_map.insert(capability.identifier.clone(), capability); } } } } Ok(capabilities_map) } /// Permissions that are generated from commands using [`autogenerate_command_permissions`]. pub struct AutogeneratedPermissions { /// The allow permissions generated from commands. pub allowed: Vec<String>, /// The deny permissions generated from commands. pub denied: Vec<String>, } /// Autogenerate permission files for a list of commands. pub fn autogenerate_command_permissions( path: &Path, commands: &[&str], license_header: &str, schema_ref: bool, ) -> AutogeneratedPermissions { if !path.exists() { fs::create_dir_all(path).expect("unable to create autogenerated commands dir"); } let schema_entry = if schema_ref { let cwd = env::current_dir().unwrap(); let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count(); let schema_path = (1..components_len) .map(|_| "..") .collect::<PathBuf>() .join(PERMISSION_SCHEMAS_FOLDER_NAME) .join(PERMISSION_SCHEMA_FILE_NAME); format!( "\n\"$schema\" = \"{}\"\n", dunce::simplified(&schema_path) .display() .to_string() .replace('\\', "/") ) } else { "".to_string() }; let mut autogenerated = AutogeneratedPermissions { allowed: Vec::new(), denied: Vec::new(), }; for command in commands { let slugified_command = command.replace('_', "-"); let toml = format!( r###"{license_header}# Automatically generated - DO NOT EDIT! {schema_entry} [[permission]] identifier = "allow-{slugified_command}" description = "Enables the {command} command without any pre-configured scope." commands.allow = ["{command}"] [[permission]] identifier = "deny-{slugified_command}" description = "Denies the {command} command without any pre-configured scope." commands.deny = ["{command}"] "###, ); let out_path = path.join(format!("{command}.toml")); write_if_changed(&out_path, toml) .unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}")); autogenerated .allowed .push(format!("allow-{slugified_command}")); autogenerated .denied .push(format!("deny-{slugified_command}")); } autogenerated } const PERMISSION_TABLE_HEADER: &str = "## Permission Table\n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n"; /// Generate a markdown documentation page containing the list of permissions of the plugin. pub fn generate_docs( permissions: &[PermissionFile], out_dir: &Path, plugin_identifier: &str, ) -> Result<(), Error> { let mut default_permission = "".to_owned(); let mut permission_table = "".to_string(); fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String { let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n"); if let Some(d) = description { docs.push_str(&format!("<td>\n\n{d}\n\n</td>")); } docs.push_str("\n</tr>"); docs } for permission in permissions { for set in &permission.set { permission_table.push_str(&docs_from( &set.identifier, Some(&set.description), plugin_identifier, )); permission_table.push('\n'); } if let Some(default) = &permission.default { default_permission.push_str("## Default Permission\n\n"); default_permission.push_str(default.description.as_deref().unwrap_or_default().trim()); default_permission.push('\n'); default_permission.push('\n'); if !default.permissions.is_empty() { default_permission.push_str("#### This default permission set includes the following:\n\n"); for permission in &default.permissions { default_permission.push_str(&format!("- `{permission}`\n")); } default_permission.push('\n'); } } for permission in &permission.permission { permission_table.push_str(&docs_from( &permission.identifier, permission.description.as_deref(), plugin_identifier, )); permission_table.push('\n'); } } let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n"); let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME); write_if_changed(&reference_path, docs).map_err(|e| Error::WriteFile(e, reference_path))?; Ok(()) } // TODO: We have way too many duplicated code around getting the config files, e.g. // - crates/tauri-codegen/src/lib.rs (`get_config`) // - crates/tauri-build/src/lib.rs (`try_build`) // - crates/tauri-cli/src/helpers/config.rs (`get_internal`) /// Generate allowed commands file for the `generate_handler` macro to remove never allowed commands pub fn generate_allowed_commands( out_dir: &Path, capabilities_from_files: Option<BTreeMap<String, Capability>>, permissions_map: BTreeMap<String, Vec<PermissionFile>>, ) -> Result<(), anyhow::Error> { println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}"); let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME); let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR); let should_generate_allowed_commands = remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty(); if !should_generate_allowed_commands { let _ = std::fs::remove_file(allowed_commands_file_path); return Ok(()); } // It's safe to `unwrap` here since we have checked if the result is ok above let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap()); let capabilities_path = config_directory.join("capabilities"); // Cargo re-builds if the variable points to an empty path, // so we check for exists here // see https://github.com/rust-lang/cargo/issues/4213 if capabilities_path.exists() { println!("cargo:rerun-if-changed={}", capabilities_path.display()); } let target_triple = env::var("TARGET")?; let target = crate::platform::Target::from_triple(&target_triple); let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?; for config_file_path in config_paths { println!("cargo:rerun-if-changed={}", config_file_path.display()); } if let Ok(env) = std::env::var("TAURI_CONFIG") { let merge_config: serde_json::Value = serde_json::from_str(&env)?; json_patch::merge(&mut config, &merge_config); } println!("cargo:rerun-if-env-changed=TAURI_CONFIG"); // Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly. let old_cwd = std::env::current_dir()?; std::env::set_current_dir(config_directory)?; let config: Config = serde_json::from_value(config)?; // Reset working directory. std::env::set_current_dir(old_cwd)?; let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map .into_iter() .map(|(key, permissions)| { let key = key .strip_prefix("tauri-plugin-") .unwrap_or(&key) .to_string(); let manifest = crate::acl::manifest::Manifest::new(permissions, None); (key, manifest) }) .collect(); let capabilities_from_files = if let Some(capabilities) = capabilities_from_files { capabilities } else { crate::acl::build::parse_capabilities(&format!( "{}/**/*", glob::Pattern::escape(&capabilities_path.to_string_lossy()) ))? }; let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?; let permission_entries = capabilities .into_values() .flat_map(|capabilities| capabilities.permissions); let mut allowed_commands = AllowedCommands { has_app_acl: has_app_manifest(&acl), ..Default::default() }; for permission_entry in permission_entries { let Ok(permissions) = crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl) else { continue; }; for permission in permissions { let plugin_name = permission.key; let allowed_command_names = &permission.permission.commands.allow; for allowed_command in allowed_command_names { let command_name = if plugin_name == crate::acl::APP_ACL_KEY { allowed_command.to_string() } else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") { format!("plugin:{core_plugin_name}|{allowed_command}") } else { format!("plugin:{plugin_name}|{allowed_command}") }; allowed_commands.commands.insert(command_name); } } } write_if_changed( allowed_commands_file_path, serde_json::to_string(&allowed_commands)?, )?; Ok(()) }