123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- use std::{
- collections::{BTreeMap, BTreeSet, HashMap},
- env::current_dir,
- fs::{copy, create_dir_all, read_to_string, write},
- 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,
- },
- platform::Target,
- };
- 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";
- /// 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(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
- let mut schema = schema_for!(CapabilityFile);
- fn schema_from(key: &str, id: &str, description: Option<&str>) -> Schema {
- let command_name = if key == APP_ACL_KEY {
- id.to_string()
- } else {
- format!("{key}:{id}")
- };
- Schema::Object(SchemaObject {
- metadata: Some(Box::new(Metadata {
- description: description
- .as_ref()
- .map(|d| format!("{command_name} -> {d}")),
- ..Default::default()
- })),
- instance_type: Some(InstanceType::String.into()),
- enum_values: Some(vec![serde_json::Value::String(command_name)]),
- ..Default::default()
- })
- }
- let mut permission_schemas = Vec::new();
- for (key, manifest) in acl_manifests {
- for (set_id, set) in &manifest.permission_sets {
- permission_schemas.push(schema_from(key, set_id, Some(&set.description)));
- }
- permission_schemas.push(schema_from(
- key,
- "default",
- manifest
- .default_permission
- .as_ref()
- .map(|d| d.description.as_ref()),
- ));
- for (permission_id, permission) in &manifest.permissions {
- permission_schemas.push(schema_from(
- key,
- permission_id,
- permission.description.as_deref(),
- ));
- }
- }
- if let Some(Schema::Object(obj)) = schema.definitions.get_mut("Identifier") {
- obj.object = None;
- obj.instance_type = None;
- obj.metadata.as_mut().map(|metadata| {
- metadata
- .description
- .replace("Permission identifier".to_string());
- metadata
- });
- obj.subschemas.replace(Box::new(SubschemaValidation {
- one_of: Some(permission_schemas),
- ..Default::default()
- }));
- }
- 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
- }
- 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")?;
- let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
- if schema_str != read_to_string(&schema_path).unwrap_or_default() {
- write(&schema_path, schema_str)?;
- copy(
- schema_path,
- out_dir.join(format!(
- "{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
- if target.is_desktop() {
- "desktop"
- } else {
- "mobile"
- }
- )),
- )?;
- }
- Ok(())
- }
- 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)
- }
- 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")?;
- 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);
- }
- 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.starts_with(inlined_path))
- },
- )?);
- }
- Ok(tauri_utils::acl::manifest::Manifest::new(
- permission_files,
- None,
- ))
- }
- pub fn validate_capabilities(
- acl_manifests: &BTreeMap<String, Manifest>,
- capabilities: &BTreeMap<String, Capability>,
- ) -> Result<()> {
- let target = tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap());
- for capability in capabilities.values() {
- if !capability
- .platforms
- .as_ref()
- .map(|platforms| platforms.contains(&target))
- .unwrap_or(true)
- {
- continue;
- }
- for permission_entry in &capability.permissions {
- let permission_id = permission_entry.identifier();
- 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| {
- // the default permission is always treated as valid, the CLI automatically adds it on the `tauri add` command
- permission_name == "default"
- || 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(", ")
- );
- }
- }
- }
- Ok(())
- }
|