|
@@ -3,9 +3,10 @@
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
use std::{
|
|
|
- collections::{BTreeMap, BTreeSet},
|
|
|
+ collections::{BTreeMap, BTreeSet, HashMap},
|
|
|
+ env::current_dir,
|
|
|
fs::{copy, create_dir_all, read_to_string, write},
|
|
|
- path::PathBuf,
|
|
|
+ path::{Path, PathBuf},
|
|
|
};
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
@@ -19,7 +20,8 @@ use schemars::{
|
|
|
use tauri_utils::{
|
|
|
acl::{
|
|
|
capability::{Capability, CapabilityFile},
|
|
|
- plugin::Manifest,
|
|
|
+ manifest::Manifest,
|
|
|
+ APP_ACL_KEY,
|
|
|
},
|
|
|
platform::Target,
|
|
|
};
|
|
@@ -28,35 +30,110 @@ 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 PLUGIN_MANIFESTS_FILE_NAME: &str = "plugin-manifests.json";
|
|
|
+const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
|
|
|
+
|
|
|
+/// Definition of a plugin that is part of the Tauri application instead of having its own crate.
|
|
|
+///
|
|
|
+/// By default it generates a plugin manifest that parses permissions from the `permissions/$plugin-name` directory.
|
|
|
+/// 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)]
|
|
|
+pub struct InlinedPlugin {
|
|
|
+ commands: &'static [&'static str],
|
|
|
+ permissions_path_pattern: Option<&'static str>,
|
|
|
+}
|
|
|
+
|
|
|
+impl InlinedPlugin {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self::default()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Define a list of commands that gets permissions autogenerated in the format of `allow-$command` and `deny-$command`
|
|
|
+ /// where $command is the command name in snake_case.
|
|
|
+ pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
|
|
|
+ self.commands = commands;
|
|
|
+ self
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Sets a glob pattern that is used to find the permissions of this inlined plugin.
|
|
|
+ ///
|
|
|
+ /// **Note:** You must emit [rerun-if-changed] instructions for the plugin permissions directory.
|
|
|
+ ///
|
|
|
+ /// By default it is `./permissions/$plugin-name/**/*`
|
|
|
+ pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
|
|
|
+ self.permissions_path_pattern.replace(pattern);
|
|
|
+ self
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Tauri application permission manifest.
|
|
|
+///
|
|
|
+/// By default it generates a manifest that parses permissions from the `permissions` directory.
|
|
|
+/// 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)]
|
|
|
+pub struct AppManifest {
|
|
|
+ commands: &'static [&'static str],
|
|
|
+ permissions_path_pattern: Option<&'static str>,
|
|
|
+}
|
|
|
+
|
|
|
+impl AppManifest {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self::default()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Define a list of commands that gets permissions autogenerated in the format of `allow-$command` and `deny-$command`
|
|
|
+ /// where $command is the command name in snake_case.
|
|
|
+ pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
|
|
|
+ self.commands = commands;
|
|
|
+ self
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Sets a glob pattern that is used to find the permissions of the app.
|
|
|
+ ///
|
|
|
+ /// **Note:** You must emit [rerun-if-changed] instructions for the permissions directory.
|
|
|
+ ///
|
|
|
+ /// By default it is `./permissions/**/*` ignoring any [`InlinedPlugin`].
|
|
|
+ pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
|
|
|
+ self.permissions_path_pattern.replace(pattern);
|
|
|
+ self
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
|
|
|
+fn capabilities_schema(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
|
|
|
let mut schema = schema_for!(CapabilityFile);
|
|
|
|
|
|
- fn schema_from(plugin: &str, id: &str, description: Option<&str>) -> Schema {
|
|
|
+ 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!("{plugin}:{id} -> {d}")),
|
|
|
+ .map(|d| format!("{command_name} -> {d}")),
|
|
|
..Default::default()
|
|
|
})),
|
|
|
instance_type: Some(InstanceType::String.into()),
|
|
|
- enum_values: Some(vec![serde_json::Value::String(format!("{plugin}:{id}"))]),
|
|
|
+ enum_values: Some(vec![serde_json::Value::String(command_name)]),
|
|
|
..Default::default()
|
|
|
})
|
|
|
}
|
|
|
|
|
|
let mut permission_schemas = Vec::new();
|
|
|
|
|
|
- for (plugin, manifest) in plugin_manifests {
|
|
|
+ for (key, manifest) in acl_manifests {
|
|
|
for (set_id, set) in &manifest.permission_sets {
|
|
|
- permission_schemas.push(schema_from(plugin, set_id, Some(&set.description)));
|
|
|
+ permission_schemas.push(schema_from(key, set_id, Some(&set.description)));
|
|
|
}
|
|
|
|
|
|
if let Some(default) = &manifest.default_permission {
|
|
|
permission_schemas.push(schema_from(
|
|
|
- plugin,
|
|
|
+ key,
|
|
|
"default",
|
|
|
Some(default.description.as_ref()),
|
|
|
));
|
|
@@ -64,7 +141,7 @@ fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSch
|
|
|
|
|
|
for (permission_id, permission) in &manifest.permissions {
|
|
|
permission_schemas.push(schema_from(
|
|
|
- plugin,
|
|
|
+ key,
|
|
|
permission_id,
|
|
|
permission.description.as_deref(),
|
|
|
));
|
|
@@ -96,11 +173,11 @@ fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSch
|
|
|
{
|
|
|
let mut global_scope_one_of = Vec::new();
|
|
|
|
|
|
- for (plugin, manifest) in plugin_manifests {
|
|
|
+ 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 {plugin}: {e}"));
|
|
|
+ .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {key}: {e}"));
|
|
|
|
|
|
let global_scope_schema = Schema::Object(SchemaObject {
|
|
|
array: Some(Box::new(ArrayValidation {
|
|
@@ -122,14 +199,14 @@ fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSch
|
|
|
|
|
|
let mut permission_schemas = Vec::new();
|
|
|
if let Some(default) = &manifest.default_permission {
|
|
|
- permission_schemas.push(schema_from(plugin, "default", Some(&default.description)));
|
|
|
+ permission_schemas.push(schema_from(key, "default", Some(&default.description)));
|
|
|
}
|
|
|
for set in manifest.permission_sets.values() {
|
|
|
- permission_schemas.push(schema_from(plugin, &set.identifier, Some(&set.description)));
|
|
|
+ permission_schemas.push(schema_from(key, &set.identifier, Some(&set.description)));
|
|
|
}
|
|
|
for permission in manifest.permissions.values() {
|
|
|
permission_schemas.push(schema_from(
|
|
|
- plugin,
|
|
|
+ key,
|
|
|
&permission.identifier,
|
|
|
permission.description.as_deref(),
|
|
|
));
|
|
@@ -182,11 +259,8 @@ fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSch
|
|
|
schema
|
|
|
}
|
|
|
|
|
|
-pub fn generate_schema(
|
|
|
- plugin_manifests: &BTreeMap<String, Manifest>,
|
|
|
- target: Target,
|
|
|
-) -> Result<()> {
|
|
|
- let schema = capabilities_schema(plugin_manifests);
|
|
|
+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")?;
|
|
@@ -221,17 +295,17 @@ pub fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<
|
|
|
Ok(capabilities_path)
|
|
|
}
|
|
|
|
|
|
-pub fn save_plugin_manifests(plugin_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
|
|
|
- let plugin_manifests_path =
|
|
|
- PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(PLUGIN_MANIFESTS_FILE_NAME);
|
|
|
- let plugin_manifests_json = serde_json::to_string(&plugin_manifests)?;
|
|
|
- if plugin_manifests_json != read_to_string(&plugin_manifests_path).unwrap_or_default() {
|
|
|
- std::fs::write(&plugin_manifests_path, plugin_manifests_json)?;
|
|
|
+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(plugin_manifests_path)
|
|
|
+ Ok(acl_manifests_path)
|
|
|
}
|
|
|
|
|
|
-pub fn get_plugin_manifests() -> Result<BTreeMap<String, Manifest>> {
|
|
|
+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()
|
|
@@ -246,8 +320,135 @@ pub fn get_plugin_manifests() -> Result<BTreeMap<String, Manifest>> {
|
|
|
Ok(processed)
|
|
|
}
|
|
|
|
|
|
+pub fn inline_plugins(
|
|
|
+ out_dir: &Path,
|
|
|
+ inlined_plugins: HashMap<&'static str, InlinedPlugin>,
|
|
|
+) -> Result<BTreeMap<String, Manifest>> {
|
|
|
+ let mut acl_manifests = BTreeMap::new();
|
|
|
+
|
|
|
+ for (name, plugin) in inlined_plugins {
|
|
|
+ let plugin_out_dir = out_dir.join("plugins").join(name);
|
|
|
+ create_dir_all(&plugin_out_dir)?;
|
|
|
+
|
|
|
+ let mut permission_files = if plugin.commands.is_empty() {
|
|
|
+ Vec::new()
|
|
|
+ } else {
|
|
|
+ tauri_utils::acl::build::autogenerate_command_permissions(
|
|
|
+ &plugin_out_dir,
|
|
|
+ plugin.commands,
|
|
|
+ "",
|
|
|
+ false,
|
|
|
+ );
|
|
|
+ tauri_utils::acl::build::define_permissions(
|
|
|
+ &plugin_out_dir.join("*").to_string_lossy(),
|
|
|
+ name,
|
|
|
+ &plugin_out_dir,
|
|
|
+ |_| true,
|
|
|
+ )?
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Some(pattern) = plugin.permissions_path_pattern {
|
|
|
+ permission_files.extend(tauri_utils::acl::build::define_permissions(
|
|
|
+ pattern,
|
|
|
+ name,
|
|
|
+ &plugin_out_dir,
|
|
|
+ |_| true,
|
|
|
+ )?);
|
|
|
+ } else {
|
|
|
+ let default_permissions_path = Path::new("permissions").join(name);
|
|
|
+ println!(
|
|
|
+ "cargo:rerun-if-changed={}",
|
|
|
+ default_permissions_path.display()
|
|
|
+ );
|
|
|
+ permission_files.extend(tauri_utils::acl::build::define_permissions(
|
|
|
+ &default_permissions_path
|
|
|
+ .join("**")
|
|
|
+ .join("*")
|
|
|
+ .to_string_lossy(),
|
|
|
+ name,
|
|
|
+ &plugin_out_dir,
|
|
|
+ |_| true,
|
|
|
+ )?);
|
|
|
+ }
|
|
|
+
|
|
|
+ let manifest = tauri_utils::acl::manifest::Manifest::new(permission_files, None);
|
|
|
+ acl_manifests.insert(name.into(), manifest);
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(acl_manifests)
|
|
|
+}
|
|
|
+
|
|
|
+pub 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)?;
|
|
|
+ let pkg_name = "__app__";
|
|
|
+
|
|
|
+ let mut permission_files = if manifest.commands.is_empty() {
|
|
|
+ Vec::new()
|
|
|
+ } else {
|
|
|
+ let autogenerated_path = Path::new("./permissions/autogenerated");
|
|
|
+ tauri_utils::acl::build::autogenerate_command_permissions(
|
|
|
+ autogenerated_path,
|
|
|
+ manifest.commands,
|
|
|
+ "",
|
|
|
+ false,
|
|
|
+ );
|
|
|
+ tauri_utils::acl::build::define_permissions(
|
|
|
+ &autogenerated_path.join("*").to_string_lossy(),
|
|
|
+ pkg_name,
|
|
|
+ &app_out_dir,
|
|
|
+ |_| true,
|
|
|
+ )?
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Some(pattern) = manifest.permissions_path_pattern {
|
|
|
+ permission_files.extend(tauri_utils::acl::build::define_permissions(
|
|
|
+ pattern,
|
|
|
+ pkg_name,
|
|
|
+ &app_out_dir,
|
|
|
+ |_| true,
|
|
|
+ )?);
|
|
|
+ } else {
|
|
|
+ let default_permissions_path = Path::new("permissions");
|
|
|
+ println!(
|
|
|
+ "cargo:rerun-if-changed={}",
|
|
|
+ default_permissions_path.display()
|
|
|
+ );
|
|
|
+
|
|
|
+ let permissions_root = current_dir()?.join("permissions");
|
|
|
+ let inlined_plugins_permissions: Vec<_> = inlined_plugins
|
|
|
+ .keys()
|
|
|
+ .map(|name| permissions_root.join(name))
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ permission_files.extend(tauri_utils::acl::build::define_permissions(
|
|
|
+ &default_permissions_path
|
|
|
+ .join("**")
|
|
|
+ .join("*")
|
|
|
+ .to_string_lossy(),
|
|
|
+ pkg_name,
|
|
|
+ &app_out_dir,
|
|
|
+ // filter out directories containing inlined plugins
|
|
|
+ |p| {
|
|
|
+ inlined_plugins_permissions
|
|
|
+ .iter()
|
|
|
+ .any(|inlined_path| p.strip_prefix(inlined_path).is_err())
|
|
|
+ },
|
|
|
+ )?);
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(tauri_utils::acl::manifest::Manifest::new(
|
|
|
+ permission_files,
|
|
|
+ None,
|
|
|
+ ))
|
|
|
+}
|
|
|
+
|
|
|
pub fn validate_capabilities(
|
|
|
- plugin_manifests: &BTreeMap<String, Manifest>,
|
|
|
+ acl_manifests: &BTreeMap<String, Manifest>,
|
|
|
capabilities: &BTreeMap<String, Capability>,
|
|
|
) -> Result<()> {
|
|
|
let target = tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap());
|
|
@@ -259,39 +460,47 @@ pub fn validate_capabilities(
|
|
|
|
|
|
for permission_entry in &capability.permissions {
|
|
|
let permission_id = permission_entry.identifier();
|
|
|
- if let Some((plugin_name, permission_name)) = permission_id.get().split_once(':') {
|
|
|
- let permission_exists = plugin_manifests
|
|
|
- .get(plugin_name)
|
|
|
- .map(|manifest| {
|
|
|
- if permission_name == "default" {
|
|
|
- manifest.default_permission.is_some()
|
|
|
- } else {
|
|
|
- manifest.permissions.contains_key(permission_name)
|
|
|
- || manifest.permission_sets.contains_key(permission_name)
|
|
|
- }
|
|
|
- })
|
|
|
- .unwrap_or(false);
|
|
|
-
|
|
|
- if !permission_exists {
|
|
|
- let mut available_permissions = Vec::new();
|
|
|
- for (plugin, manifest) in plugin_manifests {
|
|
|
- if manifest.default_permission.is_some() {
|
|
|
- available_permissions.push(format!("{plugin}:default"));
|
|
|
- }
|
|
|
- for p in manifest.permissions.keys() {
|
|
|
- available_permissions.push(format!("{plugin}:{p}"));
|
|
|
- }
|
|
|
- for p in manifest.permission_sets.keys() {
|
|
|
- available_permissions.push(format!("{plugin}:{p}"));
|
|
|
- }
|
|
|
+ let (key, permission_name) = permission_id
|
|
|
+ .get()
|
|
|
+ .split_once(':')
|
|
|
+ .unwrap_or_else(|| (APP_ACL_KEY, permission_id.get()));
|
|
|
+
|
|
|
+ let permission_exists = acl_manifests
|
|
|
+ .get(key)
|
|
|
+ .map(|manifest| {
|
|
|
+ if permission_name == "default" {
|
|
|
+ manifest.default_permission.is_some()
|
|
|
+ } else {
|
|
|
+ manifest.permissions.contains_key(permission_name)
|
|
|
+ || manifest.permission_sets.contains_key(permission_name)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .unwrap_or(false);
|
|
|
+
|
|
|
+ if !permission_exists {
|
|
|
+ let mut available_permissions = Vec::new();
|
|
|
+ for (key, manifest) in acl_manifests {
|
|
|
+ let prefix = if key == APP_ACL_KEY {
|
|
|
+ "".to_string()
|
|
|
+ } else {
|
|
|
+ format!("{key}:")
|
|
|
+ };
|
|
|
+ if manifest.default_permission.is_some() {
|
|
|
+ available_permissions.push(format!("{prefix}default"));
|
|
|
+ }
|
|
|
+ for p in manifest.permissions.keys() {
|
|
|
+ available_permissions.push(format!("{prefix}{p}"));
|
|
|
+ }
|
|
|
+ for p in manifest.permission_sets.keys() {
|
|
|
+ available_permissions.push(format!("{prefix}{p}"));
|
|
|
}
|
|
|
-
|
|
|
- anyhow::bail!(
|
|
|
- "Permission {} not found, expected one of {}",
|
|
|
- permission_id.get(),
|
|
|
- available_permissions.join(", ")
|
|
|
- );
|
|
|
}
|
|
|
+
|
|
|
+ anyhow::bail!(
|
|
|
+ "Permission {} not found, expected one of {}",
|
|
|
+ permission_id.get(),
|
|
|
+ available_permissions.join(", ")
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
}
|