tauri-utils 3.0.0-alpha.0

Utilities for Tauri
Documentation
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! Plugin ACL types.

use std::{collections::BTreeMap, num::NonZeroU64};

use super::{Commands, Permission, PermissionSet};
use serde::{Deserialize, Serialize};

/// The default permission set of the plugin.
///
/// Works similarly to a permission with the "default" identifier.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DefaultPermission {
  /// The version of the permission.
  pub version: Option<NonZeroU64>,

  /// Human-readable description of what the permission does.
  /// Tauri convention is to use `<h4>` headings in markdown content
  /// for Tauri documentation generation purposes.
  pub description: Option<String>,

  /// All permissions this set contains.
  pub permissions: Vec<String>,
}

/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionFile {
  /// The default permission set for the plugin
  pub default: Option<DefaultPermission>,

  /// A list of permissions sets defined
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub set: Vec<PermissionSet>,

  /// A list of inlined permissions
  #[serde(default)]
  pub permission: Vec<Permission>,

  /// A list of command names that get `allow-$command` and `deny-$command` permissions
  /// autogenerated on demand instead of being stored as explicit permissions.
  ///
  /// See [`Manifest::command_permission`] and the ACL resolver for how these are expanded.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub commands: Vec<String>,
}

/// Plugin manifest.
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Manifest {
  /// Default permission.
  pub default_permission: Option<PermissionSet>,
  /// Plugin permissions.
  pub permissions: BTreeMap<String, Permission>,
  /// Plugin permission sets.
  pub permission_sets: BTreeMap<String, PermissionSet>,
  /// Commands that have `allow-$command` and `deny-$command` permissions autogenerated on demand.
  ///
  /// Storing the command names instead of two explicit permissions per command drastically reduces
  /// the size of the manifest (and the resolved ACL embedded in the app) when a plugin exposes a
  /// large number of commands. The implicit permissions are materialized by [`Self::command_permission`].
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub commands: Vec<String>,
  /// The global scope schema.
  pub global_scope_schema: Option<serde_json::Value>,
}

impl Manifest {
  /// Creates a new manifest from the given plugin permission files and global scope schema.
  pub fn new(
    permission_files: Vec<PermissionFile>,
    global_scope_schema: Option<serde_json::Value>,
  ) -> Self {
    let mut manifest = Self {
      default_permission: None,
      permissions: BTreeMap::new(),
      permission_sets: BTreeMap::new(),
      commands: Vec::new(),
      global_scope_schema,
    };

    for permission_file in permission_files {
      if let Some(default) = permission_file.default {
        manifest.default_permission.replace(PermissionSet {
          identifier: "default".into(),
          description: default
            .description
            .unwrap_or_else(|| "Default plugin permissions.".to_string()),
          permissions: default.permissions,
        });
      }

      for permission in permission_file.permission {
        let key = permission.identifier.clone();
        manifest.permissions.insert(key, permission);
      }

      for set in permission_file.set {
        let key = set.identifier.clone();
        manifest.permission_sets.insert(key, set);
      }

      manifest.commands.extend(permission_file.commands);
    }

    // keep the list deterministic (it ends up embedded in the app) and free of duplicates
    manifest.commands.sort();
    manifest.commands.dedup();

    manifest
  }

  /// Materializes the `allow-$command`/`deny-$command` (or `allow-*`/`deny-*`) [`Permission`] for the
  /// manifest's [`commands`](Self::commands), if `identifier` refers to one of them.
  ///
  /// Permission identifiers are slugified (snake_case `_` becomes `-`) while command names keep their
  /// original form, so `allow-do-something` resolves the `do_something` command.
  ///
  /// The `allow-*`/`deny-*` wildcards allow/deny **all** of the manifest's commands while resolving to a
  /// single command (`*`) instead of one per command, which keeps the resolved ACL small even when a
  /// manifest exposes many commands. The wildcards are only available for the application manifest, so
  /// `allow_wildcard` must be set accordingly by the caller (it knows the manifest's ACL key).
  pub fn command_permission(&self, identifier: &str, allow_wildcard: bool) -> Option<Permission> {
    let (deny, command_slug) = if let Some(slug) = identifier.strip_prefix("allow-") {
      (false, slug)
    } else {
      let slug = identifier.strip_prefix("deny-")?;
      (true, slug)
    };

    let command = if command_slug == "*" {
      // wildcard: app manifest only, and only when it actually exposes commands
      if !allow_wildcard || self.commands.is_empty() {
        return None;
      }
      "*".to_string()
    } else {
      self
        .commands
        .iter()
        .find(|command| command.replace('_', "-") == command_slug)?
        .clone()
    };

    let mut commands = Commands::default();
    if deny {
      commands.deny.push(command);
    } else {
      commands.allow.push(command);
    }

    Some(Permission {
      identifier: identifier.to_string(),
      commands,
      ..Default::default()
    })
  }
}

#[cfg(feature = "schema")]
type ScopeSchema = (schemars::Schema, serde_json::Map<String, serde_json::Value>);

#[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::<schemars::Schema>(s.clone()).map(|mut root| {
          // Extract definitions from the schema
          let definitions = root
            .remove("$defs")
            .or_else(|| root.remove("definitions"))
            .and_then(|v| match v {
              serde_json::Value::Object(m) => Some(m),
              _ => None,
            })
            .unwrap_or_default();

          // Wrap in an array schema
          let items = serde_json::Value::from(root);
          let scope_schema = schemars::json_schema!({
            "type": "array",
            "items": items
          });

          (scope_schema, definitions)
        })
      })
      .transpose()
      .map_err(Into::into)
  }
}

#[cfg(any(feature = "build", feature = "build-2"))]
mod build {
  use proc_macro2::TokenStream;
  use quote::{ToTokens, TokenStreamExt, quote};
  use std::convert::identity;

  use super::*;
  use crate::{literal_struct, tokens::*};

  impl ToTokens for DefaultPermission {
    fn to_tokens(&self, tokens: &mut TokenStream) {
      let version = opt_lit_owned(self.version.as_ref().map(|v| {
        let v = v.get();
        quote!(::core::num::NonZeroU64::new(#v).unwrap())
      }));
      // Only used in build script and macros, so don't include them in runtime
      let description = quote! { ::core::option::Option::None };
      let permissions = vec_lit(&self.permissions, str_lit);
      literal_struct!(
        tokens,
        ::tauri::utils::acl::plugin::DefaultPermission,
        version,
        description,
        permissions
      )
    }
  }

  impl ToTokens for Manifest {
    fn to_tokens(&self, tokens: &mut TokenStream) {
      let default_permission = opt_lit(self.default_permission.as_ref());

      let permissions = map_lit(
        quote! { ::std::collections::BTreeMap },
        &self.permissions,
        str_lit,
        identity,
      );

      let permission_sets = map_lit(
        quote! { ::std::collections::BTreeMap },
        &self.permission_sets,
        str_lit,
        identity,
      );

      let commands = vec_lit(&self.commands, str_lit);

      // Only used in build script and macros, so don't include them in runtime
      // let global_scope_schema =
      //   opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit));
      let global_scope_schema = quote! { ::core::option::Option::None };

      literal_struct!(
        tokens,
        ::tauri::utils::acl::manifest::Manifest,
        default_permission,
        permissions,
        permission_sets,
        commands,
        global_scope_schema
      )
    }
  }
}