feat: GCP Secret Manager support

This commit is contained in:
2025-09-12 18:36:16 -06:00
parent ac45287336
commit bcbd755a37
6 changed files with 768 additions and 44 deletions
+5 -5
View File
@@ -163,7 +163,7 @@ fn generate_files_secret_injections(
secrets: HashMap<&str, String>,
run_config: &RunConfig,
) -> Result<Vec<(PathBuf, String, String)>> {
let re = Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}")?;
let re = Regex::new(r"\{\{(.+)\}\}")?;
let mut results = Vec::new();
for file in run_config
.files
@@ -283,14 +283,14 @@ mod tests {
#[test]
fn test_generate_files_secret_injections() {
let mut secrets = HashMap::new();
secrets.insert("SECRET1", "value1".to_string());
secrets.insert("testing/SOME-secret", "value1".to_string());
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "{{SECRET1}}").unwrap();
fs::write(&file_path, "{{testing/SOME-secret}}").unwrap();
let run_config = RunConfig {
name: Some("test".to_string()),
secrets: Some(vec!["SECRET1".to_string()]),
secrets: Some(vec!["testing/SOME-secret".to_string()]),
files: Some(vec![file_path.clone()]),
flag: None,
flag_position: None,
@@ -301,7 +301,7 @@ mod tests {
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, file_path);
assert_str_eq!(result[0].1, "{{SECRET1}}");
assert_str_eq!(result[0].1, "{{testing/SOME-secret}}");
assert_str_eq!(result[0].2, "value1");
}
+4
View File
@@ -150,6 +150,10 @@ impl ProviderConfig {
debug!("Using AWS Secrets Manager provider");
provider_def
}
SupportedProvider::GcpSecretManager { provider_def } => {
debug!("Using GCP Secret Manager provider");
provider_def
}
}
}
}
+201
View File
@@ -0,0 +1,201 @@
use crate::providers::SecretProvider;
use anyhow::{Context, Result, anyhow};
use gcloud_sdk::google::cloud::secretmanager::v1;
use gcloud_sdk::google::cloud::secretmanager::v1::replication::Automatic;
use gcloud_sdk::google::cloud::secretmanager::v1::secret_manager_service_client::SecretManagerServiceClient;
use gcloud_sdk::google::cloud::secretmanager::v1::{
AccessSecretVersionRequest, AddSecretVersionRequest, CreateSecretRequest, ListSecretsRequest,
Replication, Secret, replication,
};
use gcloud_sdk::proto_ext::secretmanager::SecretPayload;
use gcloud_sdk::tonic::Code;
use gcloud_sdk::{GoogleApi, GoogleAuthMiddleware};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use v1::DeleteSecretRequest;
use validator::Validate;
type SecretsManagerClient = GoogleApi<SecretManagerServiceClient<GoogleAuthMiddleware>>;
#[skip_serializing_none]
/// Configuration for GCP Secret Manager provider
/// See [GCP Secret Manager](https://cloud.google.com/secret-manager)
/// for more information.
///
/// This provider stores secrets in GCP Secret Manager. It requires
/// a GCP project ID to be specified.
///
/// Example
/// ```no_run
/// use gman::providers::{SecretProvider, SupportedProvider};
/// use gman::config::{Config, ProviderConfig};
/// use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
///
/// let provider = GcpSecretManagerProvider {
/// gcp_project_id: Some("my-gcp-project".to_string()),
/// };
/// let _ = provider.set_secret("MY_SECRET", "value");
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct GcpSecretManagerProvider {
#[validate(required)]
pub gcp_project_id: Option<String>,
}
#[async_trait::async_trait]
impl SecretProvider for GcpSecretManagerProvider {
fn name(&self) -> &'static str {
"GcpSecretManagerProvider"
}
async fn get_secret(&self, key: &str) -> Result<String> {
let secret_value = self
.get_client()
.await?
.get()
.access_secret_version(AccessSecretVersionRequest {
name: format!(
"projects/{}/secrets/{}/versions/latest",
self.gcp_project_id.as_ref().unwrap(),
key
),
})
.await?
.into_inner()
.payload
.ok_or_else(|| anyhow!("Secret '{}' not found", key))?
.data
.ref_sensitive_value()
.to_vec();
let secret_string = String::from_utf8(secret_value)
.with_context(|| format!("Invalid UTF-8 in secret '{})'", key))?;
Ok(secret_string)
}
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
let parent = format!("projects/{}", self.gcp_project_id.as_ref().unwrap());
let secret_name = format!("{}/secrets/{}", parent, key);
let secret = Secret {
replication: Some(Replication {
replication: Some(replication::Replication::Automatic(Automatic {
customer_managed_encryption: None,
})),
}),
..Default::default()
};
let client = self.get_client().await?;
client
.get()
.create_secret(CreateSecretRequest {
parent: parent.clone(),
secret_id: key.to_string(),
secret: Some(secret),
})
.await
.map_err(|e| {
if e.code() == Code::AlreadyExists {
anyhow!("Secret already exists")
} else {
e.into()
}
})?;
let bytes = value.as_ref();
let crc32c = crc32c::crc32c(bytes) as i64;
client
.get()
.add_secret_version(AddSecretVersionRequest {
parent: secret_name,
payload: Some(SecretPayload {
data: bytes.to_vec().into(),
data_crc32c: Some(crc32c),
}),
})
.await?;
Ok(())
}
async fn delete_secret(&self, key: &str) -> Result<()> {
let name = format!(
"projects/{}/secrets/{}",
self.gcp_project_id.as_ref().unwrap(),
key
);
self.get_client()
.await?
.get()
.delete_secret(DeleteSecretRequest {
name,
etag: "".to_string(),
})
.await?;
Ok(())
}
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
let parent = format!(
"projects/{}/secrets/{}",
self.gcp_project_id.as_ref().unwrap(),
key
);
let bytes = value.as_ref();
let crc32c = crc32c::crc32c(bytes) as i64;
self.get_client()
.await?
.get()
.add_secret_version(AddSecretVersionRequest {
parent,
payload: Some(SecretPayload {
data: bytes.to_vec().into(),
data_crc32c: Some(crc32c),
}),
})
.await?;
Ok(())
}
async fn list_secrets(&self) -> Result<Vec<String>> {
let request = ListSecretsRequest {
parent: format!("projects/{}", self.gcp_project_id.as_ref().unwrap()),
..Default::default()
};
let secrets = self
.get_client()
.await?
.get()
.list_secrets(request)
.await?
.into_inner()
.secrets
.iter()
.map(|s| {
let full_secret_name = &s.name;
if let Some(secret_name) = full_secret_name.split("/secrets/").nth(1) {
secret_name.to_string()
} else {
full_secret_name.to_string()
}
})
.collect();
Ok(secrets)
}
}
impl GcpSecretManagerProvider {
async fn get_client(&self) -> Result<SecretsManagerClient> {
let client = GoogleApi::from_function(
SecretManagerServiceClient::new,
"https://secretmanager.googleapis.com",
None,
)
.await?;
Ok(client)
}
}
+10 -2
View File
@@ -3,6 +3,7 @@
//! Implementations provide storage/backends for secrets and a common
//! interface used by the CLI.
pub mod aws_secrets_manager;
pub mod gcp_secret_manager;
mod git_sync;
pub mod local;
@@ -11,6 +12,8 @@ use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use validator::{Validate, ValidationErrors};
use aws_secrets_manager::AwsSecretsManagerProvider;
use gcp_secret_manager::GcpSecretManagerProvider;
/// A secret storage backend capable of CRUD, with optional
/// update, listing, and sync support.
@@ -43,7 +46,6 @@ pub trait SecretProvider: Send + Sync {
/// Registry of built-in providers.
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]
//TODO test that this works with the AWS config
pub enum SupportedProvider {
Local {
#[serde(flatten)]
@@ -51,7 +53,11 @@ pub enum SupportedProvider {
},
AwsSecretsManager {
#[serde(flatten)]
provider_def: aws_secrets_manager::AwsSecretsManagerProvider,
provider_def: AwsSecretsManagerProvider,
},
GcpSecretManager {
#[serde(flatten)]
provider_def: GcpSecretManagerProvider,
},
}
@@ -60,6 +66,7 @@ impl Validate for SupportedProvider {
match self {
SupportedProvider::Local { provider_def } => provider_def.validate(),
SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(),
SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(),
}
}
}
@@ -77,6 +84,7 @@ impl Display for SupportedProvider {
match self {
SupportedProvider::Local { .. } => write!(f, "local"),
SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"),
SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"),
}
}
}