Skip to content

Commit

Permalink
Tauri ACL/Allowlist v2 Implementation and Plugin System Refactor (#8428)
Browse files Browse the repository at this point in the history
* tauri-plugin concept

* wip

* move command module to its own directory

* wip: new command traits and generated code

* wip: whip

* wip: static dispatch

there is a man standing behind me

* wip

* re-add authority

* fix build [skip ci]

* parse plugin permissions

* merge permission files [skip ci]

* parse capabilities [skip ci]

* resolve acl (untested) [skip ci]

* split functionality, add some docs

* remove command2 stuff

* actually check runtime authority

* small fixes [skip ci]

* add function to auto generate basic permission for a command [skip ci]

* retrieve command scope, implement CommandArg [skip ci]

* fix tests [skip ci]

* global scope

* lint

* license headers [skip ci]

* skip canonicalize

* separate scope type in example

* remove inlinedpermission struct [skip ci]

* permission file schema

* capabilities schema

* move items from tauri-plugin to tauri-utils

this allows tauri-plugin to depend on tauri directly again
which will be used by the runtime feature as a superset to
existing plugin traits

* enable schema and glob [skip ci]

* fix glob [skip ci]

* fix capability schema [skip ci]

* enhance schema for permission set possible values [skip ci]

* permission set can reference other sets [skip ci]

* setup tests for resolving ACL

* fixture for permission set [skip ci]

* remote context test and small fix[skip ci]

* ignore empty scope [skip ci]

* code review [skip ci]

* lint [skip ci]

* runtime fixes

* readd schema feature on tauri-config-schema [skip ci]

* remove plugin example from workspace, it breaks workspace features resolution [skip ci]

* scope as array, add test [skip ci]

* accept new shapshot [skip ci]

* core plugin permissions, default is now a set

* license headers

* fix  on windows

* update global api

* glob is no longer optional on tauri-utils

* add missing permissions on api example [skip ci]

* remove ipc scope and dangerous remote access config

* lint

* fix asset scope usage

* create out dir [skip ci]

* reuse cargo_pkg_name [skip ci]

* capability window glob pattern [skip ci]

* add platforms for capability [skip ci]

* per platform schema [skip ci]

* lint [skip ci]

* rename allowlist build mod [skip ci]

* check restricted visibility

* simplify capability target [skip ci]

* hide codegen build behind tauri-build::try_run

* optimize build scripts [skip ci]

* fix tests

* tests for RuntimeAuthority::resolve_access

* remote domain glob pattern

* lint

---------

Co-authored-by: Chip Reed <chip@chip.sh>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
  • Loading branch information
5 people committed Jan 23, 2024
1 parent 303708d commit 3c2f79f
Show file tree
Hide file tree
Showing 206 changed files with 9,564 additions and 819 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ members = [
"core/tauri-build",
"core/tauri-codegen",
"core/tauri-config-schema",
"core/tauri-plugin",

# integration tests
"core/tests/restart",
"core/tests/acl",
]

exclude = [
Expand All @@ -22,6 +24,7 @@ exclude = [
"examples/web/core",
"examples/file-associations/src-tauri",
"examples/workspace",
"examples/plugins/tauri-plugin-example",
]

[workspace.package]
Expand Down
3 changes: 3 additions & 0 deletions core/tauri-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ walkdir = "2"
tauri-winres = "0.1"
semver = "1"
dirs-next = "2"
glob = "0.3"
toml = "0.8"
schemars = "0.8"

[target."cfg(target_os = \"macos\")".dependencies]
swift-rs = { version = "1.0.6", features = [ "build" ] }
Expand Down
174 changes: 174 additions & 0 deletions core/tauri-build/src/acl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{
collections::BTreeMap,
fs::{copy, create_dir_all, File},
io::{BufWriter, Write},
path::PathBuf,
};

use anyhow::{Context, Result};
use schemars::{
schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation},
schema_for,
};
use tauri_utils::{
acl::{build::CapabilityFile, capability::Capability, plugin::Manifest},
platform::Target,
};

const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";

fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
let mut schema = schema_for!(CapabilityFile);

fn schema_from(plugin: &str, id: &str, description: Option<&str>) -> Schema {
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: description
.as_ref()
.map(|d| format!("{plugin}:{id} -> {d}")),
..Default::default()
})),
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![serde_json::Value::String(format!("{plugin}:{id}"))]),
..Default::default()
})
}

let mut permission_schemas = Vec::new();

for (plugin, manifest) in plugin_manifests {
for (set_id, set) in &manifest.permission_sets {
permission_schemas.push(schema_from(plugin, set_id, Some(&set.description)));
}

if let Some(default) = &manifest.default_permission {
permission_schemas.push(schema_from(
plugin,
"default",
Some(default.description.as_ref()),
));
}

for (permission_id, permission) in &manifest.permissions {
permission_schemas.push(schema_from(
plugin,
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()
}));
}

schema
}

pub fn generate_schema(
plugin_manifests: &BTreeMap<String, Manifest>,
target: Target,
) -> Result<()> {
let schema = capabilities_schema(plugin_manifests);
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
let out_dir = PathBuf::from("capabilities").join(CAPABILITIES_SCHEMA_FOLDER_NAME);
create_dir_all(&out_dir).context("unable to create schema output directory")?;

let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
let mut schema_file = BufWriter::new(File::create(&schema_path)?);
write!(schema_file, "{schema_str}")?;

copy(
schema_path,
out_dir.join(format!(
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
if target.is_desktop() {
"desktop"
} else {
"mobile"
}
)),
)?;

Ok(())
}

