vault_audit_tools/
vault_api.rs

1//! Vault API client for entity enrichment.
2//!
3//! This module provides a client for interacting with the `HashiCorp` Vault API
4//! to enrich audit log data with additional entity information.
5//!
6//! # Environment Variables
7//!
8//! The client respects standard Vault environment variables:
9//!
10//! - `VAULT_ADDR` - Vault server address (e.g., `https://vault.example.com:8200`)
11//! - `VAULT_TOKEN` - Authentication token for API access
12//! - `VAULT_SKIP_VERIFY` - Skip TLS certificate verification (set to `1`, `true`, or `yes`)
13//! - `VAULT_CACERT` - Path to CA certificate for TLS verification
14//!
15//! # Examples
16//!
17//! ```no_run
18//! use vault_audit_tools::vault_api::VaultClient;
19//!
20//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
21//! let addr = "https://vault.example.com:8200";
22//! let token = "hvs.secret".to_string();
23//! let client = VaultClient::new(addr, token)?;
24//! # Ok(())
25//! # }
26//! ```
27
28use anyhow::{anyhow, Context, Result};
29use reqwest::Client;
30use serde::de::DeserializeOwned;
31use serde_json::Value;
32use std::env;
33use std::fs;
34
35/// Check if TLS verification should be skipped based on environment or flag.
36///
37/// Returns `true` if either the `insecure_flag` parameter is true, or if
38/// the `VAULT_SKIP_VERIFY` environment variable is set to a truthy value
39/// (`1`, `true`, `yes`, case-insensitive).
40pub fn should_skip_verify(insecure_flag: bool) -> bool {
41    if insecure_flag {
42        return true;
43    }
44
45    // Check VAULT_SKIP_VERIFY environment variable
46    env::var("VAULT_SKIP_VERIFY")
47        .ok()
48        .and_then(|v| {
49            v.parse::<bool>().ok().or_else(|| {
50                // Also accept "1", "true", "yes" (case-insensitive)
51                match v.to_lowercase().as_str() {
52                    "1" | "true" | "yes" => Some(true),
53                    _ => Some(false),
54                }
55            })
56        })
57        .unwrap_or(false)
58}
59
60/// Vault API client configuration.
61///
62/// Provides methods to interact with Vault's identity and secrets engines.
63#[derive(Debug, Clone)]
64pub struct VaultClient {
65    addr: String,
66    token: String,
67    client: Client,
68}
69
70impl VaultClient {
71    /// Create a new Vault client from address and token
72    pub fn new(addr: &str, token: String) -> Result<Self> {
73        Self::new_with_skip_verify(addr, token, false)
74    }
75
76    /// Create a new Vault client with option to skip TLS verification
77    pub fn new_with_skip_verify(addr: &str, token: String, skip_verify: bool) -> Result<Self> {
78        let client = Client::builder()
79            .danger_accept_invalid_certs(skip_verify)
80            .build()
81            .context("Failed to create HTTP client")?;
82
83        Ok(Self {
84            addr: addr.trim_end_matches('/').to_string(),
85            token,
86            client,
87        })
88    }
89
90    /// Create a client from environment variables
91    #[allow(dead_code)]
92    pub fn from_env() -> Result<Self> {
93        let addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string());
94
95        let token = if let Ok(token) = env::var("VAULT_TOKEN") {
96            token
97        } else if let Ok(token_file) = env::var("VAULT_TOKEN_FILE") {
98            fs::read_to_string(&token_file)
99                .with_context(|| format!("Failed to read token from file: {}", token_file))?
100                .trim()
101                .to_string()
102        } else {
103            return Err(anyhow!(
104                "VAULT_TOKEN or VAULT_TOKEN_FILE must be set. Provide a token via:\n\
105                 - Environment variable: export VAULT_TOKEN=hvs.xxxxx\n\
106                 - Token file: export VAULT_TOKEN_FILE=/path/to/token"
107            ));
108        };
109
110        Self::new(&addr, token)
111    }
112
113    /// Create a client with optional parameters (for CLI)
114    pub fn from_options(
115        vault_addr: Option<&str>,
116        vault_token: Option<&str>,
117        skip_verify: bool,
118    ) -> Result<Self> {
119        let addr = vault_addr
120            .map(std::string::ToString::to_string)
121            .or_else(|| env::var("VAULT_ADDR").ok())
122            .unwrap_or_else(|| "http://127.0.0.1:8200".to_string());
123
124        let token = if let Some(t) = vault_token {
125            t.to_string()
126        } else if let Ok(t) = env::var("VAULT_TOKEN") {
127            t
128        } else if let Ok(token_file) = env::var("VAULT_TOKEN_FILE") {
129            fs::read_to_string(&token_file)
130                .with_context(|| format!("Failed to read token from file: {}", token_file))?
131                .trim()
132                .to_string()
133        } else {
134            return Err(anyhow!(
135                "VAULT_TOKEN or VAULT_TOKEN_FILE must be set. Provide a token via:\n\
136                 - Command-line: --vault-token hvs.xxxxx\n\
137                 - Environment variable: export VAULT_TOKEN=hvs.xxxxx\n\
138                 - Token file: export VAULT_TOKEN_FILE=/path/to/token"
139            ));
140        };
141
142        Self::new_with_skip_verify(&addr, token, skip_verify)
143    }
144
145    /// Make a GET request to a Vault API endpoint
146    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
147        let url = format!("{}{}", self.addr, path);
148
149        let response = self
150            .client
151            .get(&url)
152            .header("X-Vault-Token", &self.token)
153            .send()
154            .await
155            .context("Failed to send request to Vault")?;
156
157        let status = response.status();
158        let body = response
159            .text()
160            .await
161            .context("Failed to read response body")?;
162
163        if !status.is_success() {
164            return Err(anyhow!(
165                "Vault API request failed with status {}: {}",
166                status,
167                body
168            ));
169        }
170
171        serde_json::from_str(&body)
172            .with_context(|| format!("Failed to parse JSON response from {}", path))
173    }
174
175    /// Make a GET request and return raw JSON Value
176    pub async fn get_json(&self, path: &str) -> Result<Value> {
177        self.get(path).await
178    }
179
180    /// Make a GET request and return raw text (useful for NDJSON)
181    pub async fn get_text(&self, path: &str) -> Result<String> {
182        let url = format!("{}{}", self.addr, path);
183
184        let response = self
185            .client
186            .get(&url)
187            .header("X-Vault-Token", &self.token)
188            .send()
189            .await
190            .context("Failed to send request to Vault")?;
191
192        let status = response.status();
193        let body = response
194            .text()
195            .await
196            .context("Failed to read response body")?;
197
198        if !status.is_success() {
199            return Err(anyhow!(
200                "Vault API request failed with status {}: {}",
201                status,
202                body
203            ));
204        }
205
206        Ok(body)
207    }
208
209    /// Get the Vault address
210    pub fn addr(&self) -> &str {
211        &self.addr
212    }
213}
214
215/// Helper to extract data from Vault response wrapper
216pub fn extract_data<T: DeserializeOwned>(value: Value) -> Result<T> {
217    if let Some(data) = value.get("data") {
218        serde_json::from_value(data.clone())
219            .context("Failed to deserialize data from Vault response")
220    } else {
221        serde_json::from_value(value).context("Failed to deserialize Vault response")
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_client_creation() {
231        let client = VaultClient::new("http://127.0.0.1:8200", "test-token".to_string());
232        assert!(client.is_ok());
233    }
234
235    #[test]
236    fn test_addr_trimming() {
237        let client = VaultClient::new("http://127.0.0.1:8200/", "test-token".to_string()).unwrap();
238        assert_eq!(client.addr(), "http://127.0.0.1:8200");
239    }
240}