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": [ <here> ]
}
```
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
This commit is contained in:
Amr Bashir
2024-09-13 14:58:26 +03:00
committed by GitHub
parent a1e88d2b57
commit 63649d82d2
18 changed files with 1104 additions and 839 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": "patch:bug"
---
Fix schema generation for `core:default` set.

View File

@@ -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<String, Manifest>) -> RootSchema {
let mut schema = schema_for!(CapabilityFile);
/// Saves capabilities in a file inside the project, mainly to be read by tauri-cli.
fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
fs::create_dir_all(dir)?;
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 path = dir.join(CAPABILITIES_FILE_NAME);
let json = serde_json::to_string(&capabilities)?;
write_if_changed(&path, json)?;
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()
}));
}
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()
}));
};
}
}
for definitions_map in definitions {
schema.definitions.extend(definitions_map);
}
schema
Ok(path)
}
pub fn generate_schema(acl_manifests: &BTreeMap<String, Manifest>, 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")?;
/// Saves ACL manifests in a file inside the project, mainly to be read by tauri-cli.
fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
fs::create_dir_all(dir)?;
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)?;
let path = dir.join(ACL_MANIFESTS_FILE_NAME);
let json = serde_json::to_string(&acl_manifests)?;
write_if_changed(&path, json)?;
copy(
schema_path,
out_dir.join(format!(
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
if target.is_desktop() {
"desktop"
} else {
"mobile"
}
)),
)?;
}
Ok(())
Ok(path)
}
pub fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
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)
}
/// Read plugin permissions and scope schema from env vars
fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
use tauri_utils::acl;
pub fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
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)
}
pub fn get_manifests_from_plugins() -> Result<BTreeMap<String, Manifest>> {
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<BTreeMap<String, Manifest>> {
@@ -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::<Vec<String>>()
.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::<Vec<String>>()
.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<Manifest> {
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<String, Manifest>,
capabilities: &BTreeMap<String, Capability>,
) -> 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(())
}

View File

@@ -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<Path>, to: impl AsRef<Path>) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
@@ -55,8 +50,8 @@ fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> 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::<cargo_toml::Value>::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?;
Manifest::<cargo_toml::Value>::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 <https://github.com/rust-lang/cargo/issues/5457>
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();
}
}

View File

@@ -1748,9 +1748,7 @@
"anyOf": [
{
"description": "Bundle all targets.",
"enum": [
"all"
]
"const": "all"
},
{
"description": "A list of bundle targets.",

View File

@@ -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<TokenStream> {
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::<Vec<PathBuf>>(&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<TokenStream> {
#package_info,
#pattern,
#runtime_authority,
#plugin_global_api_script
#plugin_global_api_scripts
);
#with_tray_icon_code

View File

@@ -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<T: DeserializeOwned>(name: &str) -> Option<T> {
"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,

View File

@@ -1748,9 +1748,7 @@
"anyOf": [
{
"description": "Bundle all targets.",
"enum": [
"all"
]
"const": "all"
},
{
"description": "A list of bundle targets.",

View File

@@ -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<PathBuf>) -> Result<Vec<PermissionFile>, 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<F: Fn(&Path) -> bool>(
pattern: &str,
@@ -71,13 +78,10 @@ pub fn define_permissions<F: Fn(&Path) -> bool>(
.filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
.collect::<Vec<PathBuf>>();
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<F: Fn(&Path) -> bool>(
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(Error::ReadFile)?;
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,
@@ -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<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(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<BTreeMap<String, Capability>, Error> {
pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, 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::<Vec<_>>(),
);
permission_schemas.extend(
file
.permission
.iter()
.map(|permission| schema_from(&permission.identifier, permission.description.as_deref()))
.collect::<Vec<_>>(),
);
}
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<P: AsRef<Path>>(
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<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\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<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.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("</table>");
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<HashMap<String, Vec<PermissionFile>>, 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<PathBuf> = 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<HashMap<String, serde_json::Value>, 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<PathBuf>) -> Result<Vec<PermissionFile>, 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<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 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<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 {
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}</table>\n");
let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
write_if_changed(reference_path, docs).map_err(Error::WriteFile)?;
Ok(())
}

View File

@@ -195,6 +195,17 @@ pub struct Capability {
pub platforms: Option<Vec<Target>>,
}
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;

View File

@@ -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<PermissionSet>,
@@ -80,36 +82,50 @@ impl Manifest {
});
}
manifest.permissions.extend(
permission_file
.permission
.into_iter()
.map(|p| (p.identifier.clone(), p))
.collect::<BTreeMap<_, _>>(),
);
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::<BTreeMap<_, _>>(),
);
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<String, Schema>);
#[cfg(feature = "schema")]
impl Manifest {
/// Return scope schema and extra schema definitions for this plugin manifest.
pub fn global_scope_schema(&self) -> Result<Option<ScopeSchema>, super::Error> {
self
.global_scope_schema
.as_ref()
.map(|s| {
serde_json::from_value::<RootSchema>(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;

View File

@@ -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<Vec<Target>>,
}
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))]

View File

@@ -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<String, Vec<Scopes>> = 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,86 +203,6 @@ fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error
Ok(patterns)
}
struct ResolvedPermission<'a> {
key: &'a str,
permission_name: &'a str,
commands: Commands,
scope: Scopes,
}
fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
capability: &Capability,
acl: &BTreeMap<String, Manifest>,
target: Target,
mut f: F,
) -> 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)?
.into_iter()
.filter(|p| {
p.platforms
.as_ref()
.map(|platforms| platforms.contains(&target))
.unwrap_or(true)
})
.collect::<Vec<_>>();
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
.get_or_insert_with(Default::default)
.extend(allow);
}
if let Some(deny) = permission.scope.deny.clone() {
resolved_scope
.deny
.get_or_insert_with(Default::default)
.extend(deny);
}
commands.allow.extend(permission.commands.allow.clone());
commands.deny.extend(permission.commands.deny.clone());
}
f(ResolvedPermission {
key,
permission_name,
commands,
scope: resolved_scope,
})?;
}
Ok(())
}
fn resolve_command(
commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
command: String,
@@ -356,21 +243,175 @@ fn resolve_command(
Ok(())
}
struct ResolvedPermission<'a> {
key: &'a str,
permission_name: &'a str,
commands: Commands,
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<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
capability: &Capability,
acl: &BTreeMap<String, Manifest>,
target: Target,
mut f: F,
) -> Result<(), Error> {
for permission_entry in &capability.permissions {
let permission_id = permission_entry.identifier();
let permissions = get_permissions(permission_id, acl)?
.into_iter()
.filter(|p| p.permission.is_active(&target));
for TraversedPermission {
key,
permission_name,
permission,
} in permissions
{
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);
}
}
if let Some(allow) = permission.scope.allow.clone() {
resolved_scope
.allow
.get_or_insert_with(Default::default)
.extend(allow);
}
if let Some(deny) = permission.scope.deny.clone() {
resolved_scope
.deny
.get_or_insert_with(Default::default)
.extend(deny);
}
commands.allow.extend(permission.commands.allow.clone());
commands.deny.extend(permission.commands.deny.clone());
f(ResolvedPermission {
key: &key,
permission_name: &permission_name,
commands,
scope: resolved_scope,
})?;
}
}
Ok(())
}
#[derive(Debug)]
struct TraversedPermission<'a> {
key: String,
permission_name: String,
permission: &'a Permission,
}
fn get_permissions<'a>(
permission_id: &Identifier,
acl: &'a BTreeMap<String, Manifest>,
) -> Result<Vec<TraversedPermission<'a>>, Error> {
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
let permission_name = permission_id.get_base();
let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
key: display_perm_key(key).to_string(),
available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
})?;
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<String, Manifest>,
manifest: &'a Manifest,
set: &'a PermissionSet,
) -> Result<Vec<&'a Permission>, Error> {
) -> Result<Vec<TraversedPermission<'a>>, 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<String, Manifest>,
) -> Result<Vec<&'a Permission>, 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::<Vec<_>>().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<const P: usize, const S: usize>(
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");
}
}

View File

@@ -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 <trait>
// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42
/// Permission schema generator trait
pub trait PermissionSchemaGenerator<
'a,
Ps: Iterator<Item = &'a PermissionSet>,
P: Iterator<Item = &'a Permission>,
>
{
/// 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<Schema> {
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<String, Manifest>) {
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::<Vec<_>>();
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<String, Manifest>) {
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<String, Manifest>,
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<P: AsRef<Path>>(
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(())
}

View File

@@ -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()

View File

@@ -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<Vec<String>> {
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::<Vec<PathBuf>>(&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(),
)
}
}

View File

@@ -580,8 +580,6 @@ mod tests {
.iter()
.collect::<Vec<_>>();
dbg!(&resources);
assert_eq!(resources.len(), 4);
assert!(resources.iter().all(|r| r.is_err()));

View File

@@ -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<Mutex<Vec<String>>> = 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::<Vec<_>>(),
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::<Vec<_>>()
.join("\n"),
PLUGINS
.iter()
.map(|(k, _)| format!("'{k}:default'"))
.collect::<Vec<_>>()
.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() {

View File

@@ -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"
}
]
}