Skip to content

Commit 8bedbcf

Browse files
authored
Merge pull request #126 from wa5i/cli
Add CLI functionality for policy and its subcommands.
2 parents e6481c0 + 662de73 commit 8bedbcf

13 files changed

+710
-14
lines changed

src/api/sys.rs

+20
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,24 @@ impl Sys<'_> {
106106

107107
self.request_write("/v1/sys/remount", data.as_object().cloned())
108108
}
109+
110+
pub fn list_policy(&self) -> Result<HttpResponse, RvError> {
111+
self.request_read("/v1/sys/policies/acl")
112+
}
113+
114+
pub fn read_policy(&self, name: &str) -> Result<HttpResponse, RvError> {
115+
self.request_read(&format!("/v1/sys/policies/acl/{}", name))
116+
}
117+
118+
pub fn write_policy(&self, name: &str, policy: &str) -> Result<HttpResponse, RvError> {
119+
let data = json!({
120+
"policy": policy,
121+
});
122+
123+
self.request_write(&format!("/v1/sys/policies/acl/{}", name), data.as_object().cloned())
124+
}
125+
126+
pub fn delete_policy(&self, name: &str) -> Result<HttpResponse, RvError> {
127+
self.request_delete(&format!("/v1/sys/policies/acl/{}", name), None)
128+
}
109129
}

src/cli/command/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ pub mod operator;
2727
pub mod operator_init;
2828
pub mod operator_seal;
2929
pub mod operator_unseal;
30+
pub mod policy;
31+
pub mod policy_delete;
32+
pub mod policy_list;
33+
pub mod policy_read;
34+
pub mod policy_write;
3035
pub mod read;
3136
pub mod server;
3237
pub mod status;

