From 63649d82d20b8f69d973b41bd0c157997770d6a0 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Fri, 13 Sep 2024 14:58:26 +0300 Subject: [PATCH] fix(core/acl): fix `core:default` schema generation (#10971) * remove dbg! in resources test * use methods from `fs` and `env` qualified * share `ACL_MANIFESTS_FILE_NAME` and `CAPABILITIES_FILE_NAME` consts across crates * simplifiy `Manifest::new` code for better readability * move reading global api scripts logic next to the function that defines it * [tauri-build] move acl logic from lib.rs to acl.rs * use const value for schema instead of enum value with a single variant * remove unnecessary info from permissions hover * move related functions next to each other & improve readability of others * use methods from `fs` and `env` qualified * fix warning, unused return in test * document some functions * improve generated schema for better scope schema completion, simplify, reorganize and document the logic previously if you had `fs` and `http` plugins added in a project and then try to write an extended permission for `fs:allow-app-meta` ```json { "identifier": "fs:allow-app-meta", "allow": [ ] } ``` and even though identifier is from `fs` plugin, the JSON schema suggests `path` and `url`. Now it will only suggest relevant field which is `path` * resolve permissions from other plugins, generate `core:default` as a normal set instead of special logic * move `PERMISSION_SCHEMAS_FOLDER_NAME` to acl module * use gneric trait because of MSRV * ensure `gen/schemas` dir is created * clippy --- .changes/core-default-schema.md | 5 + crates/tauri-build/src/acl.rs | 337 ++++---------- crates/tauri-build/src/lib.rs | 87 +--- crates/tauri-cli/config.schema.json | 4 +- crates/tauri-codegen/src/context.rs | 38 +- crates/tauri-plugin/src/build/mod.rs | 11 +- .../schemas/config.schema.json | 4 +- crates/tauri-utils/src/acl/build.rs | 427 +++++++---------- crates/tauri-utils/src/acl/capability.rs | 11 + crates/tauri-utils/src/acl/manifest.rs | 64 ++- crates/tauri-utils/src/acl/mod.rs | 25 +- crates/tauri-utils/src/acl/resolved.rs | 436 ++++++++++++------ crates/tauri-utils/src/acl/schema.rs | 345 ++++++++++++++ crates/tauri-utils/src/config.rs | 2 +- crates/tauri-utils/src/plugin.rs | 32 +- crates/tauri-utils/src/resources.rs | 2 - crates/tauri/build.rs | 75 ++- .../permissions/schemas/schema.json | 24 +- 18 files changed, 1097 insertions(+), 832 deletions(-) create mode 100644 .changes/core-default-schema.md create mode 100644 crates/tauri-utils/src/acl/schema.rs diff --git a/.changes/core-default-schema.md b/.changes/core-default-schema.md new file mode 100644 index 000000000000..60fb90d5c831 --- /dev/null +++ b/.changes/core-default-schema.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch:bug" +--- + +Fix schema generation for `core:default` set. diff --git a/crates/tauri-build/src/acl.rs b/crates/tauri-build/src/acl.rs index bbae74dd0185..d1a1e6f647ca 100644 --- a/crates/tauri-build/src/acl.rs +++ b/crates/tauri-build/src/acl.rs @@ -3,35 +3,22 @@ // SPDX-License-Identifier: MIT use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - env::current_dir, - fs::{copy, create_dir_all, read_to_string, write}, + collections::{BTreeMap, HashMap}, + env, fs, path::{Path, PathBuf}, }; use anyhow::{Context, Result}; -use schemars::{ - schema::{ - ArrayValidation, InstanceType, Metadata, ObjectValidation, RootSchema, Schema, SchemaObject, - SubschemaValidation, - }, - schema_for, -}; use tauri_utils::{ acl::{ - capability::{Capability, CapabilityFile}, - manifest::Manifest, - APP_ACL_KEY, + capability::Capability, manifest::Manifest, schema::CAPABILITIES_SCHEMA_FOLDER_PATH, + ACL_MANIFESTS_FILE_NAME, APP_ACL_KEY, CAPABILITIES_FILE_NAME, }, platform::Target, write_if_changed, }; -const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json"; -/// Path of the folder where schemas are saved. -const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas"; -const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; -const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; +use crate::Attributes; /// Definition of a plugin that is part of the Tauri application instead of having its own crate. /// @@ -39,7 +26,7 @@ const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`]. /// /// To autogenerate permissions for each of the plugin commands, see [`Self::commands`]. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct InlinedPlugin { commands: &'static [&'static str], permissions_path_pattern: Option<&'static str>, @@ -47,7 +34,7 @@ pub struct InlinedPlugin { } /// Variants of a generated default permission that can be used on an [`InlinedPlugin`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum DefaultPermissionRule { /// Allow all commands from [`InlinedPlugin::commands`]. AllowAllCommands, @@ -95,7 +82,7 @@ impl InlinedPlugin { /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`]. /// /// To autogenerate permissions for each of the app commands, see [`Self::commands`]. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, Copy)] pub struct AppManifest { commands: &'static [&'static str], permissions_path_pattern: Option<&'static str>, @@ -124,229 +111,51 @@ impl AppManifest { } } -fn capabilities_schema(acl_manifests: &BTreeMap) -> RootSchema { - let mut schema = schema_for!(CapabilityFile); - - fn schema_from(key: &str, id: &str, description: Option<&str>) -> Schema { - let command_name = if key == APP_ACL_KEY { - id.to_string() - } else { - format!("{key}:{id}") - }; - Schema::Object(SchemaObject { - metadata: Some(Box::new(Metadata { - description: description - .as_ref() - .map(|d| format!("{command_name} -> {d}")), - ..Default::default() - })), - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![serde_json::Value::String(command_name)]), - ..Default::default() - }) - } - - let mut permission_schemas = Vec::new(); - - for (key, manifest) in acl_manifests { - for (set_id, set) in &manifest.permission_sets { - permission_schemas.push(schema_from(key, set_id, Some(&set.description))); - } - - permission_schemas.push(schema_from( - key, - "default", - manifest - .default_permission - .as_ref() - .map(|d| d.description.as_ref()), - )); - - for (permission_id, permission) in &manifest.permissions { - permission_schemas.push(schema_from( - key, - permission_id, - permission.description.as_deref(), - )); - } - } - - if let Some(Schema::Object(obj)) = schema.definitions.get_mut("Identifier") { - obj.object = None; - obj.instance_type = None; - obj.metadata.as_mut().map(|metadata| { - metadata - .description - .replace("Permission identifier".to_string()); - metadata - }); - obj.subschemas.replace(Box::new(SubschemaValidation { - one_of: Some(permission_schemas), - ..Default::default() - })); - } +/// Saves capabilities in a file inside the project, mainly to be read by tauri-cli. +fn save_capabilities(capabilities: &BTreeMap) -> Result { + let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH); + fs::create_dir_all(dir)?; - let mut definitions = Vec::new(); - - if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionEntry") { - let permission_entry_any_of_schemas = obj.subschemas().any_of.as_mut().unwrap(); - - if let Schema::Object(scope_extended_schema_obj) = - permission_entry_any_of_schemas.last_mut().unwrap() - { - let mut global_scope_one_of = Vec::new(); - - for (key, manifest) in acl_manifests { - if let Some(global_scope_schema) = &manifest.global_scope_schema { - let global_scope_schema_def: RootSchema = - serde_json::from_value(global_scope_schema.clone()) - .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {key}: {e}")); - - let global_scope_schema = Schema::Object(SchemaObject { - array: Some(Box::new(ArrayValidation { - items: Some(Schema::Object(global_scope_schema_def.schema).into()), - ..Default::default() - })), - ..Default::default() - }); - - definitions.push(global_scope_schema_def.definitions); - - let mut required = BTreeSet::new(); - required.insert("identifier".to_string()); - - let mut object = ObjectValidation { - required, - ..Default::default() - }; - - let mut permission_schemas = Vec::new(); - permission_schemas.push(schema_from( - key, - "default", - manifest - .default_permission - .as_ref() - .map(|d| d.description.as_ref()), - )); - for set in manifest.permission_sets.values() { - permission_schemas.push(schema_from(key, &set.identifier, Some(&set.description))); - } - for permission in manifest.permissions.values() { - permission_schemas.push(schema_from( - key, - &permission.identifier, - permission.description.as_deref(), - )); - } - - let identifier_schema = Schema::Object(SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(permission_schemas), - ..Default::default() - })), - ..Default::default() - }); - - object - .properties - .insert("identifier".to_string(), identifier_schema); - object - .properties - .insert("allow".to_string(), global_scope_schema.clone()); - object - .properties - .insert("deny".to_string(), global_scope_schema); - - global_scope_one_of.push(Schema::Object(SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(object)), - ..Default::default() - })); - } - } - - if !global_scope_one_of.is_empty() { - scope_extended_schema_obj.object = None; - scope_extended_schema_obj - .subschemas - .replace(Box::new(SubschemaValidation { - one_of: Some(global_scope_one_of), - ..Default::default() - })); - }; - } - } + let path = dir.join(CAPABILITIES_FILE_NAME); + let json = serde_json::to_string(&capabilities)?; + write_if_changed(&path, json)?; - for definitions_map in definitions { - schema.definitions.extend(definitions_map); - } - - schema + Ok(path) } -pub fn generate_schema(acl_manifests: &BTreeMap, target: Target) -> Result<()> { - let schema = capabilities_schema(acl_manifests); - let schema_str = serde_json::to_string_pretty(&schema).unwrap(); - let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH); - create_dir_all(&out_dir).context("unable to create schema output directory")?; - - let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}")); - if schema_str != read_to_string(&schema_path).unwrap_or_default() { - write(&schema_path, schema_str)?; - - copy( - schema_path, - out_dir.join(format!( - "{}-{CAPABILITIES_SCHEMA_FILE_NAME}", - if target.is_desktop() { - "desktop" - } else { - "mobile" - } - )), - )?; - } +/// Saves ACL manifests in a file inside the project, mainly to be read by tauri-cli. +fn save_acl_manifests(acl_manifests: &BTreeMap) -> Result { + let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH); + fs::create_dir_all(dir)?; - Ok(()) -} + let path = dir.join(ACL_MANIFESTS_FILE_NAME); + let json = serde_json::to_string(&acl_manifests)?; + write_if_changed(&path, json)?; -pub fn save_capabilities(capabilities: &BTreeMap) -> Result { - let capabilities_path = - PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(CAPABILITIES_FILE_NAME); - let capabilities_json = serde_json::to_string(&capabilities)?; - if capabilities_json != read_to_string(&capabilities_path).unwrap_or_default() { - std::fs::write(&capabilities_path, capabilities_json)?; - } - Ok(capabilities_path) + Ok(path) } -pub fn save_acl_manifests(acl_manifests: &BTreeMap) -> Result { - let acl_manifests_path = - PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(ACL_MANIFESTS_FILE_NAME); - let acl_manifests_json = serde_json::to_string(&acl_manifests)?; - if acl_manifests_json != read_to_string(&acl_manifests_path).unwrap_or_default() { - std::fs::write(&acl_manifests_path, acl_manifests_json)?; - } - Ok(acl_manifests_path) -} +/// Read plugin permissions and scope schema from env vars +fn read_plugins_manifests() -> Result> { + use tauri_utils::acl; -pub fn get_manifests_from_plugins() -> Result> { let permission_map = - tauri_utils::acl::build::read_permissions().context("failed to read plugin permissions")?; - let mut global_scope_map = tauri_utils::acl::build::read_global_scope_schemas() - .context("failed to read global scope schemas")?; + acl::build::read_permissions().context("failed to read plugin permissions")?; + let mut global_scope_map = + acl::build::read_global_scope_schemas().context("failed to read global scope schemas")?; + + let mut manifests = BTreeMap::new(); - let mut processed = BTreeMap::new(); for (plugin_name, permission_files) in permission_map { - let manifest = Manifest::new(permission_files, global_scope_map.remove(&plugin_name)); - processed.insert(plugin_name, manifest); + let global_scope_schema = global_scope_map.remove(&plugin_name); + let manifest = Manifest::new(permission_files, global_scope_schema); + manifests.insert(plugin_name, manifest); } - Ok(processed) + Ok(manifests) } -pub fn inline_plugins( +fn inline_plugins( out_dir: &Path, inlined_plugins: HashMap<&'static str, InlinedPlugin>, ) -> Result> { @@ -354,7 +163,7 @@ pub fn inline_plugins( for (name, plugin) in inlined_plugins { let plugin_out_dir = out_dir.join("plugins").join(name); - create_dir_all(&plugin_out_dir)?; + fs::create_dir_all(&plugin_out_dir)?; let mut permission_files = if plugin.commands.is_empty() { Vec::new() @@ -371,22 +180,22 @@ pub fn inline_plugins( DefaultPermissionRule::Allow(permissions) => permissions, }); if let Some(default_permissions) = default_permissions { - let default_permission_toml = format!( + let default_permissions = default_permissions + .iter() + .map(|p| format!("\"{p}\"")) + .collect::>() + .join(","); + let default_permission = format!( r###"# Automatically generated - DO NOT EDIT! [default] permissions = [{default_permissions}] -"###, - default_permissions = default_permissions - .iter() - .map(|p| format!("\"{p}\"")) - .collect::>() - .join(",") +"### ); - let default_permission_toml_path = plugin_out_dir.join("default.toml"); + let default_permission_path = plugin_out_dir.join("default.toml"); - write_if_changed(&default_permission_toml_path, default_permission_toml) - .unwrap_or_else(|_| panic!("unable to autogenerate {default_permission_toml_path:?}")); + write_if_changed(&default_permission_path, default_permission) + .unwrap_or_else(|_| panic!("unable to autogenerate {default_permission_path:?}")); } tauri_utils::acl::build::define_permissions( @@ -430,13 +239,13 @@ permissions = [{default_permissions}] Ok(acl_manifests) } -pub fn app_manifest_permissions( +fn app_manifest_permissions( out_dir: &Path, manifest: AppManifest, inlined_plugins: &HashMap<&'static str, InlinedPlugin>, ) -> Result { let app_out_dir = out_dir.join("app-manifest"); - create_dir_all(&app_out_dir)?; + fs::create_dir_all(&app_out_dir)?; let pkg_name = "__app__"; let mut permission_files = if manifest.commands.is_empty() { @@ -473,7 +282,7 @@ pub fn app_manifest_permissions( ); } - let permissions_root = current_dir()?.join("permissions"); + let permissions_root = env::current_dir()?.join("permissions"); let inlined_plugins_permissions: Vec<_> = inlined_plugins .keys() .map(|name| permissions_root.join(name)) @@ -501,7 +310,7 @@ pub fn app_manifest_permissions( )) } -pub fn validate_capabilities( +fn validate_capabilities( acl_manifests: &BTreeMap, capabilities: &BTreeMap, ) -> Result<()> { @@ -523,10 +332,6 @@ pub fn validate_capabilities( let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY); let permission_name = permission_id.get_base(); - if key == "core" && permission_name == "default" { - continue; - } - let permission_exists = acl_manifests .get(key) .map(|manifest| { @@ -567,3 +372,41 @@ pub fn validate_capabilities( Ok(()) } + +pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super::Result<()> { + let mut acl_manifests = read_plugins_manifests()?; + + let app_manifest = app_manifest_permissions( + out_dir, + attributes.app_manifest, + &attributes.inlined_plugins, + )?; + if app_manifest.default_permission.is_some() + || !app_manifest.permission_sets.is_empty() + || !app_manifest.permissions.is_empty() + { + acl_manifests.insert(APP_ACL_KEY.into(), app_manifest); + } + + acl_manifests.extend(inline_plugins(out_dir, attributes.inlined_plugins.clone())?); + + let acl_manifests_path = save_acl_manifests(&acl_manifests)?; + fs::copy(acl_manifests_path, out_dir.join(ACL_MANIFESTS_FILE_NAME))?; + + tauri_utils::acl::schema::generate_capability_schema(&acl_manifests, target)?; + + let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern { + tauri_utils::acl::build::parse_capabilities(pattern)? + } else { + println!("cargo:rerun-if-changed=capabilities"); + tauri_utils::acl::build::parse_capabilities("./capabilities/**/*")? + }; + validate_capabilities(&acl_manifests, &capabilities)?; + + let capabilities_path = save_capabilities(&capabilities)?; + fs::copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?; + + tauri_utils::plugin::save_global_api_scripts_paths(out_dir); + + Ok(()) +} diff --git a/crates/tauri-build/src/lib.rs b/crates/tauri-build/src/lib.rs index 7baecdb2e3da..9919ad648c85 100644 --- a/crates/tauri-build/src/lib.rs +++ b/crates/tauri-build/src/lib.rs @@ -17,15 +17,13 @@ pub use anyhow::Result; use cargo_toml::Manifest; use tauri_utils::{ - acl::{build::parse_capabilities, APP_ACL_KEY}, config::{BundleResources, Config, WebviewInstallMode}, resources::{external_binaries, ResourcePaths}, }; use std::{ collections::HashMap, - env::var_os, - fs::copy, + env, fs, path::{Path, PathBuf}, }; @@ -42,9 +40,6 @@ pub use codegen::context::CodegenContext; pub use acl::{AppManifest, DefaultPermissionRule, InlinedPlugin}; -const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; -const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; - fn copy_file(from: impl AsRef, to: impl AsRef) -> Result<()> { let from = from.as_ref(); let to = to.as_ref(); @@ -55,8 +50,8 @@ fn copy_file(from: impl AsRef, to: impl AsRef) -> Result<()> { return Err(anyhow::anyhow!("{:?} is not a file", from)); } let dest_dir = to.parent().expect("No data in parent"); - std::fs::create_dir_all(dest_dir)?; - std::fs::copy(from, to)?; + fs::create_dir_all(dest_dir)?; + fs::copy(from, to)?; Ok(()) } @@ -84,7 +79,7 @@ fn copy_binaries( let dest = path.join(file_name); if dest.exists() { - std::fs::remove_file(&dest).unwrap(); + fs::remove_file(&dest).unwrap(); } copy_file(&src, &dest)?; } @@ -139,16 +134,16 @@ fn copy_dir(from: &Path, to: &Path) -> Result<()> { let rel_path = entry.path().strip_prefix(from)?; let dest_path = to.join(rel_path); if entry.file_type().is_symlink() { - let target = std::fs::read_link(entry.path())?; + let target = fs::read_link(entry.path())?; if entry.path().is_dir() { symlink_dir(&target, &dest_path)?; } else { symlink_file(&target, &dest_path)?; } } else if entry.file_type().is_dir() { - std::fs::create_dir(dest_path)?; + fs::create_dir(dest_path)?; } else { - std::fs::copy(entry.path(), dest_path)?; + fs::copy(entry.path(), dest_path)?; } } Ok(()) @@ -168,7 +163,7 @@ fn copy_framework_from(src_dir: &Path, framework: &str, dest_dir: &Path) -> Resu // Copies the macOS application bundle frameworks to the target folder fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> { - std::fs::create_dir_all(dest_dir) + fs::create_dir_all(dest_dir) .with_context(|| format!("Failed to create frameworks output directory at {dest_dir:?}"))?; for framework in frameworks.iter() { if framework.ends_with(".framework") { @@ -420,8 +415,7 @@ impl Attributes { } pub fn is_dev() -> bool { - std::env::var("DEP_TAURI_DEV") - .expect("missing `cargo:dev` instruction, please update tauri to latest") + env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest") == "true" } @@ -471,19 +465,19 @@ pub fn try_build(attributes: Attributes) -> Result<()> { #[cfg(feature = "config-toml")] println!("cargo:rerun-if-changed=Tauri.toml"); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); let mobile = target_os == "ios" || target_os == "android"; cfg_alias("desktop", !mobile); cfg_alias("mobile", mobile); - let target_triple = std::env::var("TARGET").unwrap(); + let target_triple = env::var("TARGET").unwrap(); let target = tauri_utils::platform::Target::from_triple(&target_triple); let mut config = serde_json::from_value(tauri_utils::config::parse::read_from( target, - std::env::current_dir().unwrap(), + env::current_dir().unwrap(), )?)?; - if let Ok(env) = std::env::var("TAURI_CONFIG") { + if let Ok(env) = env::var("TAURI_CONFIG") { let merge_config: serde_json::Value = serde_json::from_str(&env)?; json_patch::merge(&mut config, &merge_config); } @@ -506,7 +500,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { android_package_prefix.pop(); println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}"); - if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { mobile::generate_gradle_files(project_dir, &config)?; } @@ -514,7 +508,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { let ws_path = get_workspace_dir()?; let mut manifest = - Manifest::::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?; + Manifest::::from_slice_with_metadata(&fs::read("Cargo.toml")?)?; if let Ok(ws_manifest) = Manifest::from_path(ws_path.join("Cargo.toml")) { Manifest::complete_from_path_and_workspace( @@ -526,48 +520,15 @@ pub fn try_build(attributes: Attributes) -> Result<()> { Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?; } - let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); manifest::check(&config, &mut manifest)?; - let mut acl_manifests = acl::get_manifests_from_plugins()?; - let app_manifest = acl::app_manifest_permissions( - &out_dir, - attributes.app_manifest, - &attributes.inlined_plugins, - )?; - if app_manifest.default_permission.is_some() - || !app_manifest.permission_sets.is_empty() - || !app_manifest.permissions.is_empty() - { - acl_manifests.insert(APP_ACL_KEY.into(), app_manifest); - } - acl_manifests.extend(acl::inline_plugins(&out_dir, attributes.inlined_plugins)?); - - std::fs::write( - out_dir.join(ACL_MANIFESTS_FILE_NAME), - serde_json::to_string(&acl_manifests)?, - )?; - - let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern { - parse_capabilities(pattern)? - } else { - println!("cargo:rerun-if-changed=capabilities"); - parse_capabilities("./capabilities/**/*")? - }; - acl::generate_schema(&acl_manifests, target)?; - acl::validate_capabilities(&acl_manifests, &capabilities)?; - - let capabilities_path = acl::save_capabilities(&capabilities)?; - copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?; - - acl::save_acl_manifests(&acl_manifests)?; - - tauri_utils::plugin::load_global_api_scripts(&out_dir); + acl::build(&out_dir, target, &attributes)?; println!("cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE={target_triple}"); // when running codegen in this build script, we need to access the env var directly - std::env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple); + env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple); // TODO: far from ideal, but there's no other way to get the target dir, see let target_dir = out_dir @@ -612,7 +573,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> { if let Some(frameworks) = &config.bundle.macos.frameworks { if !frameworks.is_empty() { let frameworks_dir = target_dir.parent().unwrap().join("Frameworks"); - let _ = std::fs::remove_dir_all(&frameworks_dir); + let _ = fs::remove_dir_all(&frameworks_dir); // copy frameworks to the root `target` folder (instead of `target/debug` for instance) // because the rpath is set to `@executable_path/../Frameworks`. copy_frameworks(&frameworks_dir, frameworks)?; @@ -700,17 +661,17 @@ pub fn try_build(attributes: Attributes) -> Result<()> { ) })?; - let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); + let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap(); match target_env.as_str() { "gnu" => { - let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() { + let target_arch = match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() { "x86_64" => Some("x64"), "x86" => Some("x86"), "aarch64" => Some("arm64"), arch => None, }; if let Some(target_arch) = target_arch { - for entry in std::fs::read_dir(target_dir.join("build"))? { + for entry in fs::read_dir(target_dir.join("build"))? { let path = entry?.path(); let webview2_loader_path = path .join("out") @@ -718,14 +679,14 @@ pub fn try_build(attributes: Attributes) -> Result<()> { .join("WebView2Loader.dll"); if path.to_string_lossy().contains("webview2-com-sys") && webview2_loader_path.exists() { - std::fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?; + fs::copy(webview2_loader_path, target_dir.join("WebView2Loader.dll"))?; break; } } } } "msvc" => { - if std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") { + if env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") { static_vcruntime::build(); } } diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 4cc3b65b1ce3..329bed448dde 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -1748,9 +1748,7 @@ "anyOf": [ { "description": "Bundle all targets.", - "enum": [ - "all" - ] + "const": "all" }, { "description": "A list of bundle targets.", diff --git a/crates/tauri-codegen/src/context.rs b/crates/tauri-codegen/src/context.rs index 849af6dd2740..3291c87db6d6 100644 --- a/crates/tauri-codegen/src/context.rs +++ b/crates/tauri-codegen/src/context.rs @@ -18,6 +18,7 @@ use proc_macro2::TokenStream; use quote::quote; use sha2::{Digest, Sha256}; use syn::Expr; +use tauri_utils::acl::{ACL_MANIFESTS_FILE_NAME, CAPABILITIES_FILE_NAME}; use tauri_utils::{ acl::capability::{Capability, CapabilityFile}, acl::manifest::Manifest, @@ -26,13 +27,9 @@ use tauri_utils::{ config::{CapabilityEntry, Config, FrontendDist, PatternKind}, html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef}, platform::Target, - plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH, tokens::{map_lit, str_lit}, }; -const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; -const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; - /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context. pub struct ContextData { pub dev: bool, @@ -450,32 +447,13 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL"); let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved)); - let plugin_global_api_script_file_list_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH); - let plugin_global_api_script = - if config.app.with_global_tauri && plugin_global_api_script_file_list_path.exists() { - let file_list_str = std::fs::read_to_string(plugin_global_api_script_file_list_path) - .expect("failed to read plugin global API script paths"); - let file_list = serde_json::from_str::>(&file_list_str) - .expect("failed to parse plugin global API script paths"); - - let mut plugins = Vec::new(); - for path in file_list { - plugins.push(std::fs::read_to_string(&path).unwrap_or_else(|e| { - panic!( - "failed to read plugin global API script {}: {e}", - path.display() - ) - })); - } - - Some(plugins) + let plugin_global_api_scripts = if config.app.with_global_tauri { + if let Some(scripts) = tauri_utils::plugin::read_global_api_scripts(&out_dir) { + let scripts = scripts.into_iter().map(|s| quote!(#s)); + quote!(::std::option::Option::Some(&[#(#scripts),*])) } else { - None - }; - - let plugin_global_api_script = if let Some(scripts) = plugin_global_api_script { - let scripts = scripts.into_iter().map(|s| quote!(#s)); - quote!(::std::option::Option::Some(&[#(#scripts),*])) + quote!(::std::option::Option::None) + } } else { quote!(::std::option::Option::None) }; @@ -501,7 +479,7 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { #package_info, #pattern, #runtime_authority, - #plugin_global_api_script + #plugin_global_api_scripts ); #with_tray_icon_code diff --git a/crates/tauri-plugin/src/build/mod.rs b/crates/tauri-plugin/src/build/mod.rs index fda9eb5f5d89..cd582d67f4de 100644 --- a/crates/tauri-plugin/src/build/mod.rs +++ b/crates/tauri-plugin/src/build/mod.rs @@ -11,7 +11,7 @@ pub mod mobile; use serde::de::DeserializeOwned; -use std::{env::var, io::Cursor}; +use std::{env, io::Cursor}; const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"]; @@ -20,7 +20,7 @@ pub fn plugin_config(name: &str) -> Option { "TAURI_{}_PLUGIN_CONFIG", name.to_uppercase().replace('-', "_") ); - if let Ok(config_str) = var(&config_env_var_name) { + if let Ok(config_str) = env::var(&config_env_var_name) { println!("cargo:rerun-if-env-changed={config_env_var_name}"); serde_json::from_reader(Cursor::new(config_str)) .map(Some) @@ -105,10 +105,9 @@ impl<'a> Builder<'a> { let _links = std::env::var("CARGO_MANIFEST_LINKS").map_err(|_| Error::LinksMissing)?; let autogenerated = Path::new("permissions").join(acl::build::AUTOGENERATED_FOLDER_NAME); - let commands_dir = autogenerated.join("commands"); - std::fs::create_dir_all(&autogenerated).expect("unable to create permissions dir"); + let commands_dir = autogenerated.join("commands"); if !self.commands.is_empty() { acl::build::autogenerate_command_permissions(&commands_dir, self.commands, "", true); } @@ -120,12 +119,12 @@ impl<'a> Builder<'a> { if permissions.is_empty() { let _ = std::fs::remove_file(format!( "./permissions/{}/{}", - acl::build::PERMISSION_SCHEMAS_FOLDER_NAME, + acl::PERMISSION_SCHEMAS_FOLDER_NAME, acl::PERMISSION_SCHEMA_FILE_NAME )); let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME)); } else { - acl::build::generate_schema(&permissions, "./permissions")?; + acl::schema::generate_permissions_schema(&permissions, "./permissions")?; acl::build::generate_docs( &permissions, &autogenerated, diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 4cc3b65b1ce3..329bed448dde 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -1748,9 +1748,7 @@ "anyOf": [ { "description": "Bundle all targets.", - "enum": [ - "all" - ] + "const": "all" }, { "description": "A list of bundle targets.", diff --git a/crates/tauri-utils/src/acl/build.rs b/crates/tauri-utils/src/acl/build.rs index 188342006520..20beeea2db51 100644 --- a/crates/tauri-utils/src/acl/build.rs +++ b/crates/tauri-utils/src/acl/build.rs @@ -6,21 +6,16 @@ use std::{ collections::{BTreeMap, HashMap}, - env::{current_dir, vars_os}, - fs::{create_dir_all, read_to_string, write}, + env, fs, path::{Path, PathBuf}, }; use crate::{acl::Error, write_if_changed}; -use schemars::{ - schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation}, - schema_for, -}; use super::{ capability::{Capability, CapabilityFile}, manifest::PermissionFile, - PERMISSION_SCHEMA_FILE_NAME, + PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME, }; /// Known name of the folder containing autogenerated permissions. @@ -35,9 +30,6 @@ 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 foldername of the permission schema files -pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas"; - /// Known filename of the permission documentation file pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md"; @@ -49,6 +41,21 @@ const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas"; const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__"; +fn parse_permissions(paths: Vec) -> Result, Error> { + let mut permissions = Vec::new(); + for path in paths { + let permission_file = fs::read_to_string(&path).map_err(Error::ReadFile)?; + let ext = path.extension().unwrap().to_string_lossy().to_string(); + 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 bool>( pattern: &str, @@ -71,13 +78,10 @@ pub fn define_permissions bool>( .filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME) .collect::>(); - let permission_files_path = - out_dir.join(format!("{}-permission-files", pkg_name.replace(':', "-"))); - std::fs::write( - &permission_files_path, - serde_json::to_string(&permission_files)?, - ) - .map_err(Error::WriteFile)?; + let pkg_name_valid_path = pkg_name.replace(':', "-"); + let permission_files_path = out_dir.join(format!("{}-permission-files", pkg_name_valid_path)); + let permission_files_json = serde_json::to_string(&permission_files)?; + fs::write(&permission_files_path, permission_files_json).map_err(Error::WriteFile)?; if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") { println!( @@ -94,6 +98,40 @@ pub fn define_permissions bool>( parse_permissions(permission_files) } +/// Read all permissions listed from the defined cargo cfg key value. +pub fn read_permissions() -> Result>, 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(Error::ReadFile)?; + let permissions: Vec = 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, @@ -101,7 +139,7 @@ pub fn define_global_scope_schema( out_dir: &Path, ) -> Result<(), Error> { let path = out_dir.join("global-scope.json"); - write(&path, serde_json::to_vec(&schema)?).map_err(Error::WriteFile)?; + fs::write(&path, serde_json::to_vec(&schema)?).map_err(Error::WriteFile)?; if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") { println!( @@ -115,13 +153,44 @@ pub fn define_global_scope_schema( Ok(()) } +/// Read all global scope schemas listed from the defined cargo cfg key value. +pub fn read_global_scope_schemas() -> Result, 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(Error::ReadFile)?; + 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( - capabilities_path_pattern: &str, -) -> Result, Error> { +pub fn parse_capabilities(pattern: &str) -> Result, Error> { let mut capabilities_map = BTreeMap::new(); - for path in glob::glob(capabilities_path_pattern)? + for path in glob::glob(pattern)? .flatten() // filter extension .filter(|p| { p.extension() @@ -140,6 +209,7 @@ pub fn parse_capabilities( identifier: capability.identifier, }); } + capabilities_map.insert(capability.identifier.clone(), capability); } CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => { @@ -149,6 +219,7 @@ pub fn parse_capabilities( identifier: capability.identifier, }); } + capabilities_map.insert(capability.identifier.clone(), capability); } } @@ -158,246 +229,6 @@ pub fn parse_capabilities( Ok(capabilities_map) } -fn permissions_schema(permissions: &[PermissionFile]) -> RootSchema { - let mut schema = schema_for!(PermissionFile); - - fn schema_from(id: &str, description: Option<&str>) -> Schema { - Schema::Object(SchemaObject { - metadata: Some(Box::new(Metadata { - description: description.map(|d| format!("{id} -> {d}")), - ..Default::default() - })), - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![serde_json::Value::String(id.into())]), - ..Default::default() - }) - } - - let mut permission_schemas = Vec::new(); - for file in permissions { - if let Some(permission) = &file.default { - permission_schemas.push(schema_from("default", permission.description.as_deref())); - } - - permission_schemas.extend( - file - .set - .iter() - .map(|set| schema_from(&set.identifier, Some(set.description.as_str()))) - .collect::>(), - ); - - permission_schemas.extend( - file - .permission - .iter() - .map(|permission| schema_from(&permission.identifier, permission.description.as_deref())) - .collect::>(), - ); - } - - if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionSet") { - if let Some(Schema::Object(permissions_prop_schema)) = - obj.object().properties.get_mut("permissions") - { - permissions_prop_schema.array().items.replace( - Schema::Object(SchemaObject { - reference: Some("#/definitions/PermissionKind".into()), - ..Default::default() - }) - .into(), - ); - - schema.definitions.insert( - "PermissionKind".into(), - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(permission_schemas), - ..Default::default() - })), - ..Default::default() - }), - ); - } - } - - schema -} - -/// Generate and write a schema based on the format of a [`PermissionFile`]. -pub fn generate_schema>( - permissions: &[PermissionFile], - out_dir: P, -) -> Result<(), Error> { - let schema = permissions_schema(permissions); - let schema_str = serde_json::to_string_pretty(&schema).unwrap(); - - let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME); - create_dir_all(&out_dir).expect("unable to create schema output directory"); - - let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME); - if schema_str != read_to_string(&schema_path).unwrap_or_default() { - write(schema_path, schema_str).map_err(Error::WriteFile)?; - } - - Ok(()) -} - -/// 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 permission_table = "".to_string(); - let permission_table_header = - "## Permission Table \n\n\n\n\n\n\n" - .to_string(); - - let mut default_permission = "## Default Permission\n\n".to_string(); - let mut contains_default = false; - - fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String { - let mut docs = format!("\n\n\n"); - if let Some(d) = description { - docs.push_str(&format!("")); - } - docs.push_str("\n"); - 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.description.as_deref().unwrap_or_default()); - default_permission.push('\n'); - default_permission.push('\n'); - for permission in &default.permissions { - default_permission.push_str(&format!("- `{permission}`")); - default_permission.push('\n'); - } - - contains_default = true; - } - - for permission in &permission.permission { - permission_table.push_str(&docs_from( - &permission.identifier, - permission.description.as_deref(), - plugin_identifier, - )); - permission_table.push('\n'); - } - } - permission_table.push_str("
IdentifierDescription
\n\n`{plugin_identifier}:{id}`\n\n\n\n{d}\n\n
"); - - if !contains_default { - default_permission = "".to_string(); - } - - let docs = format!("{default_permission}\n{permission_table_header}\n{permission_table}\n"); - - let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME); - if docs != read_to_string(&reference_path).unwrap_or_default() { - std::fs::write(reference_path, docs).map_err(Error::WriteFile)?; - } - - Ok(()) -} - -/// Read all permissions listed from the defined cargo cfg key value. -pub fn read_permissions() -> Result>, Error> { - let mut permissions_map = HashMap::new(); - - for (key, value) in 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 = std::fs::read_to_string(&permissions_path).map_err(Error::ReadFile)?; - let permissions: Vec = serde_json::from_str(&permissions_str)?; - let permissions = parse_permissions(permissions)?; - - let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-"); - permissions_map.insert( - plugin_crate_name - .strip_prefix("tauri-plugin-") - .map(|n| n.to_string()) - .unwrap_or(plugin_crate_name), - permissions, - ); - } - } - - Ok(permissions_map) -} - -/// Read all global scope schemas listed from the defined cargo cfg key value. -pub fn read_global_scope_schemas() -> Result, Error> { - let mut permissions_map = HashMap::new(); - - for (key, value) in 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 = std::fs::read_to_string(&path).map_err(Error::ReadFile)?; - let schema: serde_json::Value = serde_json::from_str(&json)?; - - let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-"); - permissions_map.insert( - plugin_crate_name - .strip_prefix("tauri-plugin-") - .map(|n| n.to_string()) - .unwrap_or(plugin_crate_name), - schema, - ); - } - } - - Ok(permissions_map) -} - -fn parse_permissions(paths: Vec) -> Result, Error> { - let mut permissions = Vec::new(); - for path in paths { - let permission_file = std::fs::read_to_string(&path).map_err(Error::ReadFile)?; - let ext = path.extension().unwrap().to_string_lossy().to_string(); - 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) -} - /// Permissions that are generated from commands using [`autogenerate_command_permissions`]. pub struct AutogeneratedPermissions { /// The allow permissions generated from commands. @@ -414,11 +245,11 @@ pub fn autogenerate_command_permissions( schema_ref: bool, ) -> AutogeneratedPermissions { if !path.exists() { - create_dir_all(path).expect("unable to create autogenerated commands dir"); + fs::create_dir_all(path).expect("unable to create autogenerated commands dir"); } let schema_entry = if schema_ref { - let cwd = current_dir().unwrap(); + 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(|_| "..") @@ -473,3 +304,71 @@ commands.deny = ["{command}"] autogenerated } + +const PERMISSION_TABLE_HEADER: &str = + "## Permission Table \n\n\n\n\n\n\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 permission_table = "".to_string(); + + let mut default_permission = "## Default Permission\n\n".to_string(); + let mut contains_default = false; + + fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String { + let mut docs = format!("\n\n\n"); + if let Some(d) = description { + docs.push_str(&format!("")); + } + docs.push_str("\n"); + 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 { + contains_default = true; + + default_permission.push_str(default.description.as_deref().unwrap_or_default()); + default_permission.push('\n'); + default_permission.push('\n'); + for permission in &default.permissions { + default_permission.push_str(&format!("- `{permission}`")); + 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'); + } + } + + if !contains_default { + default_permission = "".to_string(); + } + + let docs = + format!("{default_permission}\n{PERMISSION_TABLE_HEADER}\n{permission_table}
IdentifierDescription
\n\n`{plugin_identifier}:{id}`\n\n\n\n{d}\n\n
\n"); + + let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME); + write_if_changed(reference_path, docs).map_err(Error::WriteFile)?; + + Ok(()) +} diff --git a/crates/tauri-utils/src/acl/capability.rs b/crates/tauri-utils/src/acl/capability.rs index ebfdba33b87c..38d3099e1dd2 100644 --- a/crates/tauri-utils/src/acl/capability.rs +++ b/crates/tauri-utils/src/acl/capability.rs @@ -195,6 +195,17 @@ pub struct Capability { pub platforms: Option>, } +impl Capability { + /// Whether this capability should be active based on the platform target or not. + pub fn is_active(&self, target: &Target) -> bool { + self + .platforms + .as_ref() + .map(|platforms| platforms.contains(target)) + .unwrap_or(true) + } +} + #[cfg(feature = "schema")] fn unique_permission(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { use schemars::schema; diff --git a/crates/tauri-utils/src/acl/manifest.rs b/crates/tauri-utils/src/acl/manifest.rs index 7c0af899efa6..ddba46f21967 100644 --- a/crates/tauri-utils/src/acl/manifest.rs +++ b/crates/tauri-utils/src/acl/manifest.rs @@ -7,6 +7,8 @@ use std::{collections::BTreeMap, num::NonZeroU64}; use super::{Permission, PermissionSet}; +#[cfg(feature = "schema")] +use schemars::schema::*; use serde::{Deserialize, Serialize}; /// The default permission set of the plugin. @@ -44,7 +46,7 @@ pub struct PermissionFile { } /// Plugin manifest. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] pub struct Manifest { /// Default permission. pub default_permission: Option, @@ -80,36 +82,50 @@ impl Manifest { }); } - manifest.permissions.extend( - permission_file - .permission - .into_iter() - .map(|p| (p.identifier.clone(), p)) - .collect::>(), - ); + for permission in permission_file.permission { + let key = permission.identifier.clone(); + manifest.permissions.insert(key, permission); + } - manifest.permission_sets.extend( - permission_file - .set - .into_iter() - .map(|set| { - ( - set.identifier.clone(), - PermissionSet { - identifier: set.identifier, - description: set.description, - permissions: set.permissions, - }, - ) - }) - .collect::>(), - ); + for set in permission_file.set { + let key = set.identifier.clone(); + manifest.permission_sets.insert(key, set); + } } manifest } } +#[cfg(feature = "schema")] +type ScopeSchema = (Schema, schemars::Map); + +#[cfg(feature = "schema")] +impl Manifest { + /// Return scope schema and extra schema definitions for this plugin manifest. + pub fn global_scope_schema(&self) -> Result, super::Error> { + self + .global_scope_schema + .as_ref() + .map(|s| { + serde_json::from_value::(s.clone()).map(|s| { + // convert RootSchema to Schema + let scope_schema = Schema::Object(SchemaObject { + array: Some(Box::new(ArrayValidation { + items: Some(Schema::Object(s.schema).into()), + ..Default::default() + })), + ..Default::default() + }); + + (scope_schema, s.definitions) + }) + }) + .transpose() + .map_err(Into::into) + } +} + #[cfg(feature = "build")] mod build { use proc_macro2::TokenStream; diff --git a/crates/tauri-utils/src/acl/mod.rs b/crates/tauri-utils/src/acl/mod.rs index bd21c84c3376..3a48b377d763 100644 --- a/crates/tauri-utils/src/acl/mod.rs +++ b/crates/tauri-utils/src/acl/mod.rs @@ -30,10 +30,16 @@ use crate::platform::Target; pub use self::{identifier::*, value::*}; +/// Known foldername of the permission schema files +pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas"; /// Known filename of the permission schema JSON file pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json"; /// Known ACL key for the app permissions. pub const APP_ACL_KEY: &str = "__app-acl__"; +/// Known acl manifests file +pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; +/// Known capabilityies file +pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; #[cfg(feature = "build")] pub mod build; @@ -41,6 +47,8 @@ pub mod capability; pub mod identifier; pub mod manifest; pub mod resolved; +#[cfg(feature = "schema")] +pub mod schema; pub mod value; /// Possible errors while processing ACL files. @@ -74,6 +82,10 @@ pub enum Error { #[error("failed to create file: {0}")] CreateFile(std::io::Error), + /// IO error while creating a dir + #[error("failed to create dir: {0}")] + CreateDir(std::io::Error), + /// [`cargo_metadata`] was not able to complete successfully #[cfg(feature = "build")] #[error("failed to execute: {0}")] @@ -185,7 +197,7 @@ impl Scopes { /// It can enable commands to be accessible in the frontend of the application. /// /// If the scope is defined it can be used to fine grain control the access of individual or multiple commands. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Permission { /// The version of the permission. @@ -214,6 +226,17 @@ pub struct Permission { pub platforms: Option>, } +impl Permission { + /// Whether this permission should be active based on the platform target or not. + pub fn is_active(&self, target: &Target) -> bool { + self + .platforms + .as_ref() + .map(|platforms| platforms.contains(target)) + .unwrap_or(true) + } +} + /// A set of direct permissions grouped together under a new name. #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] diff --git a/crates/tauri-utils/src/acl/resolved.rs b/crates/tauri-utils/src/acl/resolved.rs index 7bf841b4fbaf..75a3d4e46dae 100644 --- a/crates/tauri-utils/src/acl/resolved.rs +++ b/crates/tauri-utils/src/acl/resolved.rs @@ -11,24 +11,13 @@ use crate::platform::Target; use super::{ capability::{Capability, PermissionEntry}, manifest::Manifest, - Commands, Error, ExecutionContext, Permission, PermissionSet, Scopes, Value, APP_ACL_KEY, + Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes, Value, + APP_ACL_KEY, }; /// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`]. pub type ScopeKey = u64; -const CORE_PLUGINS: &[&str] = &[ - "core:app", - "core:event", - "core:image", - "core:menu", - "core:path", - "core:resources", - "core:tray", - "core:webview", - "core:window", -]; - /// Metadata for what referenced a [`ResolvedCommand`]. #[cfg(debug_assertions)] #[derive(Default, Clone, PartialEq, Eq)] @@ -103,39 +92,17 @@ impl Resolved { let mut global_scope: BTreeMap> = BTreeMap::new(); // resolve commands - for capability in capabilities.values_mut() { - if !capability - .platforms - .as_ref() - .map(|platforms| platforms.contains(&target)) - .unwrap_or(true) - { - continue; - } - - if let Some(core_default_index) = capability.permissions.iter().position(|permission| { - matches!( - permission, - PermissionEntry::PermissionRef(i) if i.get() == "core:default" - ) - }) { - capability.permissions.remove(core_default_index); - for plugin in CORE_PLUGINS { - capability.permissions.push(PermissionEntry::PermissionRef( - format!("{plugin}:default").try_into().unwrap(), - )); - } - } - + for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) { with_resolved_permissions( capability, acl, target, |ResolvedPermission { key, - permission_name, commands, scope, + #[cfg_attr(not(debug_assertions), allow(unused))] + permission_name, }| { if commands.allow.is_empty() && commands.deny.is_empty() { // global scope @@ -236,6 +203,46 @@ fn parse_glob_patterns(mut raw: Vec) -> Result, Error Ok(patterns) } +fn resolve_command( + commands: &mut BTreeMap>, + command: String, + capability: &Capability, + scope_id: Option, + #[cfg(debug_assertions)] referenced_by_permission_identifier: String, +) -> Result<(), Error> { + let mut contexts = Vec::new(); + if capability.local { + contexts.push(ExecutionContext::Local); + } + if let Some(remote) = &capability.remote { + contexts.extend(remote.urls.iter().map(|url| { + ExecutionContext::Remote { + url: url + .parse() + .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")), + } + })); + } + + for context in contexts { + let resolved_list = commands.entry(command.clone()).or_default(); + + resolved_list.push(ResolvedCommand { + context, + #[cfg(debug_assertions)] + referenced_by: ResolvedCommandReference { + capability: capability.identifier.clone(), + permission: referenced_by_permission_identifier.clone(), + }, + windows: parse_glob_patterns(capability.windows.clone())?, + webviews: parse_glob_patterns(capability.webviews.clone())?, + scope_id, + }); + } + + Ok(()) +} + struct ResolvedPermission<'a> { key: &'a str, permission_name: &'a str, @@ -243,6 +250,8 @@ struct ResolvedPermission<'a> { scope: Scopes, } +/// Iterate over permissions in a capability, resolving permission sets if necessary +/// to produce a [`ResolvedPermission`] and calling the provided callback with it. fn with_resolved_permissions) -> Result<(), Error>>( capability: &Capability, acl: &BTreeMap, @@ -251,43 +260,39 @@ fn with_resolved_permissions) -> Result<(), Erro ) -> Result<(), Error> { for permission_entry in &capability.permissions { let permission_id = permission_entry.identifier(); - let permission_name = permission_id.get_base(); - - let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY); - let permissions = get_permissions(key, permission_name, acl)? + let permissions = get_permissions(permission_id, acl)? .into_iter() - .filter(|p| { - p.platforms - .as_ref() - .map(|platforms| platforms.contains(&target)) - .unwrap_or(true) - }) - .collect::>(); - - let mut resolved_scope = Scopes::default(); - let mut commands = Commands::default(); + .filter(|p| p.permission.is_active(&target)); - if let PermissionEntry::ExtendedPermission { - identifier: _, - scope, - } = permission_entry + for TraversedPermission { + key, + permission_name, + permission, + } in permissions { - if let Some(allow) = scope.allow.clone() { - resolved_scope - .allow - .get_or_insert_with(Default::default) - .extend(allow); - } - if let Some(deny) = scope.deny.clone() { - resolved_scope - .deny - .get_or_insert_with(Default::default) - .extend(deny); + let mut resolved_scope = Scopes::default(); + let mut commands = Commands::default(); + + if let PermissionEntry::ExtendedPermission { + identifier: _, + scope, + } = permission_entry + { + if let Some(allow) = scope.allow.clone() { + resolved_scope + .allow + .get_or_insert_with(Default::default) + .extend(allow); + } + if let Some(deny) = scope.deny.clone() { + resolved_scope + .deny + .get_or_insert_with(Default::default) + .extend(deny); + } } - } - for permission in permissions { if let Some(allow) = permission.scope.allow.clone() { resolved_scope .allow @@ -303,74 +308,110 @@ fn with_resolved_permissions) -> Result<(), Erro commands.allow.extend(permission.commands.allow.clone()); commands.deny.extend(permission.commands.deny.clone()); - } - f(ResolvedPermission { - key, - permission_name, - commands, - scope: resolved_scope, - })?; + f(ResolvedPermission { + key: &key, + permission_name: &permission_name, + commands, + scope: resolved_scope, + })?; + } } Ok(()) } -fn resolve_command( - commands: &mut BTreeMap>, - command: String, - capability: &Capability, - scope_id: Option, - #[cfg(debug_assertions)] referenced_by_permission_identifier: String, -) -> Result<(), Error> { - let mut contexts = Vec::new(); - if capability.local { - contexts.push(ExecutionContext::Local); - } - if let Some(remote) = &capability.remote { - contexts.extend(remote.urls.iter().map(|url| { - ExecutionContext::Remote { - url: url - .parse() - .unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")), - } - })); - } +#[derive(Debug)] +struct TraversedPermission<'a> { + key: String, + permission_name: String, + permission: &'a Permission, +} - for context in contexts { - let resolved_list = commands.entry(command.clone()).or_default(); +fn get_permissions<'a>( + permission_id: &Identifier, + acl: &'a BTreeMap, +) -> Result>, Error> { + let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY); + let permission_name = permission_id.get_base(); - resolved_list.push(ResolvedCommand { - context, - #[cfg(debug_assertions)] - referenced_by: ResolvedCommandReference { - capability: capability.identifier.clone(), - permission: referenced_by_permission_identifier.clone(), - }, - windows: parse_glob_patterns(capability.windows.clone())?, - webviews: parse_glob_patterns(capability.webviews.clone())?, - scope_id, - }); - } + let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest { + key: display_perm_key(key).to_string(), + available: acl.keys().cloned().collect::>().join(", "), + })?; - Ok(()) + if permission_name == "default" { + manifest + .default_permission + .as_ref() + .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default)) + .unwrap_or_else(|| Ok(Default::default())) + } else if let Some(set) = manifest.permission_sets.get(permission_name) { + get_permission_set_permissions(permission_id, acl, manifest, set) + } else if let Some(permission) = manifest.permissions.get(permission_name) { + Ok(vec![TraversedPermission { + key: key.to_string(), + permission_name: permission_name.to_string(), + permission, + }]) + } else { + Err(Error::UnknownPermission { + key: display_perm_key(key).to_string(), + permission: permission_name.to_string(), + }) + } } // get the permissions from a permission set fn get_permission_set_permissions<'a>( + permission_id: &Identifier, + acl: &'a BTreeMap, manifest: &'a Manifest, set: &'a PermissionSet, -) -> Result, Error> { +) -> Result>, Error> { + let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY); + let mut permissions = Vec::new(); - for p in &set.permissions { - if let Some(permission) = manifest.permissions.get(p) { - permissions.push(permission); - } else if let Some(permission_set) = manifest.permission_sets.get(p) { - permissions.extend(get_permission_set_permissions(manifest, permission_set)?); + for perm in &set.permissions { + // a set could include permissions from other plugins + // for example `dialog:default`, could include `fs:default` + // in this case `perm = "fs:default"` which is not a permission + // in the dialog manifest so we check if `perm` still have a prefix (i.e `fs:`) + // and if so, we resolve this prefix from `acl` first before proceeding + let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?"); + let (manifest, permission_id, key, permission_name) = + if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) { + (manifest, &id, new_key, id.get_base()) + } else { + (manifest, permission_id, key, perm.as_str()) + }; + + if permission_name == "default" { + permissions.extend( + manifest + .default_permission + .as_ref() + .map(|default| get_permission_set_permissions(permission_id, acl, manifest, default)) + .transpose()? + .unwrap_or_default(), + ); + } else if let Some(permission) = manifest.permissions.get(permission_name) { + permissions.push(TraversedPermission { + key: key.to_string(), + permission_name: permission_name.to_string(), + permission, + }); + } else if let Some(permission_set) = manifest.permission_sets.get(permission_name) { + permissions.extend(get_permission_set_permissions( + permission_id, + acl, + manifest, + permission_set, + )?); } else { return Err(Error::SetPermissionNotFound { - permission: p.to_string(), + permission: permission_name.to_string(), set: set.identifier.clone(), }); } @@ -379,39 +420,12 @@ fn get_permission_set_permissions<'a>( Ok(permissions) } -fn get_permissions<'a>( - key: &'a str, - permission_name: &'a str, - acl: &'a BTreeMap, -) -> Result, Error> { - let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest { - key: if key == APP_ACL_KEY { - "app manifest".to_string() - } else { - key.to_string() - }, - available: acl.keys().cloned().collect::>().join(", "), - })?; - - if permission_name == "default" { - manifest - .default_permission - .as_ref() - .map(|default| get_permission_set_permissions(manifest, default)) - .unwrap_or_else(|| Ok(Vec::new())) - } else if let Some(set) = manifest.permission_sets.get(permission_name) { - get_permission_set_permissions(manifest, set) - } else if let Some(permission) = manifest.permissions.get(permission_name) { - Ok(vec![permission]) +#[inline] +fn display_perm_key(prefix: &str) -> &str { + if prefix == APP_ACL_KEY { + "app manifest" } else { - Err(Error::UnknownPermission { - key: if key == APP_ACL_KEY { - "app manifest".to_string() - } else { - key.to_string() - }, - permission: permission_name.to_string(), - }) + prefix } } @@ -533,3 +547,125 @@ mod build { } } } + +#[cfg(test)] +mod tests { + + use super::{get_permissions, Identifier, Manifest, Permission, PermissionSet}; + + fn manifest( + name: &str, + permissions: [&str; P], + default_set: Option<&[&str]>, + sets: [(&str, &[&str]); S], + ) -> (String, Manifest) { + ( + name.to_string(), + Manifest { + default_permission: default_set.map(|perms| PermissionSet { + identifier: "default".to_string(), + description: "default set".to_string(), + permissions: perms.iter().map(|s| s.to_string()).collect(), + }), + permissions: permissions + .iter() + .map(|p| { + ( + p.to_string(), + Permission { + identifier: p.to_string(), + ..Default::default() + }, + ) + }) + .collect(), + permission_sets: sets + .iter() + .map(|(s, perms)| { + ( + s.to_string(), + PermissionSet { + identifier: s.to_string(), + description: format!("{s} set"), + permissions: perms.iter().map(|s| s.to_string()).collect(), + }, + ) + }) + .collect(), + ..Default::default() + }, + ) + } + + fn id(id: &str) -> Identifier { + Identifier::try_from(id.to_string()).unwrap() + } + + #[test] + fn resolves_permissions_from_other_plugins() { + let acl = [ + manifest( + "fs", + ["read", "write", "rm", "exist"], + Some(&["read", "exist"]), + [], + ), + manifest( + "http", + ["fetch", "fetch-cancel"], + None, + [("fetch-with-cancel", &["fetch", "fetch-cancel"])], + ), + manifest( + "dialog", + ["open", "save"], + None, + [( + "extra", + &[ + "save", + "fs:default", + "fs:write", + "http:default", + "http:fetch-with-cancel", + ], + )], + ), + ] + .into(); + + let permissions = get_permissions(&id("fs:default"), &acl).unwrap(); + assert_eq!(permissions.len(), 2); + assert_eq!(permissions[0].key, "fs"); + assert_eq!(permissions[0].permission_name, "read"); + assert_eq!(permissions[1].key, "fs"); + assert_eq!(permissions[1].permission_name, "exist"); + + let permissions = get_permissions(&id("fs:rm"), &acl).unwrap(); + assert_eq!(permissions.len(), 1); + assert_eq!(permissions[0].key, "fs"); + assert_eq!(permissions[0].permission_name, "rm"); + + let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap(); + assert_eq!(permissions.len(), 2); + assert_eq!(permissions[0].key, "http"); + assert_eq!(permissions[0].permission_name, "fetch"); + assert_eq!(permissions[1].key, "http"); + assert_eq!(permissions[1].permission_name, "fetch-cancel"); + + let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap(); + assert_eq!(permissions.len(), 6); + assert_eq!(permissions[0].key, "dialog"); + assert_eq!(permissions[0].permission_name, "save"); + assert_eq!(permissions[1].key, "fs"); + assert_eq!(permissions[1].permission_name, "read"); + assert_eq!(permissions[2].key, "fs"); + assert_eq!(permissions[2].permission_name, "exist"); + assert_eq!(permissions[3].key, "fs"); + assert_eq!(permissions[3].permission_name, "write"); + assert_eq!(permissions[4].key, "http"); + assert_eq!(permissions[4].permission_name, "fetch"); + assert_eq!(permissions[5].key, "http"); + assert_eq!(permissions[5].permission_name, "fetch-cancel"); + } +} diff --git a/crates/tauri-utils/src/acl/schema.rs b/crates/tauri-utils/src/acl/schema.rs new file mode 100644 index 000000000000..bdce02ea013a --- /dev/null +++ b/crates/tauri-utils/src/acl/schema.rs @@ -0,0 +1,345 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Schema generation for ACL items. + +use std::{ + collections::{btree_map::Values, BTreeMap}, + fs, + path::{Path, PathBuf}, + slice::Iter, +}; + +use schemars::schema::*; + +use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME}; +use crate::{platform::Target, write_if_changed}; + +use super::{ + capability::CapabilityFile, + manifest::{Manifest, PermissionFile}, + Permission, PermissionSet, PERMISSION_SCHEMA_FILE_NAME, +}; + +/// Capability schema file name. +pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json"; +/// Path of the folder where schemas are saved. +pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas"; + +// TODO: once MSRV is high enough, remove generic and use impl +// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42 +/// Permission schema generator trait +pub trait PermissionSchemaGenerator< + 'a, + Ps: Iterator, + P: Iterator, +> +{ + /// Whether has a default permission set or not. + fn has_default_permission_set(&self) -> bool; + + /// Default permission set description if any. + fn default_set_description(&self) -> Option<&str>; + + /// Permissions sets to generate schema for. + fn permission_sets(&'a self) -> Ps; + + /// Permissions to generate schema for. + fn permissions(&'a self) -> P; + + /// A utility function to generate a schema for a permission identifier + fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema { + let command_name = match name { + Some(name) if name == super::APP_ACL_KEY => id.to_string(), + Some(name) => format!("{name}:{id}"), + _ => id.to_string(), + }; + + Schema::Object(SchemaObject { + metadata: Some(Box::new(Metadata { + description: description.map(ToString::to_string), + ..Default::default() + })), + instance_type: Some(InstanceType::String.into()), + const_value: Some(serde_json::Value::String(command_name)), + ..Default::default() + }) + } + + /// Generate schemas for all possible permissions. + fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec { + let mut permission_schemas = Vec::new(); + + // schema for default set + if self.has_default_permission_set() { + let default = Self::perm_id_schema(name, "default", self.default_set_description()); + permission_schemas.push(default); + } + + // schema for each permission set + for set in self.permission_sets() { + let schema = Self::perm_id_schema(name, &set.identifier, Some(&set.description)); + permission_schemas.push(schema); + } + + // schema for each permission + for perm in self.permissions() { + let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref()); + permission_schemas.push(schema); + } + + permission_schemas + } +} + +impl<'a> + PermissionSchemaGenerator< + 'a, + Values<'a, std::string::String, PermissionSet>, + Values<'a, std::string::String, Permission>, + > for Manifest +{ + fn has_default_permission_set(&self) -> bool { + self.default_permission.is_some() + } + + fn default_set_description(&self) -> Option<&str> { + self + .default_permission + .as_ref() + .map(|d| d.description.as_str()) + } + + fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> { + self.permission_sets.values() + } + + fn permissions(&'a self) -> Values<'a, std::string::String, Permission> { + self.permissions.values() + } +} + +impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>> + for PermissionFile +{ + fn has_default_permission_set(&self) -> bool { + self.default.is_some() + } + + fn default_set_description(&self) -> Option<&str> { + self.default.as_ref().and_then(|d| d.description.as_deref()) + } + + fn permission_sets(&'a self) -> Iter<'a, PermissionSet> { + self.set.iter() + } + + fn permissions(&'a self) -> Iter<'a, Permission> { + self.permission.iter() + } +} + +/// Collect and include all possible identifiers in `Identifier` defintion in the schema +fn extend_identifier_schema(schema: &mut RootSchema, acl: &BTreeMap) { + if let Some(Schema::Object(identifier_schema)) = schema.definitions.get_mut("Identifier") { + let permission_schemas = acl + .iter() + .flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name))) + .collect::>(); + + let new_subschemas = Box::new(SubschemaValidation { + one_of: Some(permission_schemas.clone()), + ..Default::default() + }); + + identifier_schema.subschemas = Some(new_subschemas); + identifier_schema.object = None; + identifier_schema.instance_type = None; + identifier_schema.metadata().description = Some("Permission identifier".to_string()); + } +} + +/// Collect permission schemas and its associated scope schema and schema definitons from plugins +/// and replace `PermissionEntry` extend object syntax with a new schema that does conditional +/// checks to serve the relavent scope schema for the right permissions schema, in a nutshell, it +/// will look something like this: +/// ```text +/// PermissionEntry { +/// anyOf { +/// String, // default string syntax +/// Object { // extended object syntax +/// allOf { // JSON allOf is used but actually means anyOf +/// { +/// "if": "identifier" property anyOf "fs" plugin permission, +/// "then": add "allow" and "deny" properties that match "fs" plugin scope schema +/// }, +/// { +/// "if": "identifier" property anyOf "http" plugin permission, +/// "then": add "allow" and "deny" properties that match "http" plugin scope schema +/// }, +/// ...etc, +/// { +/// No "if" or "then", just "allow" and "deny" properties with default "#/definitions/Value" +/// }, +/// } +/// } +/// } +/// } +/// ``` +fn extend_permission_entry_schema(root_schema: &mut RootSchema, acl: &BTreeMap) { + const IDENTIFIER: &str = "identifier"; + const ALLOW: &str = "allow"; + const DENY: &str = "deny"; + + let mut collected_defs = vec![]; + + if let Some(Schema::Object(obj)) = root_schema.definitions.get_mut("PermissionEntry") { + let any_of = obj.subschemas().any_of.as_mut().unwrap(); + let Schema::Object(extened_permission_entry) = any_of.last_mut().unwrap() else { + unreachable!("PermissionsEntry should be an object not a boolean"); + }; + + // remove default properties and save it to be added later as a fallback + let obj = extened_permission_entry.object.as_mut().unwrap(); + let default_properties = std::mem::take(&mut obj.properties); + + let defaut_identifier = default_properties.get(IDENTIFIER).cloned().unwrap(); + let default_identifier = (IDENTIFIER.to_string(), defaut_identifier); + + let mut all_of = vec![]; + + let schemas = acl.iter().filter_map(|(name, manifest)| { + manifest + .global_scope_schema() + .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}")) + .map(|s| (s, manifest.gen_possible_permission_schemas(Some(name)))) + }); + + for ((scope_schema, defs), acl_perm_schema) in schemas { + let mut perm_schema = SchemaObject::default(); + perm_schema.subschemas().any_of = Some(acl_perm_schema); + + let mut if_schema = SchemaObject::default(); + if_schema.object().properties = [(IDENTIFIER.to_string(), perm_schema.into())].into(); + + let mut then_schema = SchemaObject::default(); + then_schema.object().properties = [ + (ALLOW.to_string(), scope_schema.clone()), + (DENY.to_string(), scope_schema.clone()), + ] + .into(); + + let mut obj = SchemaObject::default(); + obj.object().properties = [default_identifier.clone()].into(); + obj.subschemas().if_schema = Some(Box::new(if_schema.into())); + obj.subschemas().then_schema = Some(Box::new(then_schema.into())); + + all_of.push(Schema::Object(obj)); + collected_defs.extend(defs); + } + + // add back default properties as a fallback + let mut default_obj = SchemaObject::default(); + default_obj.object().properties = default_properties; + all_of.push(Schema::Object(default_obj)); + + // replace extended PermissionEntry with the new schema + extened_permission_entry.subschemas().all_of = Some(all_of); + } + + // extend root schema with definitions collected from plugins + root_schema.definitions.extend(collected_defs); +} + +/// Generate schema for CapabilityFile with all possible plugins permissions +pub fn generate_capability_schema( + acl: &BTreeMap, + target: Target, +) -> crate::Result<()> { + let mut schema = schemars::schema_for!(CapabilityFile); + + extend_identifier_schema(&mut schema, acl); + extend_permission_entry_schema(&mut schema, acl); + + let schema_str = serde_json::to_string_pretty(&schema).unwrap(); + let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH); + fs::create_dir_all(&out_dir)?; + + let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}")); + if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() { + fs::write(&schema_path, schema_str)?; + + fs::copy( + schema_path, + out_dir.join(format!( + "{}-{CAPABILITIES_SCHEMA_FILE_NAME}", + if target.is_desktop() { + "desktop" + } else { + "mobile" + } + )), + )?; + } + + Ok(()) +} + +/// Extend schema with collected permissions from the passed [`PermissionFile`]s. +fn extend_permission_file_schema(schema: &mut RootSchema, permissions: &[PermissionFile]) { + // collect possible permissions + let permission_schemas = permissions + .iter() + .flat_map(|p| p.gen_possible_permission_schemas(None)) + .collect(); + + if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionSet") { + let permissions_obj = obj.object().properties.get_mut("permissions"); + if let Some(Schema::Object(permissions_obj)) = permissions_obj { + // replace the permissions property schema object + // from a mere string to a referecnce to `PermissionKind` + permissions_obj.array().items.replace( + Schema::Object(SchemaObject { + reference: Some("#/definitions/PermissionKind".into()), + ..Default::default() + }) + .into(), + ); + + // add the new `PermissionKind` definition in the schema that + // is a list of all possible permissions collected + schema.definitions.insert( + "PermissionKind".into(), + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(permission_schemas), + ..Default::default() + })), + ..Default::default() + }), + ); + } + } +} + +/// Generate and write a schema based on the format of a [`PermissionFile`]. +pub fn generate_permissions_schema>( + permissions: &[PermissionFile], + out_dir: P, +) -> Result<(), Error> { + let mut schema = schemars::schema_for!(PermissionFile); + + extend_permission_file_schema(&mut schema, permissions); + + let schema_str = serde_json::to_string_pretty(&schema)?; + + let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME); + fs::create_dir_all(&out_dir).map_err(Error::CreateDir)?; + + let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME); + write_if_changed(&schema_path, schema_str).map_err(Error::WriteFile)?; + + Ok(()) +} diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 98c840271603..28a25eaa913a 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -212,7 +212,7 @@ impl schemars::JsonSchema for BundleTarget { fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { let any_of = vec![ schemars::schema::SchemaObject { - enum_values: Some(vec!["all".into()]), + const_value: Some("all".into()), metadata: Some(Box::new(schemars::schema::Metadata { description: Some("Bundle all targets.".to_owned()), ..Default::default() diff --git a/crates/tauri-utils/src/plugin.rs b/crates/tauri-utils/src/plugin.rs index 4255d0aa268f..d92883f4c563 100644 --- a/crates/tauri-utils/src/plugin.rs +++ b/crates/tauri-utils/src/plugin.rs @@ -10,6 +10,7 @@ pub use build::*; mod build { use std::{ env::vars_os, + fs, path::{Path, PathBuf}, }; @@ -30,7 +31,7 @@ mod build { /// Collects the path of all the global API scripts defined with [`define_global_api_script_path`] /// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]. - pub fn load_global_api_scripts(out_dir: &Path) { + pub fn save_global_api_scripts_paths(out_dir: &Path) { let mut scripts = Vec::new(); for (key, value) in vars_os() { @@ -42,10 +43,37 @@ mod build { } } - std::fs::write( + fs::write( out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH), serde_json::to_string(&scripts).expect("failed to serialize global API script paths"), ) .expect("failed to write global API script"); } + + /// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`] + pub fn read_global_api_scripts(out_dir: &Path) -> Option> { + let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH); + if !global_scripts_path.exists() { + return None; + } + + let global_scripts_str = fs::read_to_string(global_scripts_path) + .expect("failed to read plugin global API script paths"); + let global_scripts = serde_json::from_str::>(&global_scripts_str) + .expect("failed to parse plugin global API script paths"); + + Some( + global_scripts + .into_iter() + .map(|p| { + fs::read_to_string(&p).unwrap_or_else(|e| { + panic!( + "failed to read plugin global API script {}: {e}", + p.display() + ) + }) + }) + .collect(), + ) + } } diff --git a/crates/tauri-utils/src/resources.rs b/crates/tauri-utils/src/resources.rs index ee2ef1b94b9c..b040af39c6dc 100644 --- a/crates/tauri-utils/src/resources.rs +++ b/crates/tauri-utils/src/resources.rs @@ -580,8 +580,6 @@ mod tests { .iter() .collect::>(); - dbg!(&resources); - assert_eq!(resources.len(), 4); assert!(resources.iter().all(|r| r.is_err())); diff --git a/crates/tauri/build.rs b/crates/tauri/build.rs index 5430b2398499..f4622315c04c 100644 --- a/crates/tauri/build.rs +++ b/crates/tauri/build.rs @@ -5,12 +5,8 @@ use heck::AsShoutySnakeCase; use tauri_utils::write_if_changed; -use std::env::var_os; -use std::fs::create_dir_all; -use std::fs::read_dir; -use std::fs::read_to_string; use std::{ - env::var, + env, fs, path::{Path, PathBuf}, sync::{Mutex, OnceLock}, }; @@ -18,7 +14,6 @@ use std::{ static CHECKED_FEATURES: OnceLock>> = OnceLock::new(); const PLUGINS: &[(&str, &[(&str, bool)])] = &[ // (plugin_name, &[(command, enabled-by_default)]) - // note that when adding new core plugins, they must be added to the ACL resolver aswell ( "core:path", &[ @@ -240,7 +235,7 @@ fn main() { alias("desktop", !mobile); alias("mobile", mobile); - let out_dir = PathBuf::from(var("OUT_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let checked_features_out_path = out_dir.join("checked_features"); std::fs::write( @@ -278,12 +273,12 @@ fn main() { PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen"); println!("cargo:rerun-if-changed={}", kotlin_files_path.display()); let kotlin_files = - read_dir(kotlin_files_path).expect("failed to read Android codegen directory"); + fs::read_dir(kotlin_files_path).expect("failed to read Android codegen directory"); for file in kotlin_files { let file = file.unwrap(); - let content = read_to_string(file.path()) + let content = fs::read_to_string(file.path()) .expect("failed to read kotlin file as string") .replace("{{package}}", &package) .replace("{{library}}", &library); @@ -296,10 +291,11 @@ fn main() { } } - if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { let tauri_proguard = include_str!("./mobile/proguard-tauri.pro").replace( "$PACKAGE", - &var("WRY_ANDROID_PACKAGE").expect("missing `WRY_ANDROID_PACKAGE` environment variable"), + &env::var("WRY_ANDROID_PACKAGE") + .expect("missing `WRY_ANDROID_PACKAGE` environment variable"), ); std::fs::write( project_dir.join("app").join("proguard-tauri.pro"), @@ -326,12 +322,12 @@ fn main() { define_permissions(&out_dir); } -fn define_permissions(out_dir: &Path) { - let license_header = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy # SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: MIT "; +fn define_permissions(out_dir: &Path) { for (plugin, commands) in PLUGINS { let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin); let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name); @@ -342,7 +338,7 @@ fn define_permissions(out_dir: &Path) { tauri_utils::acl::build::autogenerate_command_permissions( &commands_dir, &commands.iter().map(|(cmd, _)| *cmd).collect::>(), - license_header, + LICENSE_HEADER, false, ); let default_permissions = commands @@ -356,7 +352,7 @@ fn define_permissions(out_dir: &Path) { .join(", "); let default_toml = format!( - r###"{license_header}# Automatically generated - DO NOT EDIT! + r###"{LICENSE_HEADER}# Automatically generated - DO NOT EDIT! [default] description = "Default permissions for the plugin." @@ -365,10 +361,8 @@ permissions = [{default_permissions}] ); let out_path = autogenerated.join("default.toml"); - if default_toml != read_to_string(&out_path).unwrap_or_default() { - std::fs::write(out_path, default_toml) - .unwrap_or_else(|_| panic!("unable to autogenerate default permissions")); - } + write_if_changed(out_path, default_toml) + .unwrap_or_else(|_| panic!("unable to autogenerate default permissions")); let permissions = tauri_utils::acl::build::define_permissions( &permissions_out_dir @@ -384,7 +378,7 @@ permissions = [{default_permissions}] let docs_out_dir = Path::new("permissions") .join(plugin_directory_name) .join("autogenerated"); - create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory"); + fs::create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory"); tauri_utils::acl::build::generate_docs( &permissions, &docs_out_dir, @@ -392,6 +386,47 @@ permissions = [{default_permissions}] ) .expect("failed to generate plugin documentation page"); } + + define_default_permission_set(out_dir); +} + +fn define_default_permission_set(out_dir: &Path) { + let permissions_out_dir = out_dir.join("permissions"); + fs::create_dir_all(&permissions_out_dir) + .expect("failed to create core:default permissions directory"); + + let default_toml = permissions_out_dir.join("default.toml"); + let toml_content = format!( + r#"# {LICENSE_HEADER} + +[default] +description = """Default core plugins set which includes: +{} +""" +permissions = [{}] +"#, + PLUGINS + .iter() + .map(|(k, _)| format!("- '{k}:default'")) + .collect::>() + .join("\n"), + PLUGINS + .iter() + .map(|(k, _)| format!("'{k}:default'")) + .collect::>() + .join(",") + ); + + write_if_changed(&default_toml, toml_content) + .unwrap_or_else(|_| panic!("unable to autogenerate core:default set")); + + let _ = tauri_utils::acl::build::define_permissions( + &permissions_out_dir.join("*.toml").to_string_lossy(), + "tauri:core", + out_dir, + |_| true, + ) + .unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}")); } fn embed_manifest_for_tests() { diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json b/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json index e355bb7a52a4..73caf69daefc 100644 --- a/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json @@ -295,32 +295,24 @@ "type": "string", "oneOf": [ { - "description": "allow-ping -> Enables the ping command without any pre-configured scope.", + "description": "Enables the ping command without any pre-configured scope.", "type": "string", - "enum": [ - "allow-ping" - ] + "const": "allow-ping" }, { - "description": "deny-ping -> Denies the ping command without any pre-configured scope.", + "description": "Denies the ping command without any pre-configured scope.", "type": "string", - "enum": [ - "deny-ping" - ] + "const": "deny-ping" }, { - "description": "global-scope -> Sets a global scope.", + "description": "Sets a global scope.", "type": "string", - "enum": [ - "global-scope" - ] + "const": "global-scope" }, { - "description": "allow-ping-scoped -> Enables the ping command with a test scope.", + "description": "Enables the ping command with a test scope.", "type": "string", - "enum": [ - "allow-ping-scoped" - ] + "const": "allow-ping-scoped" } ] }