mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
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:
5
.changes/core-default-schema.md
Normal file
5
.changes/core-default-schema.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": "patch:bug"
|
||||
---
|
||||
|
||||
Fix schema generation for `core:default` set.
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1748,9 +1748,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Bundle all targets.",
|
||||
"enum": [
|
||||
"all"
|
||||
]
|
||||
"const": "all"
|
||||
},
|
||||
{
|
||||
"description": "A list of bundle targets.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1748,9 +1748,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Bundle all targets.",
|
||||
"enum": [
|
||||
"all"
|
||||
]
|
||||
"const": "all"
|
||||
},
|
||||
{
|
||||
"description": "A list of bundle targets.",
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
345
crates/tauri-utils/src/acl/schema.rs
Normal file
345
crates/tauri-utils/src/acl/schema.rs
Normal 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(())
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,8 +580,6 @@ mod tests {
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
dbg!(&resources);
|
||||
|
||||
assert_eq!(resources.len(), 4);
|
||||
|
||||
assert!(resources.iter().all(|r| r.is_err()));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user