src/cli/command/policy.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use clap::{Parser, Subcommand};
2+
use sysexits::ExitCode;
3+
4+
use super::{policy_delete, policy_list, policy_read, policy_write};
5+
use crate::{cli::command::CommandExecutor, EXIT_CODE_INSUFFICIENT_PARAMS};
6+
7+
#[derive(Parser)]
8+
#[command(
9+
author,
10+
version,
11+
about = "Perform operator-specific tasks",
12+
long_about = r#"This command groups subcommands for interacting with policies.
13+
Users can write, read, and list policies in RustyVault.
14+
15+
List all enabled policies:
16+
17+
$ rvault policy list
18+
19+
Create a policy named "my-policy" from contents on local disk:
20+
21+
$ rvault policy write my-policy ./my-policy.hcl
22+
23+
Delete the policy named my-policy:
24+
25+
$ rvault policy delete my-policy
26+
27+
Please see the individual subcommand help for detailed usage information."#
28+
)]
29+
pub struct Policy {
30+
#[command(subcommand)]
31+
command: Option<Commands>,
32+
}
33+
34+
#[derive(Subcommand)]
35+
pub enum Commands {
36+
List(policy_list::List),
37+
Write(policy_write::Write),
38+
Delete(policy_delete::Delete),
39+
Read(policy_read::Read),
40+
}
41+
42+
impl Commands {
43+
pub fn execute(&mut self) -> ExitCode {
44+
match self {
45+
Commands::List(list) => list.execute(),
46+
Commands::Write(write) => write.execute(),
47+
Commands::Read(read) => read.execute(),
48+
Commands::Delete(delete) => delete.execute(),
49+
}
50+
}
51+
}
52+
53+
impl Policy {
54+
#[inline]
55+
pub fn execute(&mut self) -> ExitCode {
56+
if let Some(ref mut cmd) = &mut self.command {
57+
return cmd.execute();
58+
}
59+
60+
EXIT_CODE_INSUFFICIENT_PARAMS
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod test {
66+
use crate::{errors::RvError, rv_error_string, test_utils::TestHttpServer};
67+
68+
#[test]
69+
fn test_cli_policy() {
70+
let mut test_http_server = TestHttpServer::new("test_cli_policy", true);
71+
test_http_server.token = test_http_server.root_token.clone();
72+
73+
// There is no data by default, and reading should fail.
74+
let ret = test_http_server.cli(&["read"], &["kv/foo"]);
75+
assert!(ret.is_err());
76+
assert_eq!(ret.unwrap_err(), rv_error_string!("No value found at kv/foo\n"));
77+
}
78+
}

src/cli/command/policy_delete.rs

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use clap::Parser;
2+
use derive_more::Deref;
3+
4+
use crate::{
5+
cli::command::{self, CommandExecutor},
6+
errors::RvError,
7+
};
8+
9+
#[derive(Parser, Deref)]
10+
#[command(
11+
author,
12+
version,
13+
about = r#"Deletes the policy named NAME in the RustyVault server. Once the policy is deleted,
14+
all tokens associated with the policy are affected immediately.
15+
16+
Delete the policy named "my-policy":
17+
18+
$ rvault policy delete my-policy
19+
20+
Note that it is not possible to delete the "default" or "root" policies.
21+
These are built-in policies.
22+
"#
23+
)]
24+
pub struct Delete {
25+
#[clap(index = 1, value_name = "NAME", help = r#"The name of policy"#)]
26+
name: String,
27+
28+
#[deref]
29+
#[command(flatten, next_help_heading = "HTTP Options")]
30+
http_options: command::HttpOptions,
31+
32+
#[command(flatten, next_help_heading = "Output Options")]
33+
output: command::OutputOptions,
34+
}
35+
36+
impl CommandExecutor for Delete {
37+
#[inline]
38+
fn main(&self) -> Result<(), RvError> {
39+
let client = self.client()?;
40+
let sys = client.sys();
41+
42+
let policy_name = self.name.trim().to_lowercase();
43+
44+
match sys.delete_policy(&policy_name) {
45+
Ok(ret) => {
46+
if ret.response_status == 200 || ret.response_status == 204 {
47+
println!("Success! Deleted policy: {}", policy_name);
48+
} else {
49+
ret.print_debug_info();
50+
std::process::exit(2);
51+
}
52+
}
53+
Err(e) => eprintln!("{}", e),
54+
}
55+
56+
Ok(())
57+
}
58+
}
59+
60+
#[cfg(test)]
61+
mod test {
62+
use crate::test_utils::TestHttpServer;
63+
64+
#[test]
65+
fn test_cli_policy_delete() {
66+
let mut test_http_server = TestHttpServer::new("test_cli_policy_delete", true);
67+
test_http_server.token = test_http_server.root_token.clone();
68+
69+
// delete default should be failed
70+
let ret = test_http_server.cli(&["policy", "delete"], &["default"]);
71+
assert!(ret.is_err());
72+
let err = ret.unwrap_err().to_string();
73+
assert!(err.contains("cannot delete default policy"));
74+
assert!(err.contains("Code: 400. Error"));
75+
76+
// delete root should be failed
77+
let ret = test_http_server.cli(&["policy", "delete"], &["root"]);
78+
assert!(ret.is_err());
79+
let err = ret.unwrap_err().to_string();
80+
assert!(err.contains("cannot delete root policy"));
81+
assert!(err.contains("Code: 400. Error"));
82+
83+
// write a test policy
84+
let test_policy = r#"path "secret/" {}"#;
85+
let client = test_http_server.client().unwrap();
86+
let sys = client.sys();
87+
assert!(sys.write_policy("my-policy", test_policy).is_ok());
88+
89+
// delete default should be ok
90+
let ret = test_http_server.cli(&["policy", "delete"], &["my-policy"]);
91+
assert!(ret.is_ok());
92+
assert_eq!(ret.unwrap(), "Success! Deleted policy: my-policy\n");
93+
}
94+
}

