use std::{
collections::{BTreeMap, HashMap},
env, fs,
path::{Path, PathBuf},
};
use crate::{
acl::{has_app_manifest, AllowedCommands, Error},
config::Config,
write_if_changed,
};
use super::{
capability::{Capability, CapabilityFile},
manifest::PermissionFile,
ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME, PERMISSION_SCHEMA_FILE_NAME,
REMOVE_UNUSED_COMMANDS_ENV_VAR,
};
pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH";
pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH";
pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
const CAPABILITY_FILE_EXTENSIONS: &[&str] = &[
"json",
#[cfg(feature = "config-json5")]
"json5",
"toml",
];
const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
let mut permissions = Vec::new();
for path in paths {
let ext = path.extension().unwrap().to_string_lossy().to_string();
let permission_file = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
let permission: PermissionFile = match ext.as_str() {
"toml" => toml::from_str(&permission_file)?,
"json" => serde_json::from_str(&permission_file)?,
_ => return Err(Error::UnknownPermissionFormat(ext)),
};
permissions.push(permission);
}
Ok(permissions)
}
pub fn define_permissions<F: Fn(&Path) -> bool>(
pattern: &str,
pkg_name: &str,
out_dir: &Path,
filter_fn: F,
) -> Result<Vec<PermissionFile>, Error> {
let permission_files = glob::glob(pattern)?
.flatten()
.flat_map(|p| p.canonicalize())
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| PERMISSION_FILE_EXTENSIONS.contains(&e))
.unwrap_or_default()
})
.filter(|p| filter_fn(p))
.filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
.collect::<Vec<PathBuf>>();
let pkg_name_valid_path = pkg_name.replace(':', "-");
let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files"));
let permission_files_json = serde_json::to_string(&permission_files)?;
write_if_changed(&permission_files_path, permission_files_json)
.map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?;
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
println!(
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{PERMISSION_FILES_PATH_KEY}={}",
permission_files_path.display()
);
} else {
println!(
"cargo:{PERMISSION_FILES_PATH_KEY}={}",
permission_files_path.display()
);
}
parse_permissions(permission_files)
}
pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
let mut permissions_map = HashMap::new();
for (key, value) in env::vars_os() {
let key = key.to_string_lossy();
if let Some(plugin_crate_name_var) = key
.strip_prefix("DEP_")
.and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
.map(|v| {
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
.and_then(|v| v.strip_prefix("TAURI_"))
.unwrap_or(v)
})
{
let permissions_path = PathBuf::from(value);
let permissions_str =
fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?;
let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
let permissions = parse_permissions(permissions)?;
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
let plugin_crate_name = plugin_crate_name
.strip_prefix("tauri-plugin-")
.map(ToString::to_string)
.unwrap_or(plugin_crate_name);
permissions_map.insert(plugin_crate_name, permissions);
}
}
Ok(permissions_map)
}
pub fn define_global_scope_schema(
schema: schemars::schema::RootSchema,
pkg_name: &str,
out_dir: &Path,
) -> Result<(), Error> {
let path = out_dir.join("global-scope.json");
write_if_changed(&path, serde_json::to_vec(&schema)?)
.map_err(|e| Error::WriteFile(e, path.clone()))?;
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
println!(
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}",
path.display()
);
} else {
println!("cargo:{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display());
}
Ok(())
}
pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
let mut schemas_map = HashMap::new();
for (key, value) in env::vars_os() {
let key = key.to_string_lossy();
if let Some(plugin_crate_name_var) = key
.strip_prefix("DEP_")
.and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
.map(|v| {
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
.and_then(|v| v.strip_prefix("TAURI_"))
.unwrap_or(v)
})
{
let path = PathBuf::from(value);
let json = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
let schema: serde_json::Value = serde_json::from_str(&json)?;
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
let plugin_crate_name = plugin_crate_name
.strip_prefix("tauri-plugin-")
.map(ToString::to_string)
.unwrap_or(plugin_crate_name);
schemas_map.insert(plugin_crate_name, schema);
}
}
Ok(schemas_map)
}
pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> {
let mut capabilities_map = BTreeMap::new();
for path in glob::glob(pattern)?
.flatten() .filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e))
.unwrap_or_default()
})
.filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
{
match CapabilityFile::load(&path)? {
CapabilityFile::Capability(capability) => {
if capabilities_map.contains_key(&capability.identifier) {
return Err(Error::CapabilityAlreadyExists {
identifier: capability.identifier,
});
}
capabilities_map.insert(capability.identifier.clone(), capability);
}
CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => {
for capability in capabilities {
if capabilities_map.contains_key(&capability.identifier) {
return Err(Error::CapabilityAlreadyExists {
identifier: capability.identifier,
});
}
capabilities_map.insert(capability.identifier.clone(), capability);
}
}
}
}
Ok(capabilities_map)
}
pub struct AutogeneratedPermissions {
pub allowed: Vec<String>,
pub denied: Vec<String>,
}
pub fn autogenerate_command_permissions(
path: &Path,
commands: &[&str],
license_header: &str,
schema_ref: bool,
) -> AutogeneratedPermissions {
if !path.exists() {
fs::create_dir_all(path).expect("unable to create autogenerated commands dir");
}
let schema_entry = if schema_ref {
let cwd = env::current_dir().unwrap();
let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count();
let schema_path = (1..components_len)
.map(|_| "..")
.collect::<PathBuf>()
.join(PERMISSION_SCHEMAS_FOLDER_NAME)
.join(PERMISSION_SCHEMA_FILE_NAME);
format!(
"\n\"$schema\" = \"{}\"\n",
dunce::simplified(&schema_path)
.display()
.to_string()
.replace('\\', "/")
)
} else {
"".to_string()
};
let mut autogenerated = AutogeneratedPermissions {
allowed: Vec::new(),
denied: Vec::new(),
};
for command in commands {
let slugified_command = command.replace('_', "-");
let toml = format!(
r###"{license_header}# Automatically generated - DO NOT EDIT!
{schema_entry}
[[permission]]
identifier = "allow-{slugified_command}"
description = "Enables the {command} command without any pre-configured scope."
commands.allow = ["{command}"]
[[permission]]
identifier = "deny-{slugified_command}"
description = "Denies the {command} command without any pre-configured scope."
commands.deny = ["{command}"]
"###,
);
let out_path = path.join(format!("{command}.toml"));
write_if_changed(&out_path, toml)
.unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}"));
autogenerated
.allowed
.push(format!("allow-{slugified_command}"));
autogenerated
.denied
.push(format!("deny-{slugified_command}"));
}
autogenerated
}
const PERMISSION_TABLE_HEADER: &str =
"## Permission Table\n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n";
pub fn generate_docs(
permissions: &[PermissionFile],
out_dir: &Path,
plugin_identifier: &str,
) -> Result<(), Error> {
let mut default_permission = "".to_owned();
let mut permission_table = "".to_string();
fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
if let Some(d) = description {
docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
}
docs.push_str("\n</tr>");
docs
}
for permission in permissions {
for set in &permission.set {
permission_table.push_str(&docs_from(
&set.identifier,
Some(&set.description),
plugin_identifier,
));
permission_table.push('\n');
}
if let Some(default) = &permission.default {
default_permission.push_str("## Default Permission\n\n");
default_permission.push_str(default.description.as_deref().unwrap_or_default().trim());
default_permission.push('\n');
default_permission.push('\n');
if !default.permissions.is_empty() {
default_permission.push_str("#### This default permission set includes the following:\n\n");
for permission in &default.permissions {
default_permission.push_str(&format!("- `{permission}`\n"));
}
default_permission.push('\n');
}
}
for permission in &permission.permission {
permission_table.push_str(&docs_from(
&permission.identifier,
permission.description.as_deref(),
plugin_identifier,
));
permission_table.push('\n');
}
}
let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n");
let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
write_if_changed(&reference_path, docs).map_err(|e| Error::WriteFile(e, reference_path))?;
Ok(())
}
pub fn generate_allowed_commands(
out_dir: &Path,
capabilities_from_files: Option<BTreeMap<String, Capability>>,
permissions_map: BTreeMap<String, Vec<PermissionFile>>,
) -> Result<(), anyhow::Error> {
println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}");
let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME);
let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR);
let should_generate_allowed_commands =
remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty();
if !should_generate_allowed_commands {
let _ = std::fs::remove_file(allowed_commands_file_path);
return Ok(());
}
let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap());
let capabilities_path = config_directory.join("capabilities");
if capabilities_path.exists() {
println!("cargo:rerun-if-changed={}", capabilities_path.display());
}
let target_triple = env::var("TARGET")?;
let target = crate::platform::Target::from_triple(&target_triple);
let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?;
for config_file_path in config_paths {
println!("cargo:rerun-if-changed={}", config_file_path.display());
}
if let Ok(env) = std::env::var("TAURI_CONFIG") {
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
json_patch::merge(&mut config, &merge_config);
}
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
let old_cwd = std::env::current_dir()?;
std::env::set_current_dir(config_directory)?;
let config: Config = serde_json::from_value(config)?;
std::env::set_current_dir(old_cwd)?;
let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map
.into_iter()
.map(|(key, permissions)| {
let key = key
.strip_prefix("tauri-plugin-")
.unwrap_or(&key)
.to_string();
let manifest = crate::acl::manifest::Manifest::new(permissions, None);
(key, manifest)
})
.collect();
let capabilities_from_files = if let Some(capabilities) = capabilities_from_files {
capabilities
} else {
crate::acl::build::parse_capabilities(&format!(
"{}/**/*",
glob::Pattern::escape(&capabilities_path.to_string_lossy())
))?
};
let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?;
let permission_entries = capabilities
.into_iter()
.flat_map(|(_, capabilities)| capabilities.permissions);
let mut allowed_commands = AllowedCommands {
has_app_acl: has_app_manifest(&acl),
..Default::default()
};
for permission_entry in permission_entries {
let Ok(permissions) =
crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl)
else {
continue;
};
for permission in permissions {
let plugin_name = permission.key;
let allowed_command_names = &permission.permission.commands.allow;
for allowed_command in allowed_command_names {
let command_name = if plugin_name == crate::acl::APP_ACL_KEY {
allowed_command.to_string()
} else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") {
format!("plugin:{core_plugin_name}|{allowed_command}")
} else {
format!("plugin:{plugin_name}|{allowed_command}")
};
allowed_commands.commands.insert(command_name);
}
}
}
write_if_changed(
allowed_commands_file_path,
serde_json::to_string(&allowed_commands)?,
)?;
Ok(())
}