Skip to content

Commit 4811636

Browse files
authored
Add CLI feature implementation. (#89)
1. Implement output in CLI table format. 2. Added 'status' command. 3. Added 'operator' sub-commands (init, seal, unseal) for system operations. 4. Added api implementation. 5. Implemented CLI commands for read, write, delete, and list operations. 6. Implemented CLI commands for auth and login operations.
1 parent 8e51c7e commit 4811636

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3510
-280
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"rust-analyzer.linkedProjects": [
33
"Cargo.toml"
44
]
5-
}
5+
}

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ serde = { version = "^1.0", features = ["derive", "rc", "alloc"] }
2323
serde_derive = "^1.0"
2424
serde_json = "^1.0"
2525
serde_bytes = "0.11"
26+
serde_yaml = "0.9"
2627
go-defer = "^0.1"
2728
rand = "^0.8"
2829
derivative = "2.2.0"
@@ -31,8 +32,8 @@ strum = { version = "0.25", features = ["derive"] }
3132
strum_macros = "0.25"
3233
radix_trie = "0.2.1"
3334
lazy_static = "1.4.0"
34-
regex = "1.9.5"
35-
clap = { version = "4.4", features = ["wrap_help", "suggestions"] }
35+
regex = "1.11"
36+
clap = { version = "4.5", features = ["wrap_help", "derive", "env", "suggestions"] }
3637
sysexits = { version = "0.7", features = ["std"] }
3738
build-time = "0.1"
3839
hcl-rs = "0.16"
@@ -54,8 +55,9 @@ r2d2-diesel = { version = "1.0.0", optional = true }
5455
bcrypt = "0.15"
5556
url = "2.5"
5657
ureq = { version = "2.10", features = ["json"] }
57-
rustls = "0.23"
58+
rustls = { version = "0.23.16" }
5859
rustls-pemfile = "2.1"
60+
webpki-roots = { version = "0.26", default-features = true }
5961
glob = "0.3"
6062
base64 = "0.22"
6163
ipnetwork = "0.20"
@@ -67,6 +69,8 @@ ctor = "0.2.8"
6769
better_default = "1.0.5"
6870
prometheus-client = "0.22.3"
6971
sysinfo = "0.31.4"
72+
prettytable = "0.10"
73+
rpassword = "7.3"
7074
async-trait = "0.1"
7175

7276
# optional dependencies
@@ -78,6 +82,9 @@ openssl-sys = { version = "0.9.102", optional = true }
7882
#openssl = { git = "https://github.com/Tongsuo-Project/rust-tongsuo.git" }
7983
#openssl-sys = { git = "https://github.com/Tongsuo-Project/rust-tongsuo.git" }
8084

85+
[build-dependencies]
86+
toml = "0.8.19"
87+
8188
[features]
8289
default = ["crypto_adaptor_openssl"]
8390
storage_mysql = ["diesel", "r2d2", "r2d2-diesel"]

bin/rusty_vault.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,16 @@
88
//! [documentation site]: https://www.tongsuo.net
99
1010
use std::process::ExitCode;
11+
use clap::{Parser, CommandFactory};
1112

12-
use clap::Command;
13-
use rusty_vault::cli;
13+
use rusty_vault::cli::Cli;
1414