src/cli/command/policy_list.rs

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use clap::Parser;
2+
use derive_more::Deref;
3+
4+
use crate::{
5+
cli::command::{self, CommandExecutor},
6+
errors::RvError,
7+
};
8+
9+
#[derive(Parser, Deref)]
10+
#[command(author, version, about = r#"Lists the names of the policies that are installed on the RustyVault server."#)]
11+
pub struct List {
12+
#[deref]
13+
#[command(flatten, next_help_heading = "HTTP Options")]
14+
http_options: command::HttpOptions,
15+
16+
#[command(flatten, next_help_heading = "Output Options")]
17+
output: command::OutputOptions,
18+
}
19+
20+
impl CommandExecutor for List {
21+
#[inline]
22+
fn main(&self) -> Result<(), RvError> {
23+
let client = self.client()?;
24+
let sys = client.sys();
25+
26+
match sys.list_policy() {
27+
Ok(ret) => {
28+
if ret.response_status == 200 {
29+
let value = ret.response_data.as_ref().unwrap();
30+
let keys = &value["keys"];
31+
if *keys == serde_json::from_str::<serde_json::Value>("[]").unwrap() {
32+
ret.print_debug_info();
33+
println!("No policy");
34+
return Err(RvError::ErrRequestNoData);
35+
}
36+
self.output.print_value(keys, false)?;
37+
} else if ret.response_status == 404 {
38+
ret.print_debug_info();
39+
println!("No policy");
40+
return Err(RvError::ErrRequestNoData);
41+
} else {
42+
ret.print_debug_info();
43+
}
44+
}
45+
Err(e) => eprintln!("{}", e),
46+
}
47+
48+
Ok(())
49+
}
50+
}
51+
52+
#[cfg(test)]
53+
mod test {
54+
use crate::test_utils::TestHttpServer;
55+
56+
#[test]
57+
fn test_cli_policy_list() {
58+
let mut test_http_server = TestHttpServer::new("test_cli_policy_list", true);
59+
test_http_server.token = test_http_server.root_token.clone();
60+
61+
// list policy
62+
let ret = test_http_server.cli(&["policy", "list"], &[]);
63+
assert!(ret.is_ok());
64+
#[cfg(windows)]
65+
assert_eq!(ret.unwrap(), "default \r\nroot \r\n");
66+
#[cfg(not(windows))]
67+
assert_eq!(ret.unwrap(), "default \nroot \n");
68+
69+
// write a test policy
70+
let test_policy = r#"path "secret/" {}"#;
71+
let client = test_http_server.client().unwrap();
72+
let sys = client.sys();
73+
assert!(sys.write_policy("my-policy", test_policy).is_ok());
74+
75+
// list policy again
76+
let ret = test_http_server.cli(&["policy", "list"], &[]);
77+
assert!(ret.is_ok());
78+
#[cfg(windows)]
79+
assert_eq!(ret.unwrap(), "default \r\nmy-policy \r\nroot \r\n");
80+
#[cfg(not(windows))]
81+
assert_eq!(ret.unwrap(), "default \nmy-policy \nroot \n");
82+
83+
// list policy with table format
84+
let ret = test_http_server.cli(&["policy", "list"], &["--format=table"]);
85+
assert!(ret.is_ok());
86+
#[cfg(windows)]
87+
assert_eq!(ret.unwrap(), "default \r\nmy-policy \r\nroot \r\n");
88+
#[cfg(not(windows))]
89+
assert_eq!(ret.unwrap(), "default \nmy-policy \nroot \n");
90+
91+
// list policy with json format
92+
let ret = test_http_server.cli(&["policy", "list"], &["--format=json"]);
93+
assert!(ret.is_ok());
94+
assert_eq!(ret.unwrap(), "[\n \"default\",\n \"my-policy\",\n \"root\"\n]\n");
95+
96+
// list policy with yaml format
97+
let ret = test_http_server.cli(&["policy", "list"], &["--format=yaml"]);
98+
assert!(ret.is_ok());
99+
assert_eq!(ret.unwrap(), "- default\n- my-policy\n- root\n");
100+
}
101+
}

0 commit comments

Comments
 (0)