vaultrs/
client.rs

1use crate::api::AuthInfo;
2use crate::api::{token::responses::LookupTokenResponse, EndpointMiddleware};
3use crate::error::ClientError;
4use async_trait::async_trait;
5pub use reqwest::Identity;
6use rustify::clients::reqwest::Client as HTTPClient;
7use std::time::Duration;
8use std::{env, fs};
9use url::Url;
10
11/// Valid URL schemes that can be used for a Vault server address
12const VALID_SCHEMES: [&str; 2] = ["http", "https"];
13
14/// The client interface capabale of interacting with API functions
15#[async_trait]
16pub trait Client: Send + Sync + Sized {
17    /// Returns the underlying HTTP client being used for API calls
18    fn http(&self) -> &HTTPClient;
19
20    /// Returns the middleware to be used when executing API calls
21    fn middle(&self) -> &EndpointMiddleware;
22
23    /// Returns the settings used to configure this client
24    fn settings(&self) -> &VaultClientSettings;
25
26    /// Sets the underlying token for this client
27    fn set_token(&mut self, token: &str);
28
29    /// Looks up the current token being used by this client
30    async fn lookup(&self) -> Result<LookupTokenResponse, ClientError> {
31        crate::token::lookup_self(self).await
32    }
33
34    /// Renews the current token being used by this client
35    async fn renew(&self, increment: Option<&str>) -> Result<AuthInfo, ClientError> {
36        crate::token::renew_self(self, increment).await
37    }
38
39    /// Revokes the current token being used by this client
40    async fn revoke(&self) -> Result<(), ClientError> {
41        crate::token::revoke_self(self).await
42    }
43
44    /// Returns the status of the configured Vault server
45    async fn status(&self) -> Result<crate::sys::ServerStatus, ClientError> {
46        crate::sys::status(self).await
47    }
48}
49
50/// A client which can be used to execute calls against a Vault server.
51///
52/// A vault client is configured using [VaultClientSettings] and will
53/// automatically configure a backing instance of a [HTTPClient] which is
54/// used for executing [Endpoints][rustify::endpoint::Endpoint].
55pub struct VaultClient {
56    pub http: HTTPClient,
57    pub middle: EndpointMiddleware,
58    pub settings: VaultClientSettings,
59}
60
61#[async_trait]
62impl Client for VaultClient {
63    fn http(&self) -> &HTTPClient {
64        &self.http
65    }
66
67    fn middle(&self) -> &EndpointMiddleware {
68        &self.middle
69    }
70
71    fn settings(&self) -> &VaultClientSettings {
72        &self.settings
73    }
74
75    fn set_token(&mut self, token: &str) {
76        self.settings.token = token.to_string();
77        self.middle.token = token.to_string();
78    }
79}
80
81impl VaultClient {
82    /// Creates a new [VaultClient] using the given [VaultClientSettings].
83    #[instrument(skip(settings), err)]
84    pub fn new(settings: VaultClientSettings) -> Result<VaultClient, ClientError> {
85        #[cfg(not(feature = "rustls"))]
86        let mut http_client = reqwest::ClientBuilder::new();
87
88        #[cfg(feature = "rustls")]
89        let mut http_client = reqwest::ClientBuilder::new().use_rustls_tls();
90
91        // Optionally set timeout on client
92        http_client = if let Some(timeout) = settings.timeout {
93            http_client.timeout(timeout)
94        } else {
95            http_client
96        };
97
98        // Disable TLS checks if specified
99        if !settings.verify {
100            event!(tracing::Level::WARN, "Disabling TLS verification");
101        }
102        http_client = http_client.danger_accept_invalid_certs(!settings.verify);
103
104        // Adds CA certificates
105        for path in &settings.ca_certs {
106            let content = std::fs::read(path).map_err(|e| ClientError::FileReadError {
107                source: e,
108                path: path.clone(),
109            })?;
110            let cert = reqwest::Certificate::from_pem(&content).map_err(|e| {
111                ClientError::ParseCertificateError {
112                    source: e,
113                    path: path.clone(),
114                }
115            })?;
116
117            debug!("Importing CA certificate from {}", path);
118            http_client = http_client.add_root_certificate(cert);
119        }
120
121        // Adds client certificates
122        if let Some(identity) = &settings.identity {
123            http_client = http_client.identity(identity.clone());
124        }
125
126        // Configures middleware for endpoints to append API version and token
127        debug!("Using API version {}", settings.version);
128        let version_str = format!("v{}", settings.version);
129        let middle = EndpointMiddleware {
130            token: settings.token.clone(),
131            version: version_str,
132            wrap: None,
133            namespace: settings.namespace.clone(),
134        };
135
136        let http_client = http_client
137            .build()
138            .map_err(|e| ClientError::RestClientBuildError { source: e })?;
139        let http = HTTPClient::new(settings.address.as_str(), http_client);
140        Ok(VaultClient {
141            settings,
142            middle,
143            http,
144        })
145    }
146}
147
148/// Contains settings for configuring a [VaultClient].
149///
150/// Most settings that are not directly configured will have their default value
151/// pulled from their respective environment variables. Specifically:
152///
153/// * `address`: VAULT_ADDR
154/// * `ca_certs: VAULT_CACERT / VAULT_CAPATH
155/// * `token`: VAULT_TOKEN
156/// * verify`: VAULT_SKIP_VERIFY
157///
158/// The `address` is validated when the settings are built and will throw an
159/// error if the format is invalid.
160#[derive(Builder, Clone, Debug)]
161#[builder(build_fn(validate = "Self::validate"))]
162pub struct VaultClientSettings {
163    #[builder(setter(custom), default = "self.default_address()?")]
164    pub address: Url,
165    #[builder(default = "self.default_ca_certs()")]
166    pub ca_certs: Vec<String>,
167    #[builder(default = "self.default_identity()")]
168    pub identity: Option<Identity>,
169    #[builder(default)]
170    pub timeout: Option<Duration>,
171    #[builder(setter(into), default = "self.default_token()")]
172    pub token: String,
173    #[builder(default = "self.default_verify()")]
174    pub verify: bool,
175    #[builder(setter(into, strip_option), default = "1")]
176    pub version: u8,
177    #[builder(default = "false")]
178    pub wrapping: bool,
179    #[builder(default)]
180    pub namespace: Option<String>,
181}
182
183impl VaultClientSettingsBuilder {
184    /// Set an address for vault. Note that if not set, it will default
185    /// to the `VAULT_ADDR` environment variable and if that is not set either,
186    /// it will default to `http://127.0.0.1:8200`.
187    ///
188    /// # Panics
189    ///
190    /// The setter will panic if the address given contains an invalid URL format.
191    pub fn address<T>(&mut self, address: T) -> &mut Self
192    where
193        T: AsRef<str>,
194    {
195        let url = Url::parse(address.as_ref())
196            .map_err(|_| format!("Invalid URL format: {}", address.as_ref()))
197            .unwrap();
198        self.address = Some(url);
199        self
200    }
201
202    pub fn set_namespace(&mut self, str: String) -> &mut Self {
203        self.namespace = Some(Some(str));
204        self
205    }
206
207    fn default_address(&self) -> Result<Url, String> {
208        let address = if let Ok(address) = env::var("VAULT_ADDR") {
209            debug!("Using vault address from $VAULT_ADDR: {address}");
210            address
211        } else {
212            debug!("Using default vault address http://127.0.0.1:8200");
213            String::from("http://127.0.0.1:8200")
214        };
215        let url = Url::parse(&address);
216        let url = url.map_err(|_| format!("Invalid URL format: {}", &address))?;
217        // validation in derive_builder does not happen for defaults,
218        // so we need to do it ourselves, here:
219        self.validate_url(&url)?;
220        Ok(url)
221    }
222
223    fn default_token(&self) -> String {
224        match env::var("VAULT_TOKEN") {
225            Ok(s) => {
226                debug!("Using vault token from $VAULT_TOKEN");
227                s
228            }
229            Err(_) => {
230                debug!("Using default empty vault token");
231                String::from("")
232            }
233        }
234    }
235
236    fn default_verify(&self) -> bool {
237        debug!("Checking TLS verification using $VAULT_SKIP_VERIFY");
238        match env::var("VAULT_SKIP_VERIFY") {
239            Ok(value) => !matches!(value.to_lowercase().as_str(), "0" | "f" | "false"),
240            Err(_) => true,
241        }
242    }
243
244    fn default_ca_certs(&self) -> Vec<String> {
245        let mut paths: Vec<String> = Vec::new();
246
247        if let Ok(s) = env::var("VAULT_CACERT") {
248            debug!("Found CA certificate in $VAULT_CACERT");
249            paths.push(s);
250        }
251
252        if let Ok(s) = env::var("VAULT_CAPATH") {
253            debug!("Found CA certificate path in $VAULT_CAPATH");
254            if let Ok(p) = fs::read_dir(s) {
255                for path in p {
256                    paths.push(path.unwrap().path().to_str().unwrap().to_string())
257                }
258            }
259        }
260
261        paths
262    }
263
264    fn default_identity(&self) -> Option<reqwest::Identity> {
265        // Default value can be set from environment
266        let env_client_cert = env::var("VAULT_CLIENT_CERT").unwrap_or_default();
267        let env_client_key = env::var("VAULT_CLIENT_KEY").unwrap_or_default();
268
269        if env_client_cert.is_empty() || env_client_key.is_empty() {
270            debug!("No client certificate (env VAULT_CLIENT_CERT & VAULT_CLIENT_KEY are not set)");
271            return None;
272        }
273
274        #[cfg(feature = "rustls")]
275        {
276            let mut client_cert = match fs::read(&env_client_cert) {
277                Ok(content) => content,
278                Err(err) => {
279                    error!("error reading client cert '{}': {}", env_client_cert, err);
280                    return None;
281                }
282            };
283
284            let mut client_key = match fs::read(&env_client_key) {
285                Ok(content) => content,
286                Err(err) => {
287                    error!("error reading client key '{}': {}", env_client_key, err);
288                    return None;
289                }
290            };
291
292            // concat certificate and key
293            client_cert.append(&mut client_key);
294
295            match reqwest::Identity::from_pem(&client_cert) {
296                Ok(pkcs8) => return Some(pkcs8),
297                Err(err) => error!("error creating identity: {}", err),
298            };
299        }
300
301        #[cfg(feature = "native-tls")]
302        {
303            error!("Client certificates not implemented for native-tls");
304        }
305
306        None
307    }
308
309    fn validate(&self) -> Result<(), String> {
310        // Verify URL is valid
311        if let Some(url) = &self.address {
312            self.validate_url(url)
313        } else {
314            Ok(())
315        }
316    }
317
318    fn validate_url(&self, url: &Url) -> Result<(), String> {
319        // Verify scheme is valid HTTP endpoint
320        if !VALID_SCHEMES.contains(&url.scheme()) {
321            Err(format!("Invalid scheme for HTTP URL: {}", url.scheme()))
322        } else {
323            Ok(())
324        }
325    }
326}