Skip to content

Commit

Permalink
capabilities schema
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Jan 15, 2024
1 parent eba00d6 commit 781243e
Show file tree
Hide file tree
Showing 15 changed files with 126 additions and 12 deletions.
1 change: 1 addition & 0 deletions core/tauri-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ 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
110 changes: 108 additions & 2 deletions core/tauri-build/src/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,129 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::collections::HashMap;
use std::{
collections::HashMap,
fs::File,
io::{BufWriter, Write},
path::PathBuf,
};

use anyhow::{Context, Result};
use schemars::{
gen::SchemaGenerator,
schema::{InstanceType, Metadata, RootSchema, Schema, SchemaObject, SubschemaValidation},
};
use serde::Deserialize;
use tauri_utils::acl::{capability::Capability, plugin::Manifest, Permission, PermissionSet};

const CAPABILITY_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
const CAPABILITIES_SCHEMA_FILE_NAME: &str = ".schema.json";

#[derive(Deserialize)]
#[serde(untagged)]
enum CapabilityFile {
Capability(Capability),
List { capabilities: Vec<Capability> },
}

fn capabilities_schema(plugin_manifests: &HashMap<String, Manifest>) -> RootSchema {
let mut schema = SchemaGenerator::default().into_root_schema_for::<Capability>();

let mut permission_schemas = Vec::new();
for (plugin, manifest) in plugin_manifests {
for (set_id, set) in &manifest.permission_sets {
permission_schemas.push(Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(format!("{plugin}:{set_id} -> {}", set.description)),
..Default::default()
})),
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![serde_json::Value::String(format!(
"{plugin}:{set_id}"
))]),
..Default::default()
}));
}

if let Some(default) = &manifest.default_permission {
permission_schemas.push(Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: default
.description
.as_ref()
.map(|d| format!("{plugin}:default -> {d}")),
..Default::default()
})),
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![serde_json::Value::String(format!("{plugin}:default"))]),
..Default::default()
}));
}

for (permission_id, permission) in &manifest.permissions {
permission_schemas.push(Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: permission
.description
.as_ref()
.map(|d| format!("{plugin}:{permission_id} -> {d}")),
..Default::default()
})),
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![serde_json::Value::String(format!(
"{plugin}:{permission_id}"
))]),
..Default::default()
}));
}
}

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(crate) fn generate_schema(plugin_manifests: &HashMap<String, Manifest>) -> Result<()> {
let schema = capabilities_schema(plugin_manifests);
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
let out_path = PathBuf::from("capabilities").join(CAPABILITIES_SCHEMA_FILE_NAME);

let mut schema_file = BufWriter::new(File::create(out_path)?);
write!(schema_file, "{schema_str}")?;
Ok(())
}

pub fn parse_capabilities(capabilities_path_pattern: &str) -> Result<HashMap<String, Capability>> {
let mut capabilities_map = HashMap::new();

for path in glob::glob(capabilities_path_pattern)?.flatten() {
for path in glob::glob(capabilities_path_pattern)?
.flatten() // filter extension
.filter(|p| {
p.extension()
.and_then(|e| e.to_str())
.map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e))
.unwrap_or_default()
})
// filter schema file
.filter(|p| {
p.file_name()
.map(|name| name != CAPABILITIES_SCHEMA_FILE_NAME)
.unwrap_or(true)
})
{
println!("cargo:rerun-if-changed={}", path.display());

let capability_file = std::fs::read_to_string(&path)?;
Expand Down
1 change: 1 addition & 0 deletions core/tauri-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
} else {
acl::parse_capabilities("./capabilities/**/*")?
};
acl::generate_schema(&plugin_manifests)?;

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

Expand Down
4 changes: 2 additions & 2 deletions core/tauri-plugin/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ fn autogenerate_command_permissions(path: &Path, commands: &[&str]) {
[[permission]]
identifier = "allow-{slugified_command}"
description = "This enables the {command} command without any pre-configured scope."
description = "Enables the {command} command without any pre-configured scope."
commands.allow = ["{command}"]
[[permission]]
identifier = "deny-{slugified_command}"
description = "This denies the {command} command without any pre-configured scope."
description = "Denies the {command} command without any pre-configured scope."
commands.deny = ["{command}"]
"###,
command = command,
Expand Down
2 changes: 2 additions & 0 deletions core/tauri-utils/src/acl/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct CapabilitySet {
/// This can be done to create trust groups and reduce impact of vulnerabilities in certain plugins or windows.
/// Windows can be added to a capability by exact name or glob patterns like *, admin-* or main-window.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Capability {
/// Identifier of the capability.
pub identifier: String,
Expand All @@ -40,6 +41,7 @@ pub struct Capability {

/// Context of the capability.
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum CapabilityContext {
/// Capability refers to local URL usage.
#[default]
Expand Down
1 change: 1 addition & 0 deletions core/tauri-utils/src/acl/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;

/// Plugin identifier.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Identifier {
inner: String,
separator: Option<NonZeroU8>,
Expand Down
1 change: 1 addition & 0 deletions examples/api/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/api/src-tauri/capabilities/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.schema.json
1 change: 1 addition & 0 deletions examples/api/src-tauri/capabilities/run-app.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"$schema" = ".schema.json"
identifier = "run-app"
description = "app capability"
windows = ["main"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

[[permission]]
identifier = "allow-ping"
description = "This enables the ping command without any pre-configured scope."
description = "Enables the ping command without any pre-configured scope."
commands.allow = ["ping"]

[[permission]]
identifier = "deny-ping"
description = "This denies the ping command without any pre-configured scope."
description = "Denies the ping command without any pre-configured scope."
commands.deny = ["ping"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

[[permission]]
identifier = "global-scope"
description = "This sets a global scope."
description = "Sets a global scope."
[permission.scope.allow]
path = "global"
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[[permission]]
identifier = "allow-ping-scoped"
description = "This enables the ping command with a test scope."
description = "Enables the ping command with a test scope."
commands.allow = ["ping"]
[permission.scope.allow]
path = "x"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[[permission]]
version = 1
identifier = "deny-home-dir-config"
description = "This denies read access to the complete $HOME folder."
description = "Denies read access to the complete $HOME folder."

[[scope.deny]]
path = "$HOME/.config"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[[permission]]
version = 1
identifier = "allow-home-dir"
description = "This allows read access to the complete $HOME folder."
description = "Allows read access to the complete $HOME folder."
commands.allow = ["readDirectory", "readFile"]

[[scope.allow]]
Expand Down
4 changes: 2 additions & 2 deletions examples/plugins/tauri-plugin-example/permissions/set.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ permissions = ["allow-home-read-only"]

[[set]]
identifier = "allow-full-homefolder-access"
description = "This allows read and write access to the complete $HOME folder."
description = "Allows read and write access to the complete $HOME folder."
permissions = ["allow-home-read-only", "allow-home-write-only"]

[[set]]
identifier = "deny-homefolder-config-access"
description = "This denies access to the $HOME/.config folder."
description = "Denies access to the $HOME/.config folder."
permissions = ["deny-home-dir-config"]

0 comments on commit 781243e

Please sign in to comment.