fluentci_secrets/
vault.rs1use std::{collections::HashMap, path::PathBuf};
2
3use async_trait::async_trait;
4use futures::future::try_join_all;
5use reqwest::{self, StatusCode};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use tokio::io::AsyncReadExt;
9
10use super::{convert::as_valid_env_name, Vault, VaultConfig};
11
12#[derive(Serialize, Deserialize)]
13pub struct HashicorpVaultConfig {
14 pub vault_address: Option<String>,
15 pub vault_token: Option<String>,
16 pub vault_cacert: Option<PathBuf>,
17}
18
19#[derive(Error, Debug)]
20pub enum HashicorpVaultError {
21 #[error("secret '{0}' does not exist")]
22 SecretNotFound(String),
23
24 #[error("the vault token is invalid")]
25 UnauthorizedError,
26
27 #[error("the token does not have access to secret '{0}'")]
28 ForbiddenError(String),
29
30 #[error("HTTP error occurred")]
31 HttpError(#[source] reqwest::Error),
32
33 #[error("the Vault returned non-200 error code")]
34 HttpStatusCodeError(StatusCode),
35
36 #[error("cannot deserialize the response")]
37 DeserializeError(#[source] reqwest::Error),
38
39 #[error("the keys in the secret are not valid env names")]
40 InvalidEnv(#[source] anyhow::Error),
41
42 #[error("the configuration is invalid")]
43 ConfigurationError(#[from] anyhow::Error),
44}
45
46pub struct HashicorpVault {
47 address: String,
48 token: String,
49 cacert: Option<PathBuf>,
50}
51
52impl VaultConfig for HashicorpVaultConfig {
53 type Vault = HashicorpVault;
54
55 fn into_vault(self) -> anyhow::Result<Self::Vault> {
56 Ok(Self::Vault {
57 address: self.vault_address.unwrap(),
58 token: self.vault_token.unwrap(),
59 cacert: self.vault_cacert,
60 })
61 }
62}
63
64impl HashicorpVault {
65 async fn client(&self) -> Result<reqwest::Client, HashicorpVaultError> {
66 let mut builder = reqwest::Client::builder().user_agent("fluentci-engine");
67
68 if let Some(path) = self.cacert.as_ref() {
69 let mut buffer = Vec::new();
70 {
71 let mut file = tokio::fs::File::open(path)
72 .await
73 .map_err(anyhow::Error::new)?;
74 file.read_to_end(&mut buffer)
75 .await
76 .map_err(anyhow::Error::new)?;
77 }
78 let cert = reqwest::Certificate::from_pem(&buffer).map_err(anyhow::Error::new)?;
79 builder = builder.add_root_certificate(cert);
80 }
81
82 builder
83 .build()
84 .map_err(anyhow::Error::new)
85 .map_err(HashicorpVaultError::ConfigurationError)
86 }
87
88 fn parse_secrets(secret: SecretResponse) -> Result<Vec<(String, String)>, HashicorpVaultError> {
89 secret
90 .data
91 .data
92 .into_iter()
93 .map(|(k, v)| as_valid_env_name(k).map(|k| (k, v)))
94 .collect::<anyhow::Result<Vec<_>>>()
95 .map_err(HashicorpVaultError::InvalidEnv)
96 }
97
98 async fn get_single_key(
99 &self,
100 client: &reqwest::Client,
101 secret_name: impl AsRef<str>,
102 ) -> Result<Vec<(String, String)>, HashicorpVaultError> {
103 let response = client
104 .get(format!(
105 "{}/v1/secret/data/{}",
106 self.address,
107 secret_name.as_ref()
108 ))
109 .header("X-Vault-Token", &self.token)
110 .send()
111 .await
112 .map_err(HashicorpVaultError::HttpError)?;
113 handle_common_errors(secret_name.as_ref(), &response)?;
114
115 let data: SecretResponse = response
116 .json()
117 .await
118 .map_err(HashicorpVaultError::DeserializeError)?;
119 Self::parse_secrets(data)
120 }
121}
122
123#[async_trait]
124impl Vault for HashicorpVault {
125 async fn download_prefixed(&self, prefix: &str) -> anyhow::Result<Vec<(String, String)>> {
126 let client = self.client().await?;
127
128 let response = client
129 .get(format!("{}/v1/secret/metadata?list=true", self.address))
130 .header("X-Vault-Token", &self.token)
131 .send()
132 .await
133 .map_err(HashicorpVaultError::HttpError)?;
134 handle_common_errors(prefix, &response)?;
135
136 let list: ListResponse = response
137 .json()
138 .await
139 .map_err(HashicorpVaultError::DeserializeError)?;
140
141 let env_values = list
142 .data
143 .keys
144 .into_iter()
145 .filter(|p| p.starts_with(prefix))
146 .map(|s| self.get_single_key(&client, s));
147 let env_values: Vec<_> = try_join_all(env_values)
148 .await?
149 .into_iter()
150 .flatten()
151 .collect();
152 Ok(env_values)
153 }
154
155 async fn download_json(&self, secret_name: &str) -> anyhow::Result<Vec<(String, String)>> {
156 let client = self.client().await?;
157 let result = self.get_single_key(&client, secret_name).await?;
158 Ok(result)
159 }
160}
161
162fn handle_common_errors(
163 secret_name: &str,
164 response: &reqwest::Response,
165) -> Result<(), HashicorpVaultError> {
166 match response.status() {
167 StatusCode::NOT_FOUND => Err(HashicorpVaultError::SecretNotFound(secret_name.to_string())),
168 StatusCode::UNAUTHORIZED => Err(HashicorpVaultError::UnauthorizedError),
169 StatusCode::FORBIDDEN => Err(HashicorpVaultError::ForbiddenError(secret_name.to_string())),
170 StatusCode::OK => Ok(()),
171 other => Err(HashicorpVaultError::HttpStatusCodeError(other)),
172 }
173}
174
175#[derive(Deserialize, Debug)]
176struct SecretResponse {
177 pub data: Secret,
178}
179
180#[derive(Deserialize, Debug)]
181struct Secret {
182 pub data: HashMap<String, String>,
183}
184
185#[derive(Deserialize, Debug)]
186struct ListResponse {
187 pub data: KeyList,
188}
189
190#[derive(Deserialize, Debug)]
191struct KeyList {
192 pub keys: Vec<String>,
193}