1#![forbid(unsafe_code)]
2
3mod concurrency_util;
4mod models;
5mod serde_util;
6
7use async_trait::async_trait;
8
9use secrecy::ExposeSecret;
10use secrecy::Secret;
11use xand_secrets::{CheckHealthError, ReadSecretError, SecretKeyValueStore};
12
13use reqwest::{Certificate, Client as HttpClient, Response as HttpResponse, StatusCode};
14
15use serde::{Deserialize, Serialize};
16use std::fs::File;
17use std::io::Read;
18
19use concurrency_util::concurrent_and;
20use thiserror::Error;
21
22const TOKEN_HEADER: &str = "X-Vault-Token";
23
24#[derive(Debug, Error)]
25pub enum VaultSecretStoreCreationError {
26 #[error("creating the internal client failed. {internal_error}")]
27 ClientCreation {
28 #[source]
29 internal_error: reqwest::Error,
30 },
31 #[error("could not parse the given certificate file. {internal_error}")]
32 CertificateFormat {
33 #[source]
34 internal_error: reqwest::Error,
35 },
36 #[error("failed to open the provided certificate pem file. {internal_error}")]
37 CertificateFileLoad {
38 #[source]
39 internal_error: std::io::Error,
40 },
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(rename_all = "kebab-case")]
45#[serde(deny_unknown_fields)]
46pub struct VaultConfiguration {
47 pub http_endpoint: String,
48 #[serde(serialize_with = "serde_util::serialize_secret")]
49 pub token: Secret<String>,
50 pub additional_https_root_certificate_files: Option<Vec<String>>,
51}
52
53#[derive(Debug)]
54pub struct VaultSecretKeyValueStore {
55 config: VaultConfiguration,
56 client: reqwest::Client,
57}
58
59#[derive(Debug)]
60pub struct VaultKey {
61 pub path: String,
62 pub key: String,
63}
64
65impl VaultKey {
66 fn parse_from_slash_separated_key(key: &str) -> Result<Self, ReadSecretError> {
67 key.rfind('/')
68 .map(|index| VaultKey {
69 path: String::from(&key[..index]),
70 key: String::from(&key[index + 1..]),
71 })
72 .ok_or(ReadSecretError::KeyNotFound {
73 key: key.to_string(),
74 })
75 }
76}
77
78impl VaultSecretKeyValueStore {
79 pub fn create_from_config(
80 config: VaultConfiguration,
81 ) -> Result<VaultSecretKeyValueStore, VaultSecretStoreCreationError> {
82 let client = Self::build_client(&config)?;
83 Ok(VaultSecretKeyValueStore { config, client })
84 }
85
86 fn build_client(
87 config: &VaultConfiguration,
88 ) -> Result<HttpClient, VaultSecretStoreCreationError> {
89 let mut builder = HttpClient::builder();
90
91 for cert_path in config
92 .additional_https_root_certificate_files
93 .iter()
94 .flatten()
95 {
96 builder = builder.add_root_certificate(Self::load_certificate_pem(cert_path.as_str())?);
97 }
98
99 builder.build().map_err(
100 |internal_error| VaultSecretStoreCreationError::ClientCreation { internal_error },
101 )
102 }
103
104 fn load_certificate_pem(cert_path: &str) -> Result<Certificate, VaultSecretStoreCreationError> {
105 let mut buf = Vec::new();
106 File::open(cert_path)
107 .and_then(|mut f| f.read_to_end(&mut buf))
108 .map_err(
109 |internal_error| VaultSecretStoreCreationError::CertificateFileLoad {
110 internal_error,
111 },
112 )?;
113
114 reqwest::Certificate::from_pem(&buf).map_err(|internal_error| {
115 VaultSecretStoreCreationError::CertificateFormat { internal_error }
116 })
117 }
118
119 fn remove_trailing_slash(path: &str) -> &str {
120 let len = path.len();
121 if path.ends_with('/') {
122 &path[..len - 1]
123 } else {
124 path
125 }
126 }
127
128 fn get_url_for_path(&self, path: &str) -> String {
129 format!(
130 "{}/v1/{}",
131 Self::remove_trailing_slash(&self.config.http_endpoint[..]),
132 path
133 )
134 }
135
136 fn map_reqwest_error_to_read_error(error: reqwest::Error) -> ReadSecretError {
137 ReadSecretError::Request {
138 internal_error: Box::new(error),
139 }
140 }
141
142 async fn parse_read_response(
143 response: HttpResponse,
144 ) -> Result<models::KeyValueReadResponse, ReadSecretError> {
145 response
146 .json::<models::KeyValueReadResponse>()
147 .await
148 .map_err(Self::map_reqwest_error_to_read_error)
149 }
150
151 async fn get_error_list(response: HttpResponse) -> Option<Vec<String>> {
152 response
153 .json::<models::ErrorResponse>()
154 .await
155 .map(|error_response| error_response.errors)
156 .ok()
157 }
158
159 async fn map_status_code_to_read_error(
160 response: HttpResponse,
161 full_key: &str,
162 ) -> ReadSecretError {
163 let status_code = response.status();
164
165 if let StatusCode::NOT_FOUND = status_code {
166 return ReadSecretError::KeyNotFound {
167 key: full_key.to_owned(),
168 };
169 }
170
171 let internal_error = Box::new(VaultHttpError::new(
172 response.url().path().to_owned(),
173 status_code,
174 Self::get_error_list(response).await,
175 ));
176 match status_code {
177 StatusCode::FORBIDDEN => ReadSecretError::Authentication { internal_error },
178 _ => ReadSecretError::Request { internal_error },
179 }
180 }
181
182 async fn probe_health_at_path(&self, path: &str) -> Result<HttpResponse, reqwest::Error> {
183 let http_url = self.get_url_for_path(path);
184
185 let health_response = self
186 .client
187 .get(&http_url[..])
189 .header(TOKEN_HEADER, self.config.token.expose_secret())
190 .send()
191 .await?;
192
193 Ok(health_response)
194 }
195
196 async fn map_status_code_to_check_health_result(
197 response: HttpResponse,
198 ) -> Result<(), CheckHealthError> {
199 match response.status() {
200 StatusCode::OK => Ok(()),
201 StatusCode::FORBIDDEN => Err(CheckHealthError::Authentication {
202 internal_error: Box::new(VaultHttpError::new(
203 String::from(response.url().path()),
204 response.status(),
205 None,
206 )),
207 }),
208 _ => Err(CheckHealthError::RemoteInternal {
209 internal_error: Box::new(VaultHttpError::new(
210 String::from(response.url().path()),
211 response.status(),
212 None,
213 )),
214 }),
215 }
216 }
217
218 async fn check_vault_system_health(&self) -> Result<(), CheckHealthError> {
219 let health_path = "sys/health";
226 match self.probe_health_at_path(health_path).await {
227 Ok(response) if response.status().as_u16() == 429 => Ok(()),
230 Ok(response) if response.status().as_u16() == 473 => Ok(()),
235 Ok(response) => Self::map_status_code_to_check_health_result(response).await,
236 Err(e) => Err(CheckHealthError::Unreachable {
237 internal_error: Box::new(e),
238 }),
239 }
240 }
241
242 async fn check_vault_credential_health(&self) -> Result<(), CheckHealthError> {
243 let auth_path = "sys/auth";
247 match self.probe_health_at_path(auth_path).await {
248 Ok(response) => Self::map_status_code_to_check_health_result(response).await,
249 Err(e) => Err(CheckHealthError::Unreachable {
250 internal_error: Box::new(e),
251 }),
252 }
253 }
254}
255
256#[async_trait]
257impl SecretKeyValueStore for VaultSecretKeyValueStore {
258 async fn read(&self, key: &str) -> Result<Secret<String>, ReadSecretError> {
259 let VaultKey {
260 path: split_path,
261 key: split_key,
262 } = VaultKey::parse_from_slash_separated_key(key)?;
263
264 let http_url = self.get_url_for_path(split_path.as_str());
265
266 let response = self
267 .client
268 .get(&http_url[..])
269 .header(TOKEN_HEADER, self.config.token.expose_secret())
270 .send()
271 .await
272 .map_err(Self::map_reqwest_error_to_read_error)?;
273
274 if !response.status().is_success() {
275 return Err(Self::map_status_code_to_read_error(response, key).await);
276 }
277
278 let response_body = VaultSecretKeyValueStore::parse_read_response(response).await?;
279
280 let secret =
281 response_body
282 .data
283 .get(split_key.as_str())
284 .ok_or(ReadSecretError::KeyNotFound {
285 key: key.to_owned(),
286 })?;
287
288 Ok(secret.to_owned())
289 }
290
291 async fn check_health(&self) -> Result<(), CheckHealthError> {
292 concurrent_and(
293 self.check_vault_system_health(),
294 self.check_vault_credential_health(),
295 )
296 .await
297 }
298}
299
300#[derive(Debug)]
301struct VaultHttpError {
302 pub path: String,
303 pub status_code: StatusCode,
304 pub errors: Option<Vec<String>>,
305}
306
307impl VaultHttpError {
308 pub fn new(
309 path: String,
310 status_code: StatusCode,
311 errors: Option<Vec<String>>,
312 ) -> VaultHttpError {
313 VaultHttpError {
314 path,
315 status_code,
316 errors,
317 }
318 }
319}
320
321impl std::error::Error for VaultHttpError {}
322impl std::fmt::Display for VaultHttpError {
323 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
324 write!(
325 f,
326 "An error code was returned while making an HTTP request to path \"{}\". Status code: {}.{}",
327 self.path,
328 self.status_code.as_str(),
329 self.errors.as_ref().map_or(String::default(), |e| format!(" Vault returned errors: {:?}", e))
330 )
331 }
332}