1515
fn main() -> ExitCode {
16-
let mut app = Command::new("rusty_vault")
17-
.version(rusty_vault::VERSION)
18-
.help_expected(true)
19-
.disable_colored_help(false)
20-
.max_term_width(100)
21-
.about("A secure and high performance secret management software that is compatible with Hashicorp Vault.");
22-
app = cli::define_command_line_options(app);
23-
let mut app_cloned = app.clone();
16+
let mut cli = Cli::parse();
2417

25-
let matches = app.get_matches();
26-
let ret = cli::run(&matches);
18+
let ret = cli.run();
2719
if !ret.is_success() {
28-
let _ = app_cloned.print_long_help();
20+
Cli::command().print_long_help().unwrap();
2921
}
3022

3123
ret.into()

build.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use std::env;
1+
use std::{fs, env, path::Path};
2+
use toml::Value;
23

34
// This is not going to happen any more since we have a default feature definition in Cargo.toml
45
//#[cfg(not(any(feature = "crypto_adaptor_openssl", feature = "crypto_adaptor_tongsuo")))]
@@ -49,4 +50,29 @@ fn main() {
4950
} else if cfg!(feature = "crypto_adaptor_tongsuo") {
5051
println!("cargo:rustc-cfg=tongsuo");
5152
}
53+
54+
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
55+
let cargo_toml_path = Path::new(&manifest_dir).join("Cargo.toml");
56+
let content = match fs::read_to_string(cargo_toml_path) {
57+
Ok(c) => c,
58+
Err(_) => return,
59+
};
60+
let cargo_toml: Value = match toml::from_str(&content) {
61+
Ok(v) => v,
62+
Err(_) => return,
63+
};
64+
65+
if let Some(bin_table_value) = cargo_toml.get("bin") {
66+
if let Some(bin_table_array) = bin_table_value.as_array() {
67+
for bin_entry in bin_table_array {
68+
if let Some(bin_entry_table) = bin_entry.as_table() {
69+
if let Some(name_value) = bin_entry_table.get("name") {
70+
if let Some(name_str) = name_value.as_str() {
71+
println!("cargo:rustc-env=CARGO_BIN_NAME={}", name_str);
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
5278
}

src/api/auth.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use serde_json::{Map, Value};
2+
3+
use super::{Client, HttpResponse};
4+
use crate::errors::RvError;
5+
6+
pub trait LoginHandler: Send + Sync {
7+
fn auth(&self, client: &Client, data: &Map<String, Value>) -> Result<HttpResponse, RvError>;
8+
fn help(&self) -> String;
9+
}

src/api/client.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
use std::{
2+
fs,
3+
io::BufReader,
4+
sync::Arc,
5+
time::Duration,
6+
path::PathBuf,
7+
collections::HashMap,
8+
};
9+
10+
use serde_json::{Map, Value};
11+
use better_default::Default;
12+
use ureq::AgentBuilder;
13+
use rustls::{
14+
pki_types::{PrivateKeyDer, pem::PemObject},
15+
ALL_VERSIONS, ClientConfig, RootCertStore,
16+
};
17+
use webpki_roots::TLS_SERVER_ROOTS;
18+
19+
use super::HttpResponse;
20+
21+
use crate::{
22+
errors::RvError,
23+
utils::cert::DisabledVerifier,
24+
};
25+
26+
#[derive(Clone)]
27+
pub struct TLSConfig {
28+
client_config: ClientConfig,
29+
}
30+
31+
#[derive(Default)]
32+
pub struct TLSConfigBuilder {
33+
pub server_ca_pem: Option<Vec<u8>>,
34+
pub client_cert_pem: Option<Vec<u8>>,
35+
pub client_key_pem: Option<Vec<u8>>,
36+
pub tls_server_name: Option<String>,
37+
pub insecure: bool,
38+
}
39+
40+
#[derive(Default)]
41+
pub struct Client {
42+
#[default("https://127.0.0.1:8200".into())]
43+
pub address: String,
44+
pub token: String,
45+
#[default(HashMap::new())]
46+
pub headers: HashMap<String, String>,
47+
pub tls_config: Option<TLSConfig>,
48+
#[default(ureq::Agent::new())]
49+
pub http_client: ureq::Agent,
50+
}
51+
52+
impl TLSConfigBuilder {
53+
pub fn new() -> Self {
54+
TLSConfigBuilder::default()
55+
}
56+
57+
pub fn with_server_ca_path(mut self, server_ca_path: &PathBuf) -> Result<Self, RvError> {
58+
let cert_data = fs::read(server_ca_path)?;
59+
self.server_ca_pem = Some(cert_data);
60+
Ok(self)
61+
}
62+
63+
pub fn with_server_ca_pem(mut self, server_ca_pem: &str) -> Self {
64+
self.server_ca_pem = Some(server_ca_pem.as_bytes().to_vec());
65+
self
66+
}
67+
68+
pub fn with_client_cert_path(mut self, client_cert_path: &PathBuf, client_key_path: &PathBuf) -> Result<Self, RvError> {
69+
let cert_data = fs::read(client_cert_path)?;
70+
self.client_cert_pem = Some(cert_data);
71+
72+
let key_data = fs::read(client_key_path)?;
73+
self.client_key_pem = Some(key_data);
74+
75+
Ok(self)
76+
}
77+
78+
pub fn with_client_cert_pem(mut self, client_cert_pem: &str, client_key_pem: &str) -> Self {
79+
self.client_cert_pem = Some(client_cert_pem.as_bytes().to_vec());
80+
self.client_key_pem = Some(client_key_pem.as_bytes().to_vec());
81+
82+
self
83+
}
84+
85+
pub fn with_insecure(mut self, insecure: bool) -> Self {
86+
self.insecure = insecure;
87+
88+
self
89+
}
90+
91+
pub fn build(self) -> Result<TLSConfig, RvError> {
92+
let provider = rustls::crypto::CryptoProvider::get_default()
93+
.cloned()
94+
.unwrap_or(Arc::new(rustls::crypto::ring::default_provider()));
95+
96+
let builder = ClientConfig::builder_with_provider(provider.clone())
97+
.with_protocol_versions(ALL_VERSIONS)
98+
.expect("all TLS versions");
99+
100+
let builder = if self.insecure {
101+
log::debug!("Certificate verification disabled");
102+
builder
103+
.dangerous()
104+
.with_custom_certificate_verifier(Arc::new(DisabledVerifier))
105+
} else {
106+
if let Some(server_ca) = &self.server_ca_pem {
107+
let mut cert_reader = BufReader::new(&server_ca[..]);
108+
let root_certs = rustls_pemfile::certs(&mut cert_reader).collect::<Result<Vec<_>, _>>()?;
109+
110+
let mut root_store = RootCertStore::empty();
111+
let (_added, _ignored) = root_store.add_parsable_certificates(root_certs);
112+
builder.with_root_certificates(root_store)
113+
} else {
114+
let root_store = RootCertStore {
115+
roots: TLS_SERVER_ROOTS.to_vec(),
116+
};
117+
builder.with_root_certificates(root_store)
118+
}
119+
};
120+
121+
let client_config = if let (Some(client_cert_pem), Some(client_key_pem)) = (&self.client_cert_pem, &self.client_key_pem) {
122+
let mut cert_reader = BufReader::new(&client_cert_pem[..]);
123+
let client_certs = rustls_pemfile::certs(&mut cert_reader).collect::<Result<Vec<_>, _>>()?;
124+
let client_key = PrivateKeyDer::from_pem_slice(client_key_pem)?;
125+
126+
builder.with_client_auth_cert(client_certs, client_key)?
127+
} else {
128+
builder.with_no_client_auth()
129+
};
130+
131+
Ok(TLSConfig {
132+
client_config
133+
})
134+
}
135+
}
136+
137+
impl Client {
138+
pub fn new() -> Self {
139+
Client::default()
140+
}
141+
142+
pub fn with_addr(mut self, addr: &str) -> Self {
143+
self.address = addr.into();
144+
self
145+
}
146+
147+
pub fn with_token(mut self, token: &str) -> Self {
148+
self.token = token.into();
149+
self
150+
}
151+
152+
pub fn with_tls_config(mut self, tls_config: TLSConfig) -> Self {
153+
self.tls_config = Some(tls_config);
154+
self
155+
}
156+
157+
pub fn add_header(mut self, key: &str, value: &str) -> Self {
158+
self.headers.insert(key.into(), value.into());
159+
self
160+
}
161+
162+
pub fn build(mut self) -> Self {
163+
let mut agent = AgentBuilder::new()
164+
.timeout_connect(Duration::from_secs(10))
165+
.timeout(Duration::from_secs(30));
166+
167+
if let Some(tls_config) = &self.tls_config {
168+
agent = agent.tls_config(Arc::new(tls_config.client_config.clone()));
169+
}
170+
171+
self.http_client = agent.build();
172+
self
173+
}
174+
175+
pub fn request(
176+
&self,
177+
method: &str,
178+
path: &str,
179+
data: Option<Map<String, Value>>
180+
) -> Result<HttpResponse, RvError> {
181+
let url = if path.starts_with("/") {
182+
format!("{}{}", self.address, path)
183+
} else {
184+
format!("{}/{}", self.address, path)
185+
};
186+
log::debug!("request url: {}, method: {}", url, method);
187+
188+
let mut req = self.http_client.request(&method.to_uppercase(), &url);
189+
190+
req = req.set("Accept", "application/json");
191+
if !path.ends_with("/login") {
192+
req = req.set("X-RustyVault-Token", &self.token);
193+
}
194+
195+
let mut ret = HttpResponse {
196+
method: method.to_string(),
197+
url: url,
198+
..Default::default()
199+
};
200+
201+
let response_result = if let Some(send_data) = data { req.send_json(send_data) } else { req.call() };
202+
203+
match response_result {
204+
Ok(response) => {
205+
ret.response_status = response.status();
206+
if ret.response_status == 204 {
207+
return Ok(ret.clone());
208+
}
209+
let json: Value = response.into_json()?;
210+
ret.response_data = Some(json);
211+
return Ok(ret.clone());
212+
}
213+
Err(ureq::Error::Status(status, response)) => {
214+
ret.response_status = status;
215+
if let Ok(response_data) = response.into_json() {
216+
ret.response_data = Some(response_data);
217+
}
218+
return Ok(ret.clone());
219+
}
220+
Err(e) => {
221+
log::error!("Request failed: {}", e);
222+
return Err(RvError::UreqError { source: e });
223+
}
224+
}
225+
}
226+
227+
pub fn request_list(&self, path: &str) -> Result<HttpResponse, RvError> {
228+
self.request("LIST", path, None)
229+
}
230+
231+
pub fn request_read(&self, path: &str) -> Result<HttpResponse, RvError> {
232+
self.request("GET", path, None)
233+
}
234+
235+
pub fn request_get(&self, path: &str) -> Result<HttpResponse, RvError> {
236+
self.request("GET", path, None)
237+
}
238+
239+
pub fn request_write(&self, path: &str, data: Option<Map<String, Value>>,
240+
) -> Result<HttpResponse, RvError> {
241+
self.request("POST", path, data)
242+
}
243+
244+
pub fn request_put(&self, path: &str, data: Option<Map<String, Value>>,
245+
) -> Result<HttpResponse, RvError> {
246+
self.request("PUT", path, data)
247+
}
248+
249+
pub fn request_delete(&self, path: &str, data: Option<Map<String, Value>>,
250+
) -> Result<HttpResponse, RvError> {
251+
self.request("DELETE", path, data)
252+
}
253+
}

0 commit comments

Comments
 (0)