fluentci_secrets/
google.rs1use 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}