vault_audit_tools/
vault_api.rs1use anyhow::{anyhow, Context, Result};
30use reqwest::Client;
31use serde::de::DeserializeOwned;
32use serde_json::Value;
33use std::env;
34use std::fs;
35
36pub fn should_skip_verify(insecure_flag: bool) -> bool {
42 if insecure_flag {
43 return true;
44 }
45
46 env::var("VAULT_SKIP_VERIFY")
48 .ok()
49 .and_then(|v| {
50 v.parse::<bool>().ok().or_else(|| {
51 match v.to_lowercase().as_str() {
53 "1" | "true" | "yes" => Some(true),
54 _ => Some(false),
55 }
56 })
57 })
58 .unwrap_or(false)
59}
60
61#[derive(Debug, Clone)]
65pub struct VaultClient {
66 addr: String,
67 token: String,
68 namespace: Option<String>,
69 client: Client,
70}
71
72impl VaultClient {
73 #[allow(dead_code)]
75 pub fn new(addr: &str, token: String) -> Result<Self> {
76 Self::new_with_skip_verify(addr, token, false)
77 }
78
79 #[allow(dead_code)]
81 pub fn new_with_skip_verify(addr: &str, token: String, skip_verify: bool) -> Result<Self> {
82 Self::new_with_options(addr, token, None, skip_verify)
83 }
84
85 pub fn new_with_options(
87 addr: &str,
88 token: String,
89 namespace: Option<String>,
90 skip_verify: bool,
91 ) -> Result<Self> {
92 let client = Client::builder()
93 .danger_accept_invalid_certs(skip_verify)
94 .build()
95 .context("Failed to create HTTP client")?;
96
97 Ok(Self {
98 addr: addr.trim_end_matches('/').to_string(),
99 token,
100 namespace,
101 client,
102 })
103 }
104
105 #[allow(dead_code)]
107 pub fn from_env() -> Result<Self> {
108 let addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string());
109 let namespace = env::var("VAULT_NAMESPACE").ok();
110
111 let token = if let Ok(token) = env::var("VAULT_TOKEN") {
112 token
113 } else if let Ok(token_file) = env::var("VAULT_TOKEN_FILE") {
114 fs::read_to_string(&token_file)
115 .with_context(|| format!("Failed to read token from file: {}", token_file))?
116 .trim()
117 .to_string()
118 } else {
119 return Err(anyhow!(
120 "VAULT_TOKEN or VAULT_TOKEN_FILE must be set. Provide a token via:\n\
121 - Environment variable: export VAULT_TOKEN=hvs.xxxxx\n\
122 - Token file: export VAULT_TOKEN_FILE=/path/to/token"
123 ));
124 };
125
126 Self::new_with_options(&addr, token, namespace, false)
127 }
128
129 pub fn from_options(
131 vault_addr: Option<&str>,
132 vault_token: Option<&str>,
133 vault_namespace: Option<&str>,
134 skip_verify: bool,
135 ) -> Result<Self> {
136 let addr = vault_addr
137 .map(std::string::ToString::to_string)
138 .or_else(|| env::var("VAULT_ADDR").ok())
139 .unwrap_or_else(|| "http://127.0.0.1:8200".to_string());
140
141 let namespace = vault_namespace
142 .map(std::string::ToString::to_string)
143 .or_else(|| env::var("VAULT_NAMESPACE").ok());
144
145 let token = if let Some(t) = vault_token {
146 t.to_string()
147 } else if let Ok(t) = env::var("VAULT_TOKEN") {
148 t
149 } else if let Ok(token_file) = env::var("VAULT_TOKEN_FILE") {
150 fs::read_to_string(&token_file)
151 .with_context(|| format!("Failed to read token from file: {}", token_file))?
152 .trim()
153 .to_string()
154 } else {
155 return Err(anyhow!(
156 "VAULT_TOKEN or VAULT_TOKEN_FILE must be set. Provide a token via:\n\
157 - Command-line: --vault-token hvs.xxxxx\n\
158 - Environment variable: export VAULT_TOKEN=hvs.xxxxx\n\
159 - Token file: export VAULT_TOKEN_FILE=/path/to/token"
160 ));
161 };
162
163 Self::new_with_options(&addr, token, namespace, skip_verify)
164 }
165
166 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
168 let url = format!("{}{}", self.addr, path);
169
170 let mut request = self.client.get(&url).header("X-Vault-Token", &self.token);
171
172 if let Some(namespace) = &self.namespace {
173 request = request.header("X-Vault-Namespace", namespace);
174 }
175
176 let response = request
177 .send()
178 .await
179 .context("Failed to send request to Vault")?;
180
181 let status = response.status();
182 let body = response
183 .text()
184 .await
185 .context("Failed to read response body")?;
186
187 if !status.is_success() {
188 return Err(anyhow!(
189 "Vault API request failed with status {}: {}",
190 status,
191 body
192 ));
193 }
194
195 serde_json::from_str(&body)
196 .with_context(|| format!("Failed to parse JSON response from {}", path))
197 }
198
199 pub async fn get_json(&self, path: &str) -> Result<Value> {
201 self.get(path).await
202 }
203
204 pub async fn get_text(&self, path: &str) -> Result<String> {
206 let url = format!("{}{}", self.addr, path);
207
208 let mut request = self.client.get(&url).header("X-Vault-Token", &self.token);
209
210 if let Some(namespace) = &self.namespace {
211 request = request.header("X-Vault-Namespace", namespace);
212 }
213
214 let response = request
215 .send()
216 .await
217 .context("Failed to send request to Vault")?;
218
219 let status = response.status();
220 let body = response
221 .text()
222 .await
223 .context("Failed to read response body")?;
224
225 if !status.is_success() {
226 return Err(anyhow!(
227 "Vault API request failed with status {}: {}",
228 status,
229 body
230 ));
231 }
232
233 Ok(body)
234 }
235
236 pub fn addr(&self) -> &str {
238 &self.addr
239 }
240}
241
242pub fn extract_data<T: DeserializeOwned>(value: Value) -> Result<T> {
244 if let Some(data) = value.get("data") {
245 serde_json::from_value(data.clone())
246 .context("Failed to deserialize data from Vault response")
247 } else {
248 serde_json::from_value(value).context("Failed to deserialize Vault response")
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_client_creation() {
258 let client = VaultClient::new("http://127.0.0.1:8200", "test-token".to_string());
259 assert!(client.is_ok());
260 }
261
262 #[test]
263 fn test_addr_trimming() {
264 let client = VaultClient::new("http://127.0.0.1:8200/", "test-token".to_string()).unwrap();
265 assert_eq!(client.addr(), "http://127.0.0.1:8200");
266 }
267}