pub fn get_plugin_manifests() -> Result<BTreeMap<String, Manifest>> {
let permission_map =
tauri_utils::acl::build::read_permissions().context("failed to read plugin permissions")?;

let mut processed = BTreeMap::new();
for (plugin_name, permission_files) in permission_map {
processed.insert(plugin_name, Manifest::from_files(permission_files));
}

Ok(processed)
}

pub fn validate_capabilities(
plugin_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.contains(&target) {
continue;
}

for permission in &capability.permissions {
if let Some((plugin_name, permission_name)) = permission.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}"));
}
}

anyhow::bail!(
"Permission {} not found, expected one of {}",
permission.get(),
available_permissions.join(", ")
);
}
}
}
}

Ok(())
}
15 changes: 1 addition & 14 deletions core/tauri-build/src/codegen/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,7 @@ impl CodegenContext {
///
/// Unless you are doing something special with this builder, you don't need to do anything with
/// the returned output path.
///
/// # Panics
///
/// If any parts of the codegen fail, this will panic with the related error message. This is
/// typically desirable when running inside a build script; see [`Self::try_build`] for no panics.
pub fn build(self) -> PathBuf {
match self.try_build() {
Ok(out) => out,
Err(error) => panic!("Error found during Codegen::build: {error}"),
}
}

/// Non-panicking [`Self::build`]
pub fn try_build(self) -> Result<PathBuf> {
pub(crate) fn try_build(self) -> Result<PathBuf> {
let (config, config_parent) = tauri_codegen::get_config(&self.config_path)?;

// rerun if changed
Expand Down
60 changes: 55 additions & 5 deletions core/tauri-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ use cargo_toml::Manifest;
use heck::AsShoutySnakeCase;

use tauri_utils::{
acl::build::parse_capabilities,
config::{BundleResources, Config, WebviewInstallMode},
resources::{external_binaries, ResourcePaths},
};

use std::{
env::var_os,
fs::read_to_string,
path::{Path, PathBuf},
};

mod allowlist;
mod acl;
#[cfg(feature = "codegen")]
mod codegen;
/// Tauri configuration functions.
pub mod config;
mod manifest;
/// Mobile build functions.
pub mod mobile;
mod static_vcruntime;
Expand All @@ -40,6 +43,9 @@ mod static_vcruntime;
#[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
pub use codegen::context::CodegenContext;

const PLUGIN_MANIFESTS_FILE_NAME: &str = "plugin-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();
Expand Down Expand Up @@ -333,6 +339,9 @@ impl WindowsAttributes {
pub struct Attributes {
#[allow(dead_code)]
windows_attributes: WindowsAttributes,
capabilities_path_pattern: Option<&'static str>,
#[cfg(feature = "codegen")]
codegen: Option<codegen::context::CodegenContext>,
}

impl Attributes {
Expand All @@ -347,6 +356,21 @@ impl Attributes {
self.windows_attributes = windows_attributes;
self
}

/// Set the glob pattern to be used to find the capabilities.
#[must_use]
pub fn capabilities_path_pattern(mut self, pattern: &'static str) -> Self {
self.capabilities_path_pattern.replace(pattern);
self
}

#[cfg(feature = "codegen")]
#[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
#[must_use]
pub fn codegen(mut self, codegen: codegen::context::CodegenContext) -> Self {
self.codegen.replace(codegen);
self
}
}

/// Run all build time helpers for your Tauri Application.
Expand Down Expand Up @@ -399,8 +423,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
cfg_alias("desktop", !mobile);
cfg_alias("mobile", mobile);

let target_triple = std::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(
tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap()),
target,
std::env::current_dir().unwrap(),
)?)?;
if let Ok(env) = std::env::var("TAURI_CONFIG") {
Expand Down Expand Up @@ -441,13 +468,31 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
}

allowlist::check(&config, &mut manifest)?;
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());

let target_triple = std::env::var("TARGET").unwrap();
manifest::check(&config, &mut manifest)?;
let plugin_manifests = acl::get_plugin_manifests()?;
std::fs::write(
out_dir.join(PLUGIN_MANIFESTS_FILE_NAME),
serde_json::to_string(&plugin_manifests)?,
)?;
let capabilities = if let Some(pattern) = attributes.capabilities_path_pattern {
parse_capabilities(pattern)?
} else {
parse_capabilities("./capabilities/**/*")?
};
acl::generate_schema(&plugin_manifests, target)?;

acl::validate_capabilities(&plugin_manifests, &capabilities)?;

let capabilities_path = out_dir.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)?;
}

println!("cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE={target_triple}");

let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
// 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
.parent()
Expand Down Expand Up @@ -611,6 +656,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
}
}

#[cfg(feature = "codegen")]
if let Some(codegen) = attributes.codegen {
codegen.try_build()?;
}

Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ mod tests {
},
),
] {
assert_eq!(super::features_diff(&current, &expected), result);
assert_eq!(crate::manifest::features_diff(&current, &expected), result);
}
}
}

0 comments on commit 3c2f79f

Please sign in to comment.