Skip to content

Commit 06d63d6

Browse files
feat(cli): add new acl subcommands (#8827)
* 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>
1 parent 9be314f commit 06d63d6

File tree

30 files changed

+949
-124
lines changed

30 files changed

+949
-124
lines changed

.changes/cli-acl-subcommands.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'tauri-cli': 'patch:feat'
3+
'@tauri-apps/cli': 'patch:feat'
4+
---
5+
6+
Add new subcommands for managing permissions and cababilities:
7+
8+
- `tauri permission new`
9+
- `tauri permission add`
10+
- `tauri permission rm`
11+
- `tauri permission ls`
12+
- `tauri capability new`

core/tauri-plugin/src/build/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl<'a> Builder<'a> {
105105
let _ = std::fs::remove_file(format!(
106106
"./permissions/{}/{}",
107107
acl::build::PERMISSION_SCHEMAS_FOLDER_NAME,
108-
acl::build::PERMISSION_SCHEMA_FILE_NAME
108+
acl::PERMISSION_SCHEMA_FILE_NAME
109109
));
110110
let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
111111
} else {

core/tauri-utils/src/acl/build.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use schemars::{
2020
use super::{
2121
capability::{Capability, CapabilityFile},
2222
plugin::PermissionFile,
23+
PERMISSION_SCHEMA_FILE_NAME,
2324
};
2425

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

40-
/// Known filename of the permission schema JSON file
41-
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
42-
4341
/// Known filename of the permission documentation file
4442
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
4543

core/tauri-utils/src/acl/capability.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub struct Capability {
5757
#[serde(default)]
5858
pub description: String,
5959
/// Configure remote URLs that can use the capability permissions.
60+
#[serde(default, skip_serializing_if = "Option::is_none")]
6061
pub remote: Option<CapabilityRemote>,
6162
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
6263
#[serde(default = "default_capability_local")]
@@ -74,7 +75,7 @@ pub struct Capability {
7475
/// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
7576
pub permissions: Vec<PermissionEntry>,
7677
/// Target platforms this capability applies. By default all platforms applies.
77-
#[serde(default = "default_platforms")]
78+
#[serde(default = "default_platforms", skip_serializing_if = "Vec::is_empty")]
7879
pub platforms: Vec<Target>,
7980
}
8081

core/tauri-utils/src/acl/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use thiserror::Error;
1111

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

14+
/// Known filename of the permission schema JSON file
15+
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
16+
1417
#[cfg(feature = "build")]
1518
pub mod build;
1619
pub mod capability;
@@ -142,6 +145,12 @@ pub struct Scopes {
142145
pub deny: Option<Vec<Value>>,
143146
}
144147

148+
impl Scopes {
149+
fn is_empty(&self) -> bool {
150+
self.allow.is_none() && self.deny.is_none()
151+
}
152+
}
153+
145154
/// Descriptions of explicit privileges of commands.
146155
///
147156
/// It can enable commands to be accessible in the frontend of the application.
@@ -151,20 +160,22 @@ pub struct Scopes {
151160
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152161
pub struct Permission {
153162
/// The version of the permission.
163+
#[serde(skip_serializing_if = "Option::is_none")]
154164
pub version: Option<NonZeroU64>,
155165

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

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

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

166177
/// Allowed or denied scoped when using this permission.
167-
#[serde(default)]
178+
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
168179
pub scope: Scopes,
169180
}
170181

core/tauri-utils/src/acl/plugin.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
1212
/// The default permission set of the plugin.
1313
///
1414
/// Works similarly to a permission with the "default" identifier.
15-
#[derive(Debug, Deserialize)]
15+
#[derive(Debug, Deserialize, Serialize)]
1616
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1717
pub struct DefaultPermission {
1818
/// The version of the permission.
@@ -26,14 +26,14 @@ pub struct DefaultPermission {
2626
}
2727

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

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

3939
/// A list of inlined permissions

tooling/cli/Cargo.lock

Lines changed: 3 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tooling/cli/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ anyhow = "1.0"
5252
tauri-bundler = { version = "2.0.1-beta.0", default-features = false, path = "../bundler" }
5353
colored = "2.0"
5454
serde = { version = "1.0", features = [ "derive" ] }
55-
serde_json = "1.0"
55+
serde_json = { version = "1.0", features = [ "preserve_order" ] }
5656
notify = "6.1"
5757
notify-debouncer-mini = "0.4"
5858
shared_child = "1.0"
5959
duct = "0.13"
60-
toml_edit = "0.21"
60+
toml_edit = { version = "0.22", features = [ "serde" ] }
6161
json-patch = "1.2"
6262
tauri-utils = { version = "2.0.0-beta.4", path = "../../core/tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
6363
tauri-utils-v1 = { version = "1", package = "tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
@@ -93,6 +93,7 @@ itertools = "0.11"
9393
local-ip-address = "0.5"
9494
css-color = "0.2"
9595
resvg = "0.36.0"
96+
dunce = "1"
9697
glob = "0.3"
9798

9899
[target."cfg(windows)".dependencies]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use clap::{Parser, Subcommand};
6+
7+
use crate::Result;
8+
9+
mod new;
10+
11+
#[derive(Debug, Parser)]
12+
#[clap(about = "Manage or create capabilities for your app")]
13+
pub struct Cli {
14+
#[clap(subcommand)]
15+
command: Commands,
16+
}
17+
18+
#[derive(Subcommand, Debug)]
19+
enum Commands {
20+
#[clap(alias = "create")]
21+
New(new::Options),
22+
}
23+
24+
pub fn command(cli: Cli) -> Result<()> {
25+
match cli.command {
26+
Commands::New(options) => new::command(options),
27+
}
28+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use std::{collections::HashSet, path::PathBuf};
6+
7+
use clap::Parser;
8+
use tauri_utils::acl::capability::{Capability, PermissionEntry};
9+
10+
use crate::{
11+
acl::FileFormat,
12+
helpers::{app_paths::tauri_dir, prompts},
13+
Result,
14+
};
15+
16+
#[derive(Debug, Parser)]
17+
#[clap(about = "Create a new permission file")]
18+
pub struct Options {
19+
/// Capability identifier.
20+
identifier: Option<String>,
21+
/// Capability description
22+
#[clap(long)]
23+
description: Option<String>,
24+
/// Capability windows
25+
#[clap(long)]
26+
windows: Option<Vec<String>>,
27+
/// Capability permissions
28+
#[clap(long)]
29+
permission: Option<Vec<String>>,
30+
/// Output file format.
31+
#[clap(long, default_value_t = FileFormat::Json)]
32+
format: FileFormat,
33+
/// The output file.
34+
#[clap(short, long)]
35+
out: Option<PathBuf>,
36+
}
37+
38+
pub fn command(options: Options) -> Result<()> {
39+
let identifier = match options.identifier {
40+
Some(i) => i,
41+
None => prompts::input("What's the capability identifier?", None, false, false)?.unwrap(),
42+
};
43+
44+
let description = match options.description {
45+
Some(d) => Some(d),
46+
None => prompts::input::<String>("What's the capability description?", None, false, true)?
47+
.and_then(|d| if d.is_empty() { None } else { Some(d) }),
48+
};
49+
50+
let windows = match options.windows.map(FromIterator::from_iter) {
51+
Some(w) => w,
52+
None => prompts::input::<String>(
53+
"Which windows should be affected by this? (comma separated)",
54+
Some("main".into()),
55+
false,
56+
false,
57+
)?
58+
.and_then(|d| {
59+
if d.is_empty() {
60+
None
61+
} else {
62+
Some(d.split(',').map(ToString::to_string).collect())
63+
}
64+
})
65+
.unwrap_or_default(),
66+
};
67+
68+
let permissions: HashSet<String> = match options.permission.map(FromIterator::from_iter) {
69+
Some(p) => p,
70+
None => prompts::input::<String>(
71+
"What permissions to enable? (comma separated)",
72+
None,
73+
false,
74+
true,
75+
)?
76+
.and_then(|p| {
77+
if p.is_empty() {
78+
None
79+
} else {
80+
Some(p.split(',').map(ToString::to_string).collect())
81+
}
82+
})
83+
.unwrap_or_default(),
84+
};
85+
86+
let capability = Capability {
87+
identifier,
88+
description: description.unwrap_or_default(),
89+
remote: None,
90+
local: true,
91+
windows,
92+
webviews: Vec::new(),
93+
permissions: permissions
94+
.into_iter()
95+
.map(|p| {
96+
PermissionEntry::PermissionRef(
97+
p.clone()
98+
.try_into()
99+
.unwrap_or_else(|_| panic!("invalid permission {}", p)),
100+
)
101+
})
102+
.collect(),
103+
platforms: Vec::new(),
104+
};
105+
106+
let path = match options.out {
107+
Some(o) => o.canonicalize()?,
108+
None => {
109+
let dir = tauri_dir();
110+
let capabilities_dir = dir.join("capabilities");
111+
capabilities_dir.join(format!(
112+
"{}.{}",
113+
capability.identifier,
114+
options.format.extension()
115+
))
116+
}
117+
};
118+
119+
if path.exists() {
120+
let msg = format!(
121+
"Capability already exists at {}",
122+
dunce::simplified(&path).display()
123+
);
124+
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
125+
if overwrite {
126+
std::fs::remove_file(&path)?;
127+
} else {
128+
anyhow::bail!(msg);
129+
}
130+
}
131+
132+
if let Some(parent) = path.parent() {
133+
std::fs::create_dir_all(parent)?;
134+
}
135+
136+
std::fs::write(&path, options.format.serialize(&capability)?)?;
137+
138+
log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display());
139+
140+
Ok(())
141+
}

0 commit comments

Comments
 (0)