Skip to content

Commit

Permalink
feat(cli): add new acl subcommands (#8827)
Browse files Browse the repository at this point in the history
* unify `CI` var handling, and lay foundation for `permission` subcommand

* feat(cli/init&new): create `permissions` directory by default for plugins

* generate permissions with consistent pathing on windows and unix

* `pemrission create` initial implementation

* add ls command

* finalize `permission create` subcommand

* `permission rm` subcommand

* `permission add` subcommand

* remove empty `permission copy` subcommand

* clippy

* `capability create` subcommand and move modules under `acl` directory

* fix multiselect for `permission add` when capabilty doesn't have identifier

* clippy

* `create` -> `new`  and change file

* license headers

* more license headers

* clippy

* Discard changes to examples/resources/src-tauri/.gitignore

* fix build

* cleanup

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
  • Loading branch information
amrbashir and lucasfernog authored Feb 26, 2024
1 parent 9be314f commit 06d63d6
Show file tree
Hide file tree
Showing 30 changed files with 949 additions and 124 deletions.
12 changes: 12 additions & 0 deletions .changes/cli-acl-subcommands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'tauri-cli': 'patch:feat'
'@tauri-apps/cli': 'patch:feat'
---

Add new subcommands for managing permissions and cababilities:

- `tauri permission new`
- `tauri permission add`
- `tauri permission rm`
- `tauri permission ls`
- `tauri capability new`
2 changes: 1 addition & 1 deletion core/tauri-plugin/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl<'a> Builder<'a> {
let _ = std::fs::remove_file(format!(
"./permissions/{}/{}",
acl::build::PERMISSION_SCHEMAS_FOLDER_NAME,
acl::build::PERMISSION_SCHEMA_FILE_NAME
acl::PERMISSION_SCHEMA_FILE_NAME
));
let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
} else {
Expand Down
4 changes: 1 addition & 3 deletions core/tauri-utils/src/acl/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use schemars::{
use super::{
capability::{Capability, CapabilityFile},
plugin::PermissionFile,
PERMISSION_SCHEMA_FILE_NAME,
};

/// Known name of the folder containing autogenerated permissions.
Expand All @@ -37,9 +38,6 @@ pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
/// Known foldername of the permission schema files
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";

/// Known filename of the permission schema JSON file
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";

/// Known filename of the permission documentation file
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";

Expand Down
3 changes: 2 additions & 1 deletion core/tauri-utils/src/acl/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub struct Capability {
#[serde(default)]
pub description: String,
/// Configure remote URLs that can use the capability permissions.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote: Option<CapabilityRemote>,
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
#[serde(default = "default_capability_local")]
Expand All @@ -74,7 +75,7 @@ pub struct Capability {
/// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
pub permissions: Vec<PermissionEntry>,
/// Target platforms this capability applies. By default all platforms applies.
#[serde(default = "default_platforms")]
#[serde(default = "default_platforms", skip_serializing_if = "Vec::is_empty")]
pub platforms: Vec<Target>,
}

Expand Down
13 changes: 12 additions & 1 deletion core/tauri-utils/src/acl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use thiserror::Error;

pub use self::{identifier::*, value::*};

/// Known filename of the permission schema JSON file
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";

#[cfg(feature = "build")]
pub mod build;
pub mod capability;
Expand Down Expand Up @@ -142,6 +145,12 @@ pub struct Scopes {
pub deny: Option<Vec<Value>>,
}

impl Scopes {
fn is_empty(&self) -> bool {
self.allow.is_none() && self.deny.is_none()
}
}

/// Descriptions of explicit privileges of commands.
///
/// It can enable commands to be accessible in the frontend of the application.
Expand All @@ -151,20 +160,22 @@ pub struct Scopes {
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Permission {
/// The version of the permission.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<NonZeroU64>,

/// A unique identifier for the permission.
pub identifier: String,

/// Human-readable description of what the permission does.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,

/// Allowed or denied commands when using this permission.
#[serde(default)]
pub commands: Commands,

/// Allowed or denied scoped when using this permission.
#[serde(default)]
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
pub scope: Scopes,
}

Expand Down
6 changes: 3 additions & 3 deletions core/tauri-utils/src/acl/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
/// The default permission set of the plugin.
///
/// Works similarly to a permission with the "default" identifier.
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DefaultPermission {
/// The version of the permission.
Expand All @@ -26,14 +26,14 @@ pub struct DefaultPermission {
}

/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
#[derive(Debug, Deserialize)]
#[derive(Debug, 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)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub set: Vec<PermissionSet>,

/// A list of inlined permissions
Expand Down
15 changes: 3 additions & 12 deletions tooling/cli/Cargo.lock

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

5 changes: 3 additions & 2 deletions tooling/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ anyhow = "1.0"
tauri-bundler = { version = "2.0.1-beta.0", default-features = false, path = "../bundler" }
colored = "2.0"
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
serde_json = { version = "1.0", features = [ "preserve_order" ] }
notify = "6.1"
notify-debouncer-mini = "0.4"
shared_child = "1.0"
duct = "0.13"
toml_edit = "0.21"
toml_edit = { version = "0.22", features = [ "serde" ] }
json-patch = "1.2"
tauri-utils = { version = "2.0.0-beta.4", path = "../../core/tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
tauri-utils-v1 = { version = "1", package = "tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
Expand Down Expand Up @@ -93,6 +93,7 @@ itertools = "0.11"
local-ip-address = "0.5"
css-color = "0.2"
resvg = "0.36.0"
dunce = "1"
glob = "0.3"

[target."cfg(windows)".dependencies]
Expand Down
28 changes: 28 additions & 0 deletions tooling/cli/src/acl/capability/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use clap::{Parser, Subcommand};

use crate::Result;

mod new;

#[derive(Debug, Parser)]
#[clap(about = "Manage or create capabilities for your app")]
pub struct Cli {
#[clap(subcommand)]
command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
#[clap(alias = "create")]
New(new::Options),
}

pub fn command(cli: Cli) -> Result<()> {
match cli.command {
Commands::New(options) => new::command(options),
}
}
141 changes: 141 additions & 0 deletions tooling/cli/src/acl/capability/new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{collections::HashSet, path::PathBuf};

use clap::Parser;
use tauri_utils::acl::capability::{Capability, PermissionEntry};

use crate::{
acl::FileFormat,
helpers::{app_paths::tauri_dir, prompts},
Result,
};

#[derive(Debug, Parser)]
#[clap(about = "Create a new permission file")]
pub struct Options {
/// Capability identifier.
identifier: Option<String>,
/// Capability description
#[clap(long)]
description: Option<String>,
/// Capability windows
#[clap(long)]
windows: Option<Vec<String>>,
/// Capability permissions
#[clap(long)]
permission: Option<Vec<String>>,
/// Output file format.
#[clap(long, default_value_t = FileFormat::Json)]
format: FileFormat,
/// The output file.
#[clap(short, long)]
out: Option<PathBuf>,
}

pub fn command(options: Options) -> Result<()> {
let identifier = match options.identifier {
Some(i) => i,
None => prompts::input("What's the capability identifier?", None, false, false)?.unwrap(),
};

let description = match options.description {
Some(d) => Some(d),
None => prompts::input::<String>("What's the capability description?", None, false, true)?
.and_then(|d| if d.is_empty() { None } else { Some(d) }),
};

let windows = match options.windows.map(FromIterator::from_iter) {
Some(w) => w,
None => prompts::input::<String>(
"Which windows should be affected by this? (comma separated)",
Some("main".into()),
false,
false,
)?
.and_then(|d| {
if d.is_empty() {
None
} else {
Some(d.split(',').map(ToString::to_string).collect())
}
})
.unwrap_or_default(),
};

let permissions: HashSet<String> = match options.permission.map(FromIterator::from_iter) {
Some(p) => p,
None => prompts::input::<String>(
"What permissions to enable? (comma separated)",
None,
false,
true,
)?
.and_then(|p| {
if p.is_empty() {
None
} else {
Some(p.split(',').map(ToString::to_string).collect())
}
})
.unwrap_or_default(),
};

let capability = Capability {
identifier,
description: description.unwrap_or_default(),
remote: None,
local: true,
windows,
webviews: Vec::new(),
permissions: permissions
.into_iter()
.map(|p| {
PermissionEntry::PermissionRef(
p.clone()
.try_into()
.unwrap_or_else(|_| panic!("invalid permission {}", p)),
)
})
.collect(),
platforms: Vec::new(),
};

let path = match options.out {
Some(o) => o.canonicalize()?,
None => {
let dir = tauri_dir();
let capabilities_dir = dir.join("capabilities");
capabilities_dir.join(format!(
"{}.{}",
capability.identifier,
options.format.extension()
))
}
};

if path.exists() {
let msg = format!(
"Capability already exists at {}",
dunce::simplified(&path).display()
);
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
if overwrite {
std::fs::remove_file(&path)?;
} else {
anyhow::bail!(msg);
}
}

if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}

std::fs::write(&path, options.format.serialize(&capability)?)?;

log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display());

Ok(())
}
Loading

0 comments on commit 06d63d6

Please sign in to comment.