fluentci_secrets/
google.rs

1use async_trait::async_trait;
2use base64::{engine::general_purpose::STANDARD as base64, Engine as _};
3use google_secretmanager1::{
4    hyper, hyper::client::HttpConnector, hyper_rustls, hyper_rustls::HttpsConnector, oauth2,
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::path::PathBuf;
9use thiserror::Error;
10
11use super::{convert::decode_env_from_json, Vault, VaultConfig};
12
13type SecretManager = google_secretmanager1::SecretManager<HttpsConnector<HttpConnector>>;
14
15#[derive(Debug, Default, Serialize, Deserialize)]
16pub struct GoogleConfig {
17    pub google_credentials_file: Option<PathBuf>,
18    pub google_credentials_json: Option<String>,
19    pub google_project: Option<String>,
20}
21
22#[derive(Error, Debug)]
23pub enum GoogleError {
24    #[error("Google SA configuration is invalid")]
25    ConfigurationError(#[source] std::io::Error),
26    #[error("secret manager operation failed")]
27    SecretManagerError(#[source] google_secretmanager1::Error),
28    #[error("the secret is empty")]
29    EmptySecret,
30    #[error("there are no secrets in the project")]
31    NoSecrets,
32    #[error("secret encoding is invalid")]
33    WrongEncoding(#[source] anyhow::Error),
34    #[error("cannot decode secret - it is not a valid JSON")]
35    DecodeError(#[source] serde_json::Error),
36}
37
38pub type Result<T, E = GoogleError> = std::result::Result<T, E>;
39
40impl VaultConfig for GoogleConfig {
41    type Vault = GoogleConfig;
42
43    fn into_vault(self) -> anyhow::Result<Self::Vault> {
44        Ok(self)
45    }
46}
47
48impl GoogleConfig {
49    async fn to_manager(&self) -> Result<SecretManager> {
50        let auth = self
51            .to_authenticator()
52            .await
53            .map_err(GoogleError::ConfigurationError)?;
54        let manager = SecretManager::new(
55            hyper::Client::builder().build(
56                hyper_rustls::HttpsConnectorBuilder::new()
57                    .with_native_roots()
58                    .https_or_http()
59                    .enable_http1()
60                    .enable_http2()
61                    .build(),
62            ),
63            auth,
64        );
65        Ok(manager)
66    }
67
68    async fn to_authenticator(
69        &self,
70    ) -> std::io::Result<oauth2::authenticator::Authenticator<HttpsConnector<HttpConnector>>> {
71        if let Some(path) = &self.google_credentials_file {
72            let key = oauth2::read_service_account_key(path).await?;
73            let auth = oauth2::ServiceAccountAuthenticator::builder(key)
74                .build()
75                .await?;
76            Ok(auth)
77        } else if let Some(json) = &self.google_credentials_json {
78            let key = oauth2::parse_service_account_key(json)?;
79            let auth = oauth2::ServiceAccountAuthenticator::builder(key)
80                .build()
81                .await?;
82            Ok(auth)
83        } else {
84            let opts = oauth2::ApplicationDefaultCredentialsFlowOpts::default();
85            let auth = match oauth2::ApplicationDefaultCredentialsAuthenticator::builder(opts).await
86            {
87                oauth2::authenticator::ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => {
88                    auth.build().await?
89                }
90                oauth2::authenticator::ApplicationDefaultCredentialsTypes::InstanceMetadata(
91                    auth,
92                ) => auth.build().await?,
93            };
94            Ok(auth)
95        }
96    }
97}
98
99#[async_trait]
100impl Vault for GoogleConfig {
101    async fn download_prefixed(&self, prefix: &str) -> anyhow::Result<Vec<(String, String)>> {
102        let mut manager = self.to_manager().await?;
103        let project = self.google_project.as_ref().unwrap();
104        let response = manager
105            .projects()
106            .secrets_list(&format!("projects/{project}"))
107            .page_size(250)
108            .doit()
109            .await
110            .map_err(GoogleError::SecretManagerError)?;
111        let secrets: Vec<_> = response
112            .1
113            .secrets
114            .ok_or(GoogleError::NoSecrets)?
115            .into_iter()
116            .filter(|f| f.name.is_some())
117            .filter(|f| self.secret_matches(prefix, f.name.as_ref().unwrap()))
118            .collect();
119        let mut from_kv = Vec::with_capacity(secrets.len());
120        for secret in secrets {
121            let value = self
122                .get_secret_full_name(&mut manager, secret.name.as_ref().unwrap())
123                .await?;
124            let name = self
125                .strip_prefix(prefix, secret.name.as_ref().unwrap())
126                .to_string();
127            from_kv.push((name, value));
128        }
129        Ok(from_kv)
130    }
131
132    async fn download_json(&self, secret_name: &str) -> anyhow::Result<Vec<(String, String)>> {
133        let mut manager = self.to_manager().await?;
134        let secret = self.get_secret(&mut manager, secret_name).await?;
135        let value: Value = serde_json::from_str(&secret).map_err(GoogleError::DecodeError)?;
136        decode_env_from_json(secret_name, value)
137    }
138}
139
140impl GoogleConfig {
141    fn strip_project<'a>(&'_ self, name: &'a str) -> &'a str {
142        let idx = name.rfind('/').unwrap();
143        &name[(idx + 1)..]
144    }
145
146    fn secret_matches(&self, prefix: &str, name: &str) -> bool {
147        self.strip_project(name).starts_with(prefix)
148    }
149
150    fn strip_prefix<'a>(&'_ self, prefix: &'_ str, name: &'a str) -> &'a str {
151        &self.strip_project(name)[prefix.len()..]
152    }
153
154    async fn get_secret(&self, client: &mut SecretManager, secret_name: &str) -> Result<String> {
155        self.get_secret_full_name(
156            client,
157            &format!(
158                "projects/{}/secrets/{}",
159                self.google_project.as_ref().unwrap(),
160                secret_name
161            ),
162        )
163        .await
164    }
165
166    async fn get_secret_full_name(
167        &self,
168        manager: &mut SecretManager,
169        name: &str,
170    ) -> Result<String> {
171        let data = manager
172            .projects()
173            .secrets_versions_access(&format!("{name}/versions/latest"))
174            .doit()
175            .await
176            .map_err(GoogleError::SecretManagerError)?
177            .1
178            .payload
179            .ok_or(GoogleError::EmptySecret)?
180            .data
181            .ok_or(GoogleError::EmptySecret)?;
182        let raw_bytes = base64
183            .decode(data)
184            .map_err(|e| GoogleError::WrongEncoding(anyhow::anyhow!(e)))?;
185        String::from_utf8(raw_bytes).map_err(|e| GoogleError::WrongEncoding(anyhow::anyhow!(e)))
186    }
187}