fluentci_secrets/
vault.rs

1use